Documentar uma API Ktor com OpenAPI não é burocracia: é uma forma de transformar rotas, DTOs, códigos de status e regras de autenticação em um contrato que frontend, mobile, QA e integrações externas conseguem consultar sem adivinhar o comportamento do backend. Em times pequenos, isso evita mensagens soltas no chat. Em times maiores, vira base para SDKs, testes de contrato e versionamento.

Este tutorial mostra um caminho prático para usar Swagger UI e OpenAPI em Ktor sem perder o estilo idiomático do Kotlin. A ideia é começar com um contrato simples, expor uma documentação navegável, manter exemplos úteis e conectar o tema com o que você já usa em REST APIs com Kotlin, Ktor para backend e testes automatizados.

Quando vale documentar uma API Ktor com OpenAPI?

Vale documentar quando a API tem mais de um consumidor, quando uma pessoa nova precisa entender o contrato rapidamente ou quando mudanças de resposta podem quebrar outro sistema. Mesmo em um projeto de portfólio, uma página Swagger bem feita passa uma mensagem forte: você sabe construir backend pensando em consumo real, não apenas em rotas que funcionam localmente.

Os sinais de que OpenAPI deve entrar no projeto são claros:

  • o app Android, web ou serviço parceiro consome a API;
  • existem endpoints privados e públicos com autenticação diferente;
  • DTOs mudam com frequência e precisam de exemplos;
  • QA precisa saber quais erros esperar;
  • você quer gerar clientes HTTP ou validar contrato em CI;
  • há vagas ou entrevistas pedindo experiência com APIs documentadas.

Se o projeto ainda está no começo, não espere “ficar grande” para documentar. Um contrato pequeno e atualizado é melhor que uma documentação enorme escrita depois, quando ninguém lembra por que cada endpoint existe.

Modelo mental: contrato primeiro, implementação sempre perto

OpenAPI descreve a superfície HTTP da sua aplicação: caminhos, métodos, parâmetros, corpo de requisição, respostas, schemas, autenticação e metadados. O erro comum é tratar o arquivo openapi.yaml como um artefato separado que envelhece longe do código.

No Ktor, pense em três camadas:

  1. rotas Ktor definem o comportamento real;
  2. DTOs Kotlin representam entrada e saída;
  3. contrato OpenAPI explica como consumidores devem chamar a API.

O objetivo é manter essas três camadas próximas. Se você usa geração automática disponível na versão do seu stack, ótimo. Se ainda mantém YAML manual, coloque o arquivo no repositório, revise junto do código e valide no pipeline. O ponto não é a ferramenta perfeita; é impedir que contrato e implementação se contradigam.

Dependências básicas para Swagger UI em Ktor

O ecossistema Ktor permite diferentes abordagens. Em muitos projetos, você verá uma combinação de plugin de OpenAPI/Swagger, arquivo YAML servido pela aplicação e uma rota de interface visual. O exemplo abaixo usa uma estrutura didática: mantenha openapi/documentation.yaml como fonte do contrato e exponha Swagger UI em /swagger.

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.3.20"
    id("io.ktor.plugin") version "3.4.0"
    kotlin("plugin.serialization") version "2.3.20"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-server-netty-jvm")
    implementation("io.ktor:ktor-server-content-negotiation-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")

    // Use os artefatos de OpenAPI/Swagger compatíveis com a sua versão do Ktor.
    implementation("io.ktor:ktor-server-openapi")
    implementation("io.ktor:ktor-server-swagger")

    testImplementation("io.ktor:ktor-server-tests-jvm")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}

Antes de copiar versões para produção, confira a documentação oficial e a versão usada pelo seu projeto. Se você está migrando de Ktor 2.x para Ktor 3.x, leia também o resumo de Ktor 3.4 com OpenAPI e streaming para entender o que mudou no suporte de documentação.

Criando uma API de exemplo

Vamos usar uma API de tarefas porque ela é pequena o bastante para caber em um tutorial, mas real o suficiente para demonstrar listagem, criação, erros e autenticação futura.

import kotlinx.serialization.Serializable

@Serializable
data class TarefaResponse(
    val id: Long,
    val titulo: String,
    val concluida: Boolean
)

@Serializable
data class CriarTarefaRequest(
    val titulo: String
)

@Serializable
data class ErroResponse(
    val codigo: String,
    val mensagem: String
)

A rota Ktor pode começar simples:

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.tarefaRoutes() {
    val tarefas = mutableListOf(
        TarefaResponse(1, "Documentar API Ktor", false)
    )

    routing {
        route("/api/v1/tarefas") {
            get {
                call.respond(tarefas)
            }

            post {
                val request = call.receive<CriarTarefaRequest>()

                if (request.titulo.isBlank()) {
                    call.respond(
                        HttpStatusCode.BadRequest,
                        ErroResponse(
                            codigo = "titulo_obrigatorio",
                            mensagem = "Informe um título para a tarefa."
                        )
                    )
                    return@post
                }

                val nova = TarefaResponse(
                    id = tarefas.size.toLong() + 1,
                    titulo = request.titulo,
                    concluida = false
                )
                tarefas += nova

                call.respond(HttpStatusCode.Created, nova)
            }
        }
    }
}

Essa implementação é propositalmente simples. Em uma aplicação real, você provavelmente teria service, repository, banco de dados com Exposed ou outro mecanismo de persistência, além de validação mais completa. O contrato OpenAPI precisa refletir o comportamento observável, não a organização interna.

Escrevendo o primeiro openapi.yaml

Crie um arquivo como src/main/resources/openapi/documentation.yaml. Ele deve começar com metadados claros e schemas pequenos.

openapi: 3.1.0
info:
  title: API de Tarefas Kotlin Brasil
  version: 1.0.0
  description: API de exemplo construída com Ktor e Kotlin.
servers:
  - url: https://api.exemplo.com
    description: Produção
  - url: http://localhost:8080
    description: Desenvolvimento local
paths:
  /api/v1/tarefas:
    get:
      summary: Lista tarefas
      operationId: listarTarefas
      tags: [Tarefas]
      responses:
        "200":
          description: Lista de tarefas retornada com sucesso.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TarefaResponse"
              examples:
                exemplo:
                  value:
                    - id: 1
                      titulo: Documentar API Ktor
                      concluida: false
    post:
      summary: Cria uma tarefa
      operationId: criarTarefa
      tags: [Tarefas]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CriarTarefaRequest"
      responses:
        "201":
          description: Tarefa criada.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TarefaResponse"
        "400":
          description: Requisição inválida.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErroResponse"
components:
  schemas:
    CriarTarefaRequest:
      type: object
      required: [titulo]
      properties:
        titulo:
          type: string
          minLength: 1
          example: Revisar contrato OpenAPI
    TarefaResponse:
      type: object
      required: [id, titulo, concluida]
      properties:
        id:
          type: integer
          format: int64
          example: 1
        titulo:
          type: string
          example: Documentar API Ktor
        concluida:
          type: boolean
          example: false
    ErroResponse:
      type: object
      required: [codigo, mensagem]
      properties:
        codigo:
          type: string
          example: titulo_obrigatorio
        mensagem:
          type: string
          example: Informe um título para a tarefa.

Esse YAML já entrega valor: qualquer pessoa consegue abrir o contrato e entender como listar e criar tarefas. Repare que os exemplos não são enfeite. Eles ajudam consumidores a saber formato, nomes de campos, tipos e respostas esperadas.

Expondo Swagger UI no Ktor

Com o contrato dentro de resources, exponha uma rota para a documentação. O formato exato pode variar conforme a versão do Ktor e o plugin escolhido, mas a ideia é esta:

import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.routing.*

fun Application.module() {
    install(ContentNegotiation) {
        json()
    }

    tarefaRoutes()

    routing {
        swaggerUI(
            path = "swagger",
            swaggerFile = "openapi/documentation.yaml"
        )
    }
}

Depois de subir a aplicação, acesse http://localhost:8080/swagger. A interface deve mostrar os endpoints, schemas e exemplos definidos no contrato.

Se você preferir servir apenas o JSON ou YAML para uma ferramenta externa, exponha um endpoint como /openapi.yaml e publique a UI em outro lugar. O importante é que o contrato esteja disponível no mesmo ciclo de deploy da API.

Documentando erros e autenticação

Um contrato útil não mostra apenas o caminho feliz. Ele documenta erros comuns, códigos de status e regras de autenticação. Para APIs com JWT, adicione um security scheme:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - bearerAuth: []

Em endpoints públicos, você pode sobrescrever com security: []. Em endpoints privados, declare respostas como 401 e 403 com payload de erro. Isso evita uma frustração comum: o frontend descobrir em runtime que a API retorna um formato de erro diferente em cada rota.

Para Ktor, alinhe o contrato com o bloco de autenticação:

import io.ktor.server.auth.*
import io.ktor.server.routing.*

fun Application.rotasPrivadas() {
    routing {
        authenticate("auth-jwt") {
            get("/api/v1/perfil") {
                // resposta do perfil autenticado
            }
        }
    }
}

Se a rota está dentro de authenticate, o OpenAPI deve deixar isso explícito. Se a rota aceita chamadas anônimas, não marque como protegida por acidente.

Mantendo DTOs e schemas sincronizados

O maior risco de OpenAPI manual é divergência. Para reduzir isso, crie uma rotina simples:

  • toda PR que muda request ou response precisa alterar o contrato;
  • exemplos devem ser atualizados junto com os DTOs;
  • códigos de erro precisam ter nome estável;
  • campos removidos devem ser tratados como breaking change;
  • o pipeline deve validar o YAML.

Você pode usar ferramentas como swagger-cli, redocly, spectral ou geradores de cliente em CI. Mesmo que não gere código, validar o arquivo já captura indentação quebrada, $ref inválido e schemas incompletos.

Um teste simples no projeto também ajuda:

import kotlin.test.Test
import kotlin.test.assertTrue

class OpenApiContractTest {
    @Test
    fun `contrato openapi deve existir no classpath`() {
        val resource = javaClass.classLoader
            .getResource("openapi/documentation.yaml")

        assertTrue(resource != null, "Contrato OpenAPI não encontrado")
    }
}

Esse teste não valida todo o contrato, mas evita o erro básico de esquecer o arquivo fora do pacote final. Para validação forte, combine com uma ferramenta especializada no pipeline de CI/CD para Kotlin.

Versionamento de API e contrato

Versionar API não é apenas colocar /v1 na URL. O contrato deve indicar a versão do documento, e mudanças incompatíveis precisam de cuidado. Exemplos de breaking changes:

  • remover campo de resposta usado por clientes;
  • mudar tipo de id de número para string;
  • tornar obrigatório um campo antes opcional;
  • alterar o formato de erro;
  • trocar códigos HTTP sem compatibilidade.

Mudanças aditivas geralmente são seguras: adicionar campo opcional, novo endpoint, novo valor em enum documentado como extensível. Em Kotlin, prefira DTOs explícitos de request e response em vez de expor entidades internas diretamente. Isso facilita evoluir o banco sem quebrar contrato público.

Checklist para uma documentação Ktor que ajuda de verdade

Antes de considerar sua documentação pronta, passe por este checklist:

  • a página Swagger abre em ambiente local;
  • cada endpoint tem summary, operationId e tag;
  • requests têm exemplos realistas;
  • respostas de sucesso e erro estão documentadas;
  • autenticação está clara;
  • campos obrigatórios e opcionais estão corretos;
  • DTOs não expõem senha, token interno ou detalhes de banco;
  • o contrato passa em validação automatizada;
  • a documentação é atualizada no mesmo commit da mudança de rota.

Esse nível de disciplina já coloca seu projeto acima da média de muitos backends de portfólio.

Erros comuns ao usar Swagger com Ktor

1. Documentar só 200 OK. APIs reais falham. Mostre 400, 401, 403, 404, 409 e 500 quando fizer sentido.

2. Copiar DTO interno para o contrato. Entidades de banco e DTOs públicos têm responsabilidades diferentes. O contrato deve representar o que o consumidor pode depender.

3. Deixar exemplos genéricos demais. string e example vazio não ajudam. Use dados parecidos com o domínio real.

4. Esquecer autenticação por rota. Uma UI Swagger linda não resolve nada se o consumidor não sabe quando enviar bearer token.

5. Não validar em CI. YAML quebrado geralmente só aparece quando alguém tenta abrir a documentação. Valide antes do deploy.

Próximos passos

Depois que a documentação estiver funcionando, evolua para testes de contrato e geração de cliente. Um app Android, por exemplo, pode usar um cliente gerado ou pelo menos validar que os modelos esperados batem com o contrato publicado. Para backend, combine OpenAPI com testes em Kotlin, observabilidade e Docker para fechar o ciclo profissional.

Se seu foco é comparar stacks, vale ver como a documentação de APIs aparece em FastAPI e Django REST em Python e em frameworks Go para APIs. O vocabulário muda, mas a responsabilidade é a mesma: contrato claro, exemplos úteis e mudança controlada.

FAQ rápido

Ktor gera OpenAPI automaticamente?

Depende da versão e dos plugins usados no projeto. O suporte evoluiu bastante, mas muitos times ainda mantêm um openapi.yaml revisado junto do código. O melhor caminho é escolher uma abordagem que seu time consiga manter sem deixar a documentação envelhecer.

Swagger UI deve ficar público em produção?

Depende do produto. Para API pública, normalmente sim. Para API interna, você pode proteger a rota, expor apenas na intranet ou publicar a documentação em um portal separado. Nunca exponha endpoints administrativos ou exemplos com dados sensíveis.

OpenAPI substitui testes?

Não. OpenAPI descreve o contrato esperado. Testes verificam se a implementação cumpre esse contrato. O ideal é usar os dois: documentação para consumo humano e validação automatizada para evitar regressões.

Preciso usar annotations?

Não necessariamente. Uma vantagem do Kotlin com Ktor é manter rotas em DSL. Você pode documentar por YAML, por DSL de documentação ou por plugins que aproximam contrato e rota. Evite escolher uma solução apenas porque parece “mais automática”; escolha a que fica sustentável no repositório.