Neste guia, você vai ver como desenhar uma API que facilita a vida de quem testa: um contrato OpenAPI que vale de verdade, códigos HTTP usados com critério, um corpo de erro padronizado, idempotência em operações de escrita, paginação estável, versionamento previsível e autenticação que dá para exercitar em testes automatizados. A ideia é simples: quanto mais previsível a API, menos testes frágeis e menos conversas do tipo “mas na minha máquina funciona”.
Trabalho com QA desde 2010 e, em boa parte dos projetos, a maior dificuldade em testar APIs não foi a ferramenta, e sim a falta de regras claras do lado de quem a construiu. Este texto é escrito para devs, mas com o olhar de quem vai automatizar os testes depois.
OpenAPI como contrato, não como enfeite
Muita equipe gera a especificação OpenAPI no fim, só para ter uma página bonita de documentação. O problema é que a spec acaba descrevendo o que alguém lembrou, não o que a API faz. Para ser testável, o OpenAPI precisa ser tratado como contrato: a fonte da verdade sobre rotas, parâmetros, esquemas de request e response e, principalmente, sobre as respostas de erro.
Algumas práticas que fazem diferença:
- Documente todas as respostas, não só o 200. Se a rota pode devolver 400, 404 ou 409, isso precisa estar na spec com o esquema do corpo.
- Marque campos obrigatórios com
requirede useadditionalProperties: falsequando fizer sentido. Isso permite que testes de contrato detectem campos inesperados. - Use formatos e enums (
format: uuid,format: date-time, listas fechadas de status). Eles viram validação automática nos testes. - Valide a spec no pipeline: um teste que compara a resposta real com o esquema declarado pega divergências antes do cliente.
Um trecho mínimo de contrato para criar um pedido ficaria assim:
paths:
/pedidos:
post:
parameters:
- in: header
name: Idempotency-Key
required: true
schema: { type: string, format: uuid }
responses:
"201":
description: Pedido criado
"409":
description: Conflito de estado
content:
application/problem+json:
schema: { $ref: "#/components/schemas/Problem" }
"422":
description: Regra de negócio violada
content:
application/problem+json:
schema: { $ref: "#/components/schemas/Problem" }
Códigos HTTP corretos: cada um com seu significado
Responder 200 com {"erro": true} no corpo, ou 500 para qualquer problema, obriga o teste a abrir o corpo e interpretar texto. Quando o status é usado com critério, a primeira asserção já diz muito. Um guia prático:
- 400 Bad Request: a requisição está malformada. JSON inválido, tipo errado, parâmetro obrigatório ausente.
- 401 Unauthorized: não há credencial válida. Token ausente, expirado ou com assinatura inválida.
- 403 Forbidden: a pessoa está autenticada, mas não tem permissão para aquela ação ou aquele recurso.
- 404 Not Found: o recurso não existe (ou, por segurança, você opta por não revelar que existe).
- 409 Conflict: a requisição conflita com o estado atual. Cadastro duplicado, edição sobre versão desatualizada, mesma chave de idempotência com corpo diferente.
- 422 Unprocessable Content: o formato está certo, mas o conteúdo viola uma regra de negócio. Data de entrega no passado, quantidade acima do estoque.
- 429 Too Many Requests: limite de taxa atingido. Idealmente acompanhado do cabeçalho
Retry-After. - 500 Internal Server Error: falha inesperada do servidor. Nunca deveria ser resposta “normal” para entrada inválida; se um teste de dados ruins recebe 500, isso é bug.
A fronteira entre 400 e 422 costuma gerar debate. O mais importante é que a equipe escolha uma regra, registre no contrato e aplique em todas as rotas. Consistência vale mais do que a escolha perfeita. A semântica oficial desses códigos está na RFC 9110, “HTTP Semantics” (https://www.rfc-editor.org/rfc/rfc9110, consultado em 13/10/2026), com exceção do 429, definido na RFC 6585 (https://www.rfc-editor.org/rfc/rfc6585, consultado em 13/10/2026).
Corpo de erro padronizado com Problem Details
Se cada rota devolve erro num formato diferente, cada teste precisa de um parser diferente. A RFC 9457, “Problem Details for HTTP APIs” (https://www.rfc-editor.org/rfc/rfc9457, consultado em 13/10/2026), define um formato comum com o tipo de mídia application/problem+json e os membros type, title, status, detail e instance. Ela também permite membros de extensão, úteis para listar erros por campo.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.exemplo.dev/problemas/validacao",
"title": "Dados do pedido inválidos",
"status": 422,
"detail": "Um ou mais campos violam regras de negócio.",
"instance": "/pedidos",
"erros": [
{ "campo": "quantidade", "codigo": "ACIMA_DO_ESTOQUE" },
{ "campo": "dataEntrega", "codigo": "DATA_NO_PASSADO" }
],
"traceId": "4f1c2a9e-7b3d-4c8a-9e21-0d5f6a7b8c90"
}
Para testes, os ganhos são diretos: o type é estável e pode ser comparado sem depender de texto traduzido; os códigos por campo permitem asserções precisas; e um traceId ajuda a cruzar a falha com os logs. Evite colocar stack trace ou detalhes internos no detail: isso é ruim para segurança e deixa o teste dependente de mensagens que mudam a cada refatoração.
Idempotência com Idempotency-Key
Redes falham. Um cliente que envia POST /pedidos, não recebe resposta e tenta de novo pode criar dois pedidos. O padrão do cabeçalho Idempotency-Key resolve isso: o cliente gera uma chave única (um UUID, por exemplo) e o servidor guarda o resultado da primeira execução associado a ela. Ao receber a mesma chave de novo, devolve a mesma resposta, sem repetir o efeito.
POST /pedidos
Idempotency-Key: 9b2e6c1a-3f4d-4e8b-a7c2-5d1e0f9a8b37
Content-Type: application/json
{ "produtoId": "p-100", "quantidade": 2 }
Regras que tornam esse comportamento testável:
- Mesma chave e mesmo corpo: mesma resposta (mesmo status, mesmo ID do recurso criado).
- Mesma chave com corpo diferente: erro claro, em geral 409 ou 422, documentado no contrato.
- Chave ausente em rota que exige: 400 com Problem Details explicando o motivo.
- Prazo de retenção da chave definido e documentado, para que o teste saiba o que esperar.
Num teste automatizado, basta enviar a mesma requisição duas vezes e comparar as respostas. Para gerar chaves rapidamente durante testes manuais, use o gerador de UUID. O cabeçalho ainda é tratado como proposta no IETF, então vale descrever no seu contrato exatamente o comportamento adotado.
Paginação e ordenação estáveis
Um teste que pega “o primeiro item da lista” só é confiável se a ordem for determinística. Ordenar apenas por data de criação, quando vários registros têm o mesmo instante, produz resultados que variam entre chamadas. Sempre inclua um critério de desempate único, como o ID.
- Declare a ordenação padrão no contrato e aceite um parâmetro explícito, como
sort=criadoEm,id. - Prefira paginação por cursor em listas que mudam muito; offset pode pular ou repetir itens quando há inserções no meio.
- Devolva metadados consistentes, como o cursor da próxima página e o tamanho usado.
- Defina um limite máximo de página e o que acontece se ele for excedido (ajuste silencioso ou 400).
{
"dados": [
{ "id": "p-101", "criadoEm": "2026-10-13T12:00:00Z" },
{ "id": "p-102", "criadoEm": "2026-10-13T12:00:00Z" }
],
"pagina": {
"tamanho": 2,
"proximoCursor": "eyJpZCI6InAtMTAyIn0"
}
}
Para conferir se duas respostas de lista são equivalentes, ou descobrir o que mudou entre versões, o comparador de JSON poupa bastante tempo.
Versionamento sem surpresas
Não existe um único jeito certo de versionar: na URL (/v1/pedidos), em cabeçalho ou por tipo de mídia. O que torna a API testável é a política ser clara:
- Mudanças compatíveis (novo campo opcional, nova rota) não exigem nova versão, e os testes devem tolerar campos extras quando o contrato permitir.
- Mudanças incompatíveis (remover campo, mudar tipo, mudar significado de status) geram nova versão, com a anterior mantida por um período anunciado.
- Cada versão tem sua própria spec OpenAPI, e a suíte de contrato roda contra todas as versões ativas.
Autenticação testável
Autenticação é onde muitos testes travam: tokens que só se obtêm por um fluxo manual, usuários de teste compartilhados que alguém bloqueia sem querer, ambientes que exigem captcha. Algumas decisões que ajudam:
- Ofereça, em ambientes de teste, um jeito automatizável de obter token (por exemplo, client credentials ou um endpoint de login documentado).
- Tenha usuários de teste com perfis conhecidos: um padrão, um bloqueado, um sem permissão. Assim 401 e 403 viram casos fáceis de reproduzir.
- Diferencie claramente token ausente, expirado e com escopo insuficiente, cada um com seu status e seu
typede problema. - Permita configurar tempos de expiração curtos em teste, para validar renovação sem esperar horas.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://api.exemplo.dev/problemas/escopo-insuficiente",
"title": "Permissão insuficiente",
"status": 403,
"detail": "O token não possui o escopo pedidos:escrita."
}
Quando um teste falha com 401 ou 403, a primeira coisa a fazer é inspecionar o token: emissor, expiração, escopos. O decodificador de JWT mostra o cabeçalho e o payload sem enviar nada para servidores externos.
Checklist rápido antes de liberar uma rota
- Todas as respostas, inclusive de erro, estão no OpenAPI?
- Entrada inválida nunca retorna 500?
- Erros seguem Problem Details com
typeestável? - Escritas que podem ser repetidas aceitam
Idempotency-Key? - Listas têm ordenação determinística e paginação documentada?
- Existe forma automatizável de autenticar com perfis de teste distintos?
Próximos passos
A melhor forma de fixar esses conceitos é praticar. A API de prática do AppsQuality Lab tem especificação OpenAPI e usuários de teste como standard_user e locked_user, ideais para exercitar casos de 401, 403 e validação de contrato. Use também:
- Decodificador de JWT, para inspecionar tokens em falhas de autenticação.
- Comparador de JSON, para validar idempotência e diferenças entre respostas.
- Formatador de JSON, para ler corpos de erro com clareza.
- Gerador de UUID, para criar chaves de idempotência.
