Boas práticas de tratamento de erros estruturados em APIs

1. Fundamentos do tratamento de erros estruturados

Erro estruturado é aquele que segue um formato previsível e padronizado, permitindo que o cliente da API interprete programaticamente o problema. Em contraste, erros não estruturados retornam mensagens genéricas como "Algo deu errado" ou apenas códigos HTTP sem contexto adicional.

A consistência no formato de erros é fundamental para a experiência do desenvolvedor (DX). Quando todos os endpoints retornam erros no mesmo formato, o cliente pode implementar um tratamento genérico e previsível, reduzindo retrabalho e bugs. Em produção, erros estruturados aceleram a depuração, pois carregam metadados como identificadores de rastreamento e detalhes específicos do problema.

2. Padrões de resposta de erro: RFC 7807 (Problem Details)

A RFC 7807 define um formato padronizado para respostas de erro em APIs HTTP, conhecido como Problem Details. Sua estrutura básica inclui:

  • type (URI que identifica o tipo do problema)
  • title (resumo legível do erro)
  • status (código HTTP)
  • detail (descrição específica do ocorrido)
  • instance (URI que identifica a ocorrência específica)

Exemplo de resposta Problem Details:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.exemplo.com/errors/validation-error",
  "title": "Erro de validação",
  "status": 422,
  "detail": "O campo 'email' é obrigatório",
  "instance": "/api/v1/usuarios",
  "errors": [
    {
      "field": "email",
      "message": "O email informado não é válido"
    },
    {
      "field": "nome",
      "message": "O nome deve ter no mínimo 3 caracteres"
    }
  ],
  "traceId": "abc123-def456",
  "timestamp": "2025-01-15T10:30:00Z"
}

Alternativas comuns incluem JSON:API errors (padrão para APIs RESTful) e GraphQL errors (com estrutura de árvore). A escolha depende do ecossistema da API, mas a consistência é o princípio mais importante.

3. Hierarquia de erros e categorização semântica

Erros devem ser categorizados semanticamente para facilitar o tratamento:

Erros de cliente (4xx):
- 400 Bad Request — validação de entrada
- 401 Unauthorized — autenticação ausente ou inválida
- 403 Forbidden — autorização negada
- 404 Not Found — recurso inexistente
- 409 Conflict — estado conflitante (ex: duplicidade)
- 429 Too Many Requests — rate limiting

Erros de servidor (5xx):
- 500 Internal Server Error — falha inesperada
- 502 Bad Gateway — resposta inválida de serviço externo
- 503 Service Unavailable — serviço temporariamente indisponível
- 504 Gateway Timeout — timeout em dependência externa

Erros de negócio:
Erros que representam violações de regras de domínio, como saldo insuficiente, item indisponível ou limite de crédito excedido. Devem ser mapeados para códigos HTTP apropriados (geralmente 422 ou 409) com detalhes específicos no corpo da resposta.

4. Implementação de códigos de erro internos e mensagens localizadas

Crie um catálogo centralizado de códigos de erro internos:

{
  "ERROR_CODES": {
    "VALIDATION_ERROR": {
      "httpStatus": 422,
      "message": "Erro de validação nos dados fornecidos"
    },
    "NOT_FOUND": {
      "httpStatus": 404,
      "message": "Recurso não encontrado"
    },
    "INSUFFICIENT_BALANCE": {
      "httpStatus": 422,
      "message": "Saldo insuficiente para realizar a operação"
    },
    "RATE_LIMIT_EXCEEDED": {
      "httpStatus": 429,
      "message": "Limite de requisições excedido"
    }
  }
}

Separe mensagens técnicas (para desenvolvedores) de mensagens amigáveis (para usuários finais):

{
  "code": "VALIDATION_ERROR",
  "technicalMessage": "O campo 'email' falhou na validação de formato regex",
  "userMessage": "Informe um endereço de email válido",
  "locale": "pt-BR"
}

Para internacionalização (i18n), utilize arquivos de tradução versionados:

// messages_pt-BR.json
{
  "VALIDATION_ERROR": "Erro de validação nos dados fornecidos",
  "NOT_FOUND": "Recurso não encontrado"
}

// messages_en-US.json
{
  "VALIDATION_ERROR": "Validation error in provided data",
  "NOT_FOUND": "Resource not found"
}

5. Logging e rastreabilidade de erros em APIs de alto tráfego

Inclua identificadores únicos em cada resposta de erro para correlação:

{
  "requestId": "req-7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
  "correlationId": "corr-1234567890abcdef",
  "timestamp": "2025-01-15T10:30:00.123Z",
  "service": "api-usuarios",
  "version": "2.3.1"
}

Padronize logs estruturados em formato JSON:

{
  "level": "error",
  "message": "Falha ao processar pagamento",
  "errorCode": "PAYMENT_FAILED",
  "requestId": "req-abc123",
  "userId": "usr-456",
  "amount": 150.00,
  "duration": 2345,
  "stackTrace": "Error: ..."
}

Integre com ferramentas de observabilidade como OpenTelemetry para rastreamento distribuído:

{
  "traceId": "0af7651916cd43dd8448eb211c80319c",
  "spanId": "b7ad6b7169203331",
  "parentSpanId": "9c2b6d3e4f5a6789",
  "service": "api-pagamentos",
  "operation": "processPayment",
  "error": true
}

6. Tratamento de erros em camadas

Implemente middleware global para captura de exceções não tratadas:

// Middleware global de tratamento de erros
function errorHandler(err, req, res, next) {
  const errorResponse = {
    type: "https://api.exemplo.com/errors/internal-error",
    title: "Erro interno do servidor",
    status: err.status || 500,
    detail: err.message || "Ocorreu um erro inesperado",
    instance: req.originalUrl,
    requestId: req.requestId,
    timestamp: new Date().toISOString()
  };

  if (err.validationErrors) {
    errorResponse.type = "https://api.exemplo.com/errors/validation-error";
    errorResponse.title = "Erro de validação";
    errorResponse.status = 422;
    errorResponse.errors = err.validationErrors;
  }

  res.status(errorResponse.status).json(errorResponse);
}

Para validação de entrada, retorne erros de campo específicos:

{
  "type": "https://api.exemplo.com/errors/validation-error",
  "title": "Erro de validação",
  "status": 422,
  "detail": "3 campos com erro de validação",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Formato de email inválido"
    },
    {
      "field": "senha",
      "code": "MIN_LENGTH",
      "message": "Senha deve ter no mínimo 8 caracteres"
    }
  ]
}

Para erros assíncronos em filas ou chamadas externas, implemente retry com backoff exponencial e registre falhas permanentes:

{
  "eventType": "PAYMENT_PROCESSING_FAILED",
  "retryCount": 3,
  "maxRetries": 5,
  "nextRetryAt": "2025-01-15T10:35:00Z",
  "error": {
    "code": "EXTERNAL_TIMEOUT",
    "service": "gateway-pagamentos",
    "duration": 30000
  }
}

7. Documentação e versionamento de contratos de erro

Documente respostas de erro por endpoint no OpenAPI/Swagger:

paths:
  /api/v1/usuarios:
    post:
      responses:
        '201':
          description: Usuário criado com sucesso
        '422':
          description: Erro de validação
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ValidationError'
        '500':
          description: Erro interno
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/InternalError'

components:
  schemas:
    ValidationError:
      type: object
      properties:
        type:
          type: string
          example: "https://api.exemplo.com/errors/validation-error"
        title:
          type: string
          example: "Erro de validação"
        status:
          type: integer
          example: 422
        errors:
          type: array
          items:
            $ref: '#/components/schemas/FieldError'

Versionamento de esquemas de erro:

// Versão 1 - Formato inicial
// /api/v1/errors
{
  "error": "VALIDATION_ERROR",
  "message": "Campo obrigatório"
}

// Versão 2 - Formato Problem Details
// /api/v2/errors
{
  "type": "https://api.exemplo.com/v2/errors/validation-error",
  "title": "Erro de validação",
  "status": 422,
  "errors": [
    {
      "field": "email",
      "code": "REQUIRED"
    }
  ]
}

Testes de contrato com Pact garantem consistência entre API e clientes:

// Contrato Pact para erro de validação
{
  "provider": "API Usuários",
  "consumer": "App Mobile",
  "interaction": {
    "request": {
      "method": "POST",
      "path": "/api/v2/usuarios",
      "body": { "email": "invalido" }
    },
    "response": {
      "status": 422,
      "headers": {
        "Content-Type": "application/problem+json"
      },
      "body": {
        "type": "https://api.exemplo.com/v2/errors/validation-error",
        "title": "Erro de validação",
        "status": 422,
        "errors": [
          {
            "field": "email",
            "code": "INVALID_FORMAT"
          }
        ]
      }
    }
  }
}

Referências