Quando sua API retorna um erro, o consumidor precisa de duas coisas: saber o que aconteceu, e saber o que fazer. A maioria das APIs falha em entregar ambas.
O padrão mais comum que vejo em codebases: um catch genérico que retorna 500 com uma mensagem vaga. O consumidor não sabe se é problema dele, da rede, ou do servidor. Não tem como retry com segurança. Não tem como reportar o bug com contexto.
O formato de erro que funciona
Padronize a resposta de erro. Sempre o mesmo shape, independente do tipo de falha:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email inválido",
"details": [
{
"field": "email",
"message": "Formato de email inválido",
"value": "marc@"
}
],
"request_id": "req_abc123",
"timestamp": "2026-03-22T14:30:00Z"
}
}
code é uma string legível por máquina. message é legível por humano. details contém contexto específico. request_id permite correlacionar com logs. timestamp ajuda em debugging temporal.
Mapa de status codes
Cada status code tem um significado. Use-os corretamente:
- 400 Bad Request: o cliente enviou dados inválidos. Inclua o que está errado.
- 401 Unauthorized: o cliente não autenticou. Retorne headers de autenticação.
- 403 Forbidden: o cliente autenticou mas não tem permissão.
- 404 Not Found: o recurso não existe. Diferencie de 403 em recursos sensíveis.
- 409 Conflict: conflito de estado, email duplicado, versão desatualizada.
- 422 Unprocessable: dados validados mas com regras de negócio violadas.
- 429 Too Many Requests: rate limit. Retorne headers
Retry-After. - 500 Internal Error: falha no servidor. Nunca exponha stack traces.
- 503 Service Unavailable: serviço indisponível temporariamente.
Implementação em Express/Nest
Um middleware de erro centralizado evita repetição e garante consistência:
class AppError extends Error {
constructor(
public readonly code: string,
public readonly statusCode: number,
message: string,
public readonly details?: unknown[]
) {
super(message)
}
}
// Middleware de erro
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details,
request_id: req.id,
timestamp: new Date().toISOString()
}
})
}
console.error('Unhandled error:', err)
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'Erro interno do servidor',
request_id: req.id,
timestamp: new Date().toISOString()
}
})
}
O que não expor em erros
Nunca retorne stack traces, paths de arquivos, ou detalhes de implementação. Em produção, esses dados são vetores de ataque. Logue tudo internamente, retorne apenas o necessário para o consumidor corrigir o problema.
Para erros de validação, mostre os campos com problema. Para erros de autenticação, não diga se o usuário existe ou não. Para erros de banco, retorne uma mensagem genérica e logue o detalhe.
Logging de erros
Cada erro deve gerar um log com contexto: request_id, user_id, endpoint, payload (sanitizado), e stack trace completa. Ferramentas como Sentry ou Datadog agregam esses logs e permitem busca por código de erro, frequência, e impacto.
O padrão request_id no header X-Request-Id permite ao consumidor reportar exatamente qual requisição falhou, e ao time encontrar o log correspondente.
Curtiu o conteúdo?
Construo produtos web e soluções com IA do jeito certo — arquitetura sólida, código sustentável e entrega real.
Vamos conversar