Documentação e OpenAPI
O contrato da API como arquivo versionado, de onde saem documentação, clientes e testes.
O problema: documentação que ninguém confere
Toda API tem alguma documentação: uma página na wiki, um README, uma coleção de pedidos salva numa ferramenta. O problema é que ela é escrita uma vez e esquecida. O campo criado_em virou criado_em_utc, um parâmetro passou a ser obrigatório, o erro 404 mudou de formato, e a página continua igual.
Quem integra descobre a diferença em produção. E quem mantém a API não sabe quais clientes dependem do que, porque o que a API promete não está escrito em lugar nenhum que uma máquina consiga ler.
O que é a OpenAPI
A OpenAPI é um formato padrão, em YAML ou JSON, para descrever uma API HTTP: as rotas, os parâmetros, os corpos dos pedidos, as respostas de cada status e os erros. Um trecho:
paths:
/pedidos/{id}:
get:
operationId: buscarPedido
parameters:
- name: id
in: path
required: true
schema: { type: integer, minimum: 1 }
responses:
"200":
description: O pedido
content:
application/json:
schema: { $ref: "#/components/schemas/Pedido" }
"404":
description: Pedido não encontradoOs esquemas dos dados usam o JSON Schema: tipos, campos obrigatórios, formatos, valores permitidos, mínimos e máximos. A versão 3.1 da OpenAPI adota o JSON Schema completo. A 3.0, ainda muito comum, usa uma variação com diferenças pequenas, como nullable: true no lugar de type: [string, "null"].
O nome antigo era Swagger, e ele ainda aparece nas ferramentas: o Swagger UI é a página interativa mais conhecida que mostra um arquivo OpenAPI.
O que sai do arquivo
O valor da OpenAPI não está na página bonita. Está em ser um arquivo que as ferramentas leem:
| A partir do arquivo | Exemplos de ferramentas |
|---|---|
| Documentação navegável, com "experimente aqui" | Swagger UI, Redoc, Scalar |
| Clientes e tipos gerados, em várias linguagens | openapi-generator, openapi-typescript |
| Servidor de mentira (mock), para o front-end começar antes do back-end | Prism |
| Validação dos pedidos que chegam | Bibliotecas que leem o arquivo e recusam o que não bate |
| Testes de contrato, que enviam pedidos e conferem as respostas | Schemathesis, Dredd |
| Comparação de versões, para achar mudanças incompatíveis | oasdiff |
| Verificação de estilo, como um linter | Redocly CLI, Spectral |
Duas formas de chegar ao arquivo
| O código gera o arquivo (code-first) | O arquivo vem primeiro (design-first) | |
|---|---|---|
| Como funciona | O framework lê as rotas, os tipos e as anotações e publica o arquivo | O arquivo é escrito e revisado antes; o código implementa o que ele diz |
| Vantagem | Nada a manter em separado; começa em minutos | O contrato é discutido antes de existir código, e vira a referência |
| Risco | O arquivo descreve o que o framework entendeu, não o que o código faz | O código pode se afastar do arquivo sem ninguém perceber |
| Defesa | Teste de contrato contra o próprio arquivo gerado | Teste de contrato, ou validar pedidos e respostas pelo arquivo |
As duas formas têm o mesmo ponto fraco: nada garante que o arquivo e o código concordam. A diferença é só de que lado o erro aparece. Os testes abaixo medem esse ponto fraco.
A especificação gerada pelo código também erra
Escrevemos a mesma API de pedidos (criar e buscar) com o gerador de OpenAPI mais usado em cada linguagem: Fastify, FastAPI, huma (Go), Spring com springdoc e ASP.NET Core 9. Depois, rodamos o Schemathesis contra cada servidor. Ele lê a especificação publicada pelo próprio servidor, gera pedidos válidos e inválidos a partir dela e confere se as respostas batem com o que o arquivo promete.
As cinco especificações foram reprovadas em pelo menos um ponto:
| Framework | O que o teste encontrou |
|---|---|
| Fastify (TypeScript) | Respondeu 400 a corpos inválidos, sem o 400 estar documentado; o 404 saiu com um tipo de conteúdo diferente do declarado |
| FastAPI (Python) | Documentou o total de entrada como "número ou qualquer texto", e recusou o texto "aaaa" que o próprio arquivo permitia |
| huma (Go) | Recusou 7.0 como inteiro, valor que o JSON Schema considera um inteiro válido |
| springdoc (Java) | Documentou o 404 com o esquema de Pedido, mas responde 404 sem corpo; nenhum campo de Pedido aparece como obrigatório |
| ASP.NET Core 9 (C#) | Documentou clienteId com mínimo 1, mas aceitou {} e criou um pedido com clienteId: 0 e total: 0 |
O último caso é o mais grave. A especificação diz que os dois campos são obrigatórios e que o cliente precisa ser maior que zero, mas o código não confere nada disso: as anotações de validação estavam lá, e o ASP.NET Core 9 só as usa para escrever a documentação. Quem lê o arquivo confia numa regra que não existe.
O caso do springdoc é sutil e tem efeito prático. Sem campos obrigatórios no esquema, um gerador de clientes cria tipos em que todo campo pode faltar, e o código de quem integra se enche de verificações desnecessárias.
O contrato escrito à mão também erra
Em seguida, escrevemos o contrato à mão, no estilo design-first: campos obrigatórios, valores em texto para não perder centavos, inteiros limitados a 32 bits e erros no formato Problem Details (application/problem+json). Rodamos o Schemathesis com esse contrato contra os cinco servidores:
| Servidor | Divergências com o contrato |
|---|---|
| FastAPI | Erros como application/json; recusou um total de "0.00", que o contrato permitia |
| Fastify | Erros como application/json |
| huma | Aceitou cliente_id: 2147483648, acima do máximo do contrato |
| Spring | 400 não documentado; 404 sem corpo e sem tipo de conteúdo |
| ASP.NET Core | status como "Aberto", fora dos valores permitidos; corpo sem os campos obrigatórios aceito com 201 |
Nem todas as divergências eram culpa do código. O total "0.00" mostrou uma regra que o contrato esqueceu: um pedido não pode ter total zero. O teste de contrato confere os dois lados, e cada divergência obriga a decidir quem está certo.
Mudanças que quebram quem integra
Com o contrato num arquivo versionado, cada mudança vira um diff que uma ferramenta consegue julgar. Fizemos quatro mudanças comuns no contrato e passamos as duas versões pelo oasdiff:
| Mudança | Classificação do oasdiff |
|---|---|
Novo campo opcional observacao na resposta | Compatível (não apareceu na lista) |
Novo campo obrigatório canal no corpo do pedido | Incompatível |
criado_em renomeado para criado_em_utc | Incompatível, nas duas rotas |
Novo valor cancelado no status da resposta | Incompatível, nas duas rotas |
O último resultado surpreende quem nunca pensou nele: acrescentar um valor ao status parece inofensivo, mas um cliente escrito com um switch sobre os três valores antigos não sabe tratar o quarto. É o mesmo cuidado do tópico de versionamento de API, agora conferido por uma máquina a cada mudança.
Por que isso importa quando a IA escreve o código
Um assistente que escreve uma API costuma gerar a documentação junto, e ela parece completa. Os problemas típicos:
- Anotações de validação que aparecem na documentação, mas não são aplicadas, como no ASP.NET Core 9.
- Respostas de erro (400, 404, 422) sem documentação, ou documentadas com o esquema de sucesso.
- Campos sem
required, e clientes gerados em que tudo pode faltar. - Dinheiro como
number, e datas semformat: date-time. - Um arquivo OpenAPI escrito uma vez e nunca conferido contra a API.
- Mudanças incompatíveis, como renomear um campo, feitas "só para padronizar", sem nenhuma verificação.
O próximo nível compara as formas de produzir e manter o contrato, e as ferramentas de cada etapa.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
OpenAPI em cinco minutos
Explique em no máximo cinco minutos de leitura o que é a OpenAPI, para alguém que já fez APIs REST, mas sempre as documentou numa wiki. Cubra só o essencial: o que o arquivo descreve, o que se faz com ele além da página de documentação, a diferença entre gerar o arquivo pelo código e escrevê-lo antes, e por que ele pode mentir sobre a API. Use o exemplo de uma API de pedidos. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Ler um arquivo OpenAPI por dentro
Quero aprender a ler e escrever um arquivo OpenAPI 3.1. Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma: 1. A estrutura geral: info, servers, paths, components. 2. Uma operação completa: parâmetros, corpo, respostas de sucesso e de erro. 3. Os esquemas com JSON Schema: tipos, required, formatos, enum, mínimos e padrões. 4. A reutilização com $ref em esquemas, respostas e parâmetros. 5. As diferenças entre a OpenAPI 3.0 e a 3.1 que mais aparecem na prática. Em cada etapa, me dê um trecho com um erro para eu encontrar.
Gerado pelo código ou escrito antes
Quero entender as formas de produzir o arquivo OpenAPI de uma API. Explique: 1. Gerar pelo código, escrever o arquivo antes, e gerar a partir de esquemas compartilhados. 2. Por que a especificação gerada pelo código pode prometer regras que o código não aplica, com um exemplo. 3. Por que o arquivo escrito à mão se afasta do código com o tempo. 4. Como o teste de contrato pega os dois problemas. 5. Quando cada forma compensa. Depois me descreva três projetos e pergunte qual forma eu escolheria. Corrija as minhas respostas.
Mudanças que quebram quem integra
Quero aprender a reconhecer mudanças incompatíveis num contrato OpenAPI. Me mostre, uma de cada vez, dez mudanças num contrato, como renomear um campo, acrescentar um campo obrigatório na entrada, acrescentar um valor num enum da resposta ou mudar um tipo. Para cada uma, pergunte se ela quebra os clientes existentes e por quê, e só depois explique a resposta certa. No final, explique como uma ferramenta de comparação de contratos na integração contínua evita essas mudanças sem aviso.