Melhores práticas para tratamento de erros em APIs REST
As respostas de erro da sua API fazem parte do contrato. Clientes as analisam, a lógica de retentativa se baseia nelas e engenheiros de suporte as procuram às 2 da manhã. Ainda assim, muitas equipes detalham o caminho feliz e deixam os erros seguirem o padrão do framework — resultando em formatos inconsistentes, respostas 200 com "success": false e rastreamentos de pilha expondo o esquema do banco de dados.
Este guia mostra como projetar um contrato de erros consistente: escolher o código HTTP correto, padronizar o corpo com RFC 9457 Detalhes do Problema, separar códigos de mensagens humanas, indicar se o erro pode ser retentado, proteger informações sensíveis e testar cada caminho de falha no Apidog.
Comece com o código de status
O HTTP já fornece uma camada de semântica. A RFC 9110 define que:
- 4xx indica um erro do cliente; repetir a mesma requisição normalmente falhará.
- 5xx indica uma falha do servidor ou de uma dependência; a requisição pode estar correta.
Clientes genéricos, proxies, caches e bibliotecas de retentativa usam essa informação sem necessariamente ler o JSON do corpo. Portanto, escolha o status antes de definir o payload.
Consulte a referência de códigos de status HTTP do MDN e use esta tabela como guia:
| Situação | Usar | Não usar | Por quê |
|---|---|---|---|
| JSON inválido, tipo de conteúdo incorreto ou campo obrigatório ausente | 400 Bad Request |
422 |
O servidor não consegue analisar ou entender a requisição |
| Requisição válida, mas que viola regras de negócio, como valor negativo ou moeda não suportada | 422 Unprocessable Content |
400 |
A sintaxe está correta; os valores não estão |
| Credenciais ausentes, expiradas ou inválidas | 401 Unauthorized |
403 |
O cliente ainda não provou sua identidade; envie WWW-Authenticate
|
| Credenciais válidas, mas sem permissão | 403 Forbidden |
401 |
A identidade é conhecida, mas o acesso foi negado |
| Recurso inexistente ou cuja existência não será confirmada | 404 Not Found |
410 |
É o padrão seguro e também evita expor recursos sondados sem autorização |
| Recurso removido deliberada e permanentemente | 410 Gone |
404 |
Permite que clientes e rastreadores removam suas referências |
| Chave duplicada, versão antiga ou colisão de edição | 409 Conflict |
400 |
A requisição é válida, mas conflita com o estado atual |
| Cliente excedeu o limite de requisições | 429 Too Many Requests |
503 |
Inclua Retry-After para orientar o recuo |
| Exceção não tratada no seu código | 500 Internal Server Error |
502 |
O próprio servidor falhou |
| Upstream retornou uma resposta inválida ao gateway | 502 Bad Gateway |
500 |
A falha ocorreu a jusante da borda |
| Servidor sobrecarregado ou em manutenção | 503 Service Unavailable |
500 |
É uma condição temporária; inclua Retry-After quando possível |
| Upstream expirou | 504 Gateway Timeout |
500 |
Diferencia uma dependência lenta de um código quebrado |
Dois casos exigem atenção especial:
-
401vs.403é uma decisão de segurança. Retornar403para um chamador não autenticado pode revelar que o recurso existe. -
429deve incluirRetry-After. Sem essa orientação, clientes podem repetir a requisição em loops apertados. Consulte também o guia de limitação de taxa de API.
Padronize o corpo com RFC 9457
Depois de escolher o status, use um formato consistente para todas as respostas de erro. O padrão recomendado é a RFC 9457 Detalhes do Problema, com o tipo de mídia:
Content-Type: application/problem+json
O formato define cinco membros principais:
-
type: URI que identifica a categoria do erro; -
title: resumo curto e legível; -
status: código HTTP, repetido por conveniência; -
detail: descrição desta ocorrência; -
instance: URI que identifica a falha específica.
Outros campos entram como membros de extensão. O explicador da RFC 9457 detalha a especificação e sua relação com a RFC 7807.
Exemplo de erro de validação em um endpoint de pagamentos:
POST /v1/payments HTTP/1.1
Content-Type: application/json
{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "A validação da requisição falhou",
"status": 422,
"detail": "Um ou mais campos falharam na validação.",
"instance": "/v1/payments/requests/req_9f3c1a7b",
"code": "PAYMENT_VALIDATION_FAILED",
"errors": [
{
"field": "amount",
"code": "AMOUNT_NOT_POSITIVE",
"message": "o valor deve ser um inteiro positivo em unidades menores"
}
],
"request_id": "req_9f3c1a7b"
}
O membro de extensão errors[] permite que um frontend associe cada erro ao campo exato do formulário, em vez de exibir uma mensagem genérica. Escolha um formato estável para os caminhos — JSON Pointer ou notação pontilhada — e use-o em toda a API.
Aplique o mesmo envelope aos erros gerados pelo framework, gateway e balanceador. Se os handlers retornam application/problem+json, mas o balanceador devolve uma página HTML para 502, cada cliente precisará manter dois parsers.
Separe códigos de mensagens humanas
O exemplo contém code e message porque eles têm funções diferentes.
Códigos legíveis por máquina
Códigos como estes fazem parte do contrato:
AMOUNT_NOT_POSITIVE
CURRENCY_UNSUPPORTED
IDEMPOTENCY_KEY_REUSED
Eles devem ser estáveis, documentados e enumeráveis. Nunca obrigue clientes a analisar texto:
if (message.includes("positive")) {
// ...
}
Nesse momento, alterar uma frase passa a ser uma mudança incompatível.
Mensagens humanas
Mensagens são voltadas para desenvolvedores que leem logs e devem explicar o problema e sua correção. Prefira:
o valor deve ser um inteiro positivo em unidades menores
a:
valor inválido
Você pode melhorar ou localizar a mensagem sem alterar o código. Essa separação é ainda mais importante quando os consumidores incluem agentes autônomos: clientes baseados em LLM se recuperam melhor de erros estruturados e autoexplicativos. Veja também o guia sobre mensagens de erro de API para agentes de IA.
Nunca exponha informações sensíveis
Respostas de erro são uma fonte valiosa de reconhecimento para atacantes. O middleware de erro deve impedir que estes dados cheguem ao cliente:
- rastreamentos de pilha, nomes de classes e caminhos de arquivo;
- SQL bruto, fragmentos de consulta e erros de ORM;
- nomes de host internos, IPs, portas e nomes de serviço;
- versões de bibliotecas e banners de frameworks;
- segredos, tokens e strings de conexão presentes na exceção;
- confirmação de que uma conta existe, especialmente em login e redefinição de senha.
Capture a exceção na fronteira, registre os detalhes completos no servidor e associe tudo a um ID de requisição. Para o cliente, retorne apenas uma mensagem genérica:
{
"type": "https://api.example.com/problems/internal-error",
"title": "Erro interno do servidor",
"status": 500,
"detail": "Um erro interno ocorreu.",
"request_id": "req_51ad0"
}
Os logs ficam com a informação técnica; o cliente e o suporte ficam com o identificador necessário para localizar a ocorrência.
Indique quais erros podem ser retentados
Cada erro responde à pergunta: “devo tentar novamente?”. Torne essa decisão explícita no contrato.
Como regra geral:
-
429,502,503e504podem ser retentados com backoff exponencial e jitter; -
500pode justificar uma retentativa cautelosa; - a maioria dos demais
4xxé terminal; - repetir
401,403,404ou422com os mesmos dados normalmente desperdiça cota e polui os logs.
408 Request Timeout merece cuidado: a requisição pode ter sido processada depois que o cliente desistiu. Endpoints de mutação devem aceitar chaves de idempotência para evitar cobranças ou criações duplicadas.
Você também pode adicionar uma extensão explícita:
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Muitas requisições",
"status": 429,
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 30
}
A flag retryable permite substituir a regra padrão. Por exemplo, um erro 500 específico pode ser marcado como terminal quando a retentativa puder corromper o estado. Documente esse comportamento uma vez para que todos os SDKs adotem a mesma estratégia.
Use IDs de correlação e versione o contrato
Duas decisões simples evitam problemas futuros.
IDs de requisição
Aceite um cabeçalho X-Request-Id ou gere um quando ele não existir. Inclua o valor em:
- todas as linhas de log;
- todos os corpos de erro;
- tickets e respostas de suporte.
Em sistemas distribuídos, propague também o traceparent do W3C para acompanhar a requisição entre serviços.
Versionamento
Trate o contrato de erro como parte da versão da API.
Mudanças normalmente compatíveis:
- adicionar um membro de extensão;
- adicionar um novo código de erro.
Mudanças incompatíveis:
- renomear
errors[].field; - alterar o significado de um código existente;
- trocar um formato ad hoc por Detalhes do Problema sem planejar a migração.
Mantenha as URIs de type antigas estáveis e crie novas URIs para novas semânticas. Documente que clientes devem ignorar membros e códigos desconhecidos, em vez de tratá-los como falhas. Essa regra de compatibilidade permite evoluir o contrato sem necessariamente criar uma versão v2.
Teste todos os caminhos de erro no Apidog
Contratos de erro apodrecem quando ninguém os exercita. O caminho feliz aparece em todas as demonstrações; o 422 só é executado quando um cliente real o encontra.
Transforme cada caso de falha em um cenário de primeira classe no seu conjunto de testes.
Cenários de teste no servidor
Para cada endpoint, cubra casos como:
- autenticação ausente →
401; - permissão insuficiente →
403; - valor negativo →
422, comerrors[0].codeigual aAMOUNT_NOT_POSITIVE; - tráfego acima do limite →
429, com o cabeçalhoRetry-After; - dependência indisponível →
503; - dependência expirada →
504.
As asserções do Apidog verificam status, cabeçalhos e campos do corpo sem scripts complexos. Valide também o payload inteiro contra o esquema JSON de Detalhes do Problema, fazendo o CI falhar quando o formato se desviar. Consulte o guia de asserções de API.
Servidores mock para clientes
Frontend e SDKs precisam ser desenvolvidos contra respostas 4xx e 5xx antes que o backend consiga produzi-las sob demanda.
Os servidores mock do Apidog retornam os corpos exatos de Detalhes do Problema definidos na especificação. Assim, é possível simular:
-
503comRetry-After: 120; -
409causado por submissão duplicada; - payloads completos de validação com
errors[].
Depois, observe como o cliente renderiza a mensagem e decide se deve retentar — sem stubs Express feitos à mão ou alterações temporárias no backend.
Modele o contrato, transforme-o em cenários e mocks e conecte tudo ao CI. Você pode baixar o Apidog e começar gratuitamente; importar uma especificação OpenAPI existente permite criar respostas de erro mockáveis em poucos minutos.
FAQ
Devo usar 400 ou 422 para erros de validação?
Use 400 quando a requisição estiver malformada e o servidor não puder entendê-la: JSON inválido, tipo de conteúdo incorreto ou campo obrigatório ausente.
Use 422 quando a requisição for analisada corretamente, mas os valores violarem regras de domínio, como um pagamento negativo ou uma moeda não suportada.
A distinção ajuda o cliente a diagnosticar o problema: 422 significa “corrija seus dados”; 400 significa “corrija o formato da requisição”. O mais importante é aplicar a mesma regra em todos os endpoints.
O que é application/problem+json?
É o tipo de mídia definido pela RFC 9457 para Detalhes do Problema, um formato padrão de erro JSON para APIs HTTP. Ele contém type, title, status, detail e instance, além de extensões como errors[].
Como o tipo de mídia é padronizado, clientes genéricos e middleware podem reconhecer o formato sem configuração específica.
Quais erros HTTP os clientes devem retentar automaticamente?
Retente 429, 502, 503 e 504 usando backoff exponencial com jitter e respeitando Retry-After. Trate 500 como candidato a uma retentativa cautelosa.
Não retente outras respostas 4xx sem corrigir a requisição. Em endpoints de mutação, use chaves de idempotência para impedir cobranças ou criações duplicadas.
Como testar respostas de erro sem quebrar o backend?
Simule-as. Aponte o cliente para um servidor mock do Apidog que retorne os corpos 4xx e 5xx definidos na especificação. Teste a renderização, a lógica de retentativa e cada variação do contrato.
No servidor, crie cenários para payloads inválidos, autenticação ausente, permissões insuficientes e tráfego intenso. Faça asserções sobre status, cabeçalhos e esquema do corpo. Execute os dois lados no CI para manter o contrato íntegro sem forçar falhas manualmente.
Top comments (0)