HTTP Codes vs RESTful APIs
Melhores Práticas de Códigos de Status HTTP para Design de APIs RESTful
Sumário
- Categorias de Códigos de Status HTTP
- Códigos de Status Informativos 1xx
- Códigos de Status de Sucesso 2xx
- Códigos de Status de Redirecionamento 3xx
- Códigos de Status de Erro do Cliente 4xx
- Códigos de Status de Erro do Servidor 5xx
- Árvores de Decisão para Escolha de Códigos de Status
- Erros Comuns e Antipadrões
- Padrões de Tratamento de Erros
- Códigos de Status de Autenticação/Autorização
- Melhores Práticas da Indústria
- Exemplos de Respostas JSON
- Padrões RFC e Referências
Categorias de Códigos de Status HTTP {#categorias}
Os códigos de status HTTP são divididos em cinco categorias baseadas no primeiro dígito:
- 1xx (Informativo): Comunica informações do protocolo de transferência
- 2xx (Sucesso): Indica que a requisição do cliente foi aceita com sucesso
- 3xx (Redirecionamento): Indica que o cliente deve executar ações adicionais para completar a requisição
- 4xx (Erro do Cliente): Aponta para erros cometidos pelo cliente
- 5xx (Erro do Servidor): O servidor assume a responsabilidade por estes códigos de erro
Códigos de Status Informativos 1xx {#1xx}
100 Continue
- Caso de Uso: O cliente deve continuar com a requisição
- Quando Usar: Para requisições grandes onde você deseja validar cabeçalhos antes de enviar o corpo
- Exemplo: Upload de arquivos onde você verifica autorização primeiro
101 Switching Protocols
- Caso de Uso: O servidor está mudando protocolos (ex: HTTP para WebSocket)
- Quando Usar: Cenários de upgrade de protocolo
- Exemplo: Handshake do WebSocket
102 Processing (WebDAV)
- Caso de Uso: O servidor recebeu e está processando a requisição
- Quando Usar: Operações de longa duração para prevenir timeout do cliente
- Exemplo: Operações de processamento de arquivos grandes
103 Early Hints
- Caso de Uso: Retorna alguns cabeçalhos de resposta antes da resposta final
- Quando Usar: Para permitir pré-carregamento de recursos
- Exemplo: Enviando cabeçalhos Link para pré-carregamento de recursos
Códigos de Status de Sucesso 2xx {#2xx}
200 OK
- Caso de Uso: A requisição foi bem-sucedida
- Quando Usar:
- GET: Recurso recuperado com sucesso
- PUT: Recurso atualizado com sucesso
- POST: Ação completada com sucesso (mas nenhum recurso criado)
- Exemplo JSON:
{ "id": 123, "nome": "João Silva", "email": "joao@exemplo.com"}
201 Created
- Caso de Uso: Recurso criado com sucesso
- Quando Usar: Requisições POST que criam novos recursos
- Cabeçalhos Obrigatórios: Cabeçalho Location com URI do novo recurso
- Exemplo JSON:
HTTP/1.1 201 CreatedLocation: /api/usuarios/123Content-Type: application/json
{ "id": 123, "nome": "João Silva", "email": "joao@exemplo.com", "criado_em": "2023-01-01T12:00:00Z"}
202 Accepted
- Caso de Uso: Requisição aceita mas ainda não processada
- Quando Usar: Operações assíncronas, processamento em lote
- Melhor Prática: Incluir endpoint de monitoramento de status
- Exemplo JSON:
{ "mensagem": "Requisição aceita para processamento", "url_status": "/api/jobs/456/status", "conclusao_estimada": "2023-01-01T12:05:00Z"}
204 No Content
- Caso de Uso: Requisição bem-sucedida mas sem conteúdo para retornar
- Quando Usar: Operações DELETE, PUT ou PATCH sem corpo de resposta
- Importante: NÃO deve incluir corpo de mensagem
206 Partial Content
- Caso de Uso: Entrega de recurso parcial
- Quando Usar: Requisições de range para arquivos grandes
- Cabeçalhos Obrigatórios: Cabeçalho Content-Range
Códigos de Status de Redirecionamento 3xx {#3xx}
301 Moved Permanently
- Caso de Uso: Recurso movido permanentemente para nova URI
- Quando Usar: Versionamento de API, mudanças permanentes de URL
- Cabeçalhos Obrigatórios: Cabeçalho Location com nova URI
- Melhor Prática: Raramente usado em APIs devido a estratégias de versionamento
302 Found
- Caso de Uso: Recurso temporariamente movido
- Quando Usar: Redirecionamentos temporários
- Observação: O método pode mudar para GET em alguns clientes
303 See Other
- Caso de Uso: Resposta disponível em URI diferente via GET
- Quando Usar: Após POST para redirecionar para representação do recurso
- Melhor Prática: Preferido sobre 302 para padrão POST-redirect-GET
304 Not Modified
- Caso de Uso: Recurso não alterado desde a última requisição
- Quando Usar: Requisições condicionais com If-Modified-Since ou If-None-Match
- Obrigatório: Cabeçalhos ETag ou Last-Modified
307 Temporary Redirect
- Caso de Uso: Redirecionamento temporário preservando método HTTP
- Quando Usar: Quando o método não deve mudar (ao contrário do 302)
- Melhor Prática: Use em vez de 302 para métodos não-GET
308 Permanent Redirect
- Caso de Uso: Redirecionamento permanente preservando método HTTP
- Quando Usar: Quando o método não deve mudar (ao contrário do 301)
- Melhor Prática: Use em vez de 301 para métodos não-GET
Códigos de Status de Erro do Cliente 4xx {#4xx}
400 Bad Request
- Caso de Uso: Erro genérico do cliente
- Quando Usar: Sintaxe de requisição malformada, parâmetros inválidos
- Exemplo JSON:
{ "erro": { "codigo": "BAD_REQUEST", "mensagem": "Formato de requisição inválido", "detalhes": [ { "campo": "email", "mensagem": "Formato de email inválido" } ] }}
401 Unauthorized
- Caso de Uso: Autenticação necessária ou falhou
- Quando Usar: Credenciais de autenticação ausentes ou inválidas
- Cabeçalhos Obrigatórios: Cabeçalho WWW-Authenticate
- Observação: Semanticamente significa “não autenticado”
- Exemplo JSON:
{ "erro": { "codigo": "UNAUTHORIZED", "mensagem": "Autenticação necessária", "detalhes": "Por favor, forneça credenciais de autenticação válidas" }}
403 Forbidden
- Caso de Uso: Acesso negado (autenticado mas não autorizado)
- Quando Usar: Usuário não tem permissão para o recurso
- Diferença do 401: Identidade do cliente é conhecida mas o acesso é negado
- Exemplo JSON:
{ "erro": { "codigo": "FORBIDDEN", "mensagem": "Acesso negado", "detalhes": "Permissões insuficientes para acessar este recurso" }}
404 Not Found
- Caso de Uso: Recurso não encontrado
- Quando Usar:
- Recurso não existe
- Endpoint não existe
- Ocultar existência do recurso de usuários não autorizados
- Exemplo JSON:
{ "erro": { "codigo": "NOT_FOUND", "mensagem": "Recurso não encontrado", "detalhes": "O recurso solicitado não existe" }}
405 Method Not Allowed
- Caso de Uso: Método HTTP não suportado para o recurso
- Quando Usar: Usando POST em recurso somente leitura, DELETE em recurso imutável
- Cabeçalhos Obrigatórios: Cabeçalho Allow listando métodos suportados
- Exemplo JSON:
HTTP/1.1 405 Method Not AllowedAllow: GET, POST
{ "erro": { "codigo": "METHOD_NOT_ALLOWED", "mensagem": "Método não permitido", "metodos_permitidos": ["GET", "POST"] }}
406 Not Acceptable
- Caso de Uso: Servidor não pode produzir tipo de conteúdo solicitado
- Quando Usar: Cabeçalho Accept especifica tipo de mídia não suportado
- Exemplo JSON:
{ "erro": { "codigo": "NOT_ACCEPTABLE", "mensagem": "Tipo de conteúdo solicitado não suportado", "tipos_suportados": ["application/json", "application/xml"] }}
408 Request Timeout
- Caso de Uso: Servidor expirou esperando pela requisição
- Quando Usar: Cliente demorou muito para enviar requisição completa
- Melhor Prática: Inclua cabeçalho Retry-After se apropriado
409 Conflict
- Caso de Uso: Requisição conflita com estado atual do recurso
- Quando Usar:
- Falhas de bloqueio otimista
- Conflitos de estado do recurso
- Criação de recurso duplicado
- Exemplo JSON:
{ "erro": { "codigo": "CONFLICT", "mensagem": "Conflito de recurso", "detalhes": "Recurso foi modificado por outra requisição" }}
410 Gone
- Caso de Uso: Recurso permanentemente removido
- Quando Usar: Recurso intencionalmente removido e não retornará
- Diferença do 404: Indica explicitamente remoção permanente
412 Precondition Failed
- Caso de Uso: Pré-condições nos cabeçalhos de requisição não atendidas
- Quando Usar: Condições If-Match, If-None-Match, If-Modified-Since falham
- Uso Comum: Controle de concorrência otimista
413 Content Too Large
- Caso de Uso: Entidade de requisição maior que limites do servidor
- Quando Usar: Uploads de arquivos excedendo limites de tamanho
- Melhor Prática: Inclua cabeçalho Retry-After se temporário
415 Unsupported Media Type
- Caso de Uso: Formato de entidade de requisição não suportado
- Quando Usar: Cabeçalho Content-Type especifica formato não suportado
- Exemplo JSON:
{ "erro": { "codigo": "UNSUPPORTED_MEDIA_TYPE", "mensagem": "Tipo de mídia não suportado", "tipos_suportados": ["application/json", "application/xml"] }}
422 Unprocessable Entity
- Caso de Uso: Requisição bem formada mas semanticamente inválida
- Quando Usar: Erros de validação, violações de regras de negócio
- Exemplo JSON:
{ "erro": { "codigo": "UNPROCESSABLE_ENTITY", "mensagem": "Falha na validação", "detalhes": [ { "campo": "idade", "mensagem": "Deve estar entre 0 e 120" }, { "campo": "email", "mensagem": "Deve ser único" } ] }}
429 Too Many Requests
- Caso de Uso: Limite de taxa excedido
- Quando Usar: Cliente excedeu limites de taxa de requisições
- Cabeçalhos Obrigatórios: Cabeçalho Retry-After
- Exemplo JSON:
HTTP/1.1 429 Too Many RequestsRetry-After: 3600
{ "erro": { "codigo": "RATE_LIMIT_EXCEEDED", "mensagem": "Limite de taxa excedido", "repetir_apos": 3600, "limite": 100, "restante": 0 }}
Códigos de Status de Erro do Servidor 5xx {#5xx}
500 Internal Server Error
- Caso de Uso: Erro genérico do servidor
- Quando Usar: Condições inesperadas do servidor
- Melhor Prática: Registre detalhes internamente, retorne mensagem genérica ao cliente
- Exemplo JSON:
{ "erro": { "codigo": "INTERNAL_SERVER_ERROR", "mensagem": "Ocorreu um erro inesperado", "detalhes": "Por favor, tente novamente mais tarde" }}
501 Not Implemented
- Caso de Uso: Servidor não suporta funcionalidade solicitada
- Quando Usar: Funcionalidade ainda não implementada
- Melhor Prática: Indique possível disponibilidade futura
502 Bad Gateway
- Caso de Uso: Servidor atuando como gateway recebeu resposta inválida
- Quando Usar: Erros de servidor upstream em microserviços
- Melhor Prática: Distinga de 500 para depuração
503 Service Unavailable
- Caso de Uso: Servidor temporariamente indisponível
- Quando Usar: Manutenção, sobrecarga, interrupções temporárias
- Cabeçalhos Obrigatórios: Cabeçalho Retry-After quando possível
- Exemplo JSON:
HTTP/1.1 503 Service UnavailableRetry-After: 1800
{ "erro": { "codigo": "SERVICE_UNAVAILABLE", "mensagem": "Serviço temporariamente indisponível", "repetir_apos": 1800 }}
504 Gateway Timeout
- Caso de Uso: Timeout do gateway esperando resposta upstream
- Quando Usar: Servidor upstream demorou muito para responder
- Melhor Prática: Configure valores de timeout apropriados
Árvores de Decisão para Escolha de Códigos de Status {#arvores-decisao}
Árvore de Decisão Primária
Requisição Recebida├─► Problema de Autenticação? → 401 Unauthorized│ ↓├─► Problema de Autorização? → 403 Forbidden│ ↓├─► Recurso Não Encontrado? → 404 Not Found│ ↓├─► Método Não Permitido? → 405 Method Not Allowed│ ↓├─► Formato da Requisição Válido?│ ├─► Não → 400 Bad Request│ ├─► Tipo de Mídia Não Suportado → 415 Unsupported Media Type│ ├─► Conteúdo Muito Grande → 413 Content Too Large│ └─► Sim ↓│├─► Validação Passou?│ ├─► Não → 422 Unprocessable Entity│ └─► Sim ↓│├─► Conflito de Recurso? → 409 Conflict│ ↓├─► Limite de Taxa? → 429 Too Many Requests│ ↓├─► Erro do Servidor? → 500/502/503/504│ ↓└─► Sucesso ├─► Recurso Criado? → 201 Created ├─► Sem Conteúdo? → 204 No Content ├─► Aceito para Processamento? → 202 Accepted └─► Sucesso Padrão → 200 OK
Árvore de Decisão de Resposta de Sucesso
Requisição Bem-Sucedida├─► Você criou um novo recurso?│ ├─► Sim → 201 Created (com cabeçalho Location)│ └─► Não ↓│├─► Você tem conteúdo para retornar?│ ├─► Não → 204 No Content│ └─► Sim ↓│├─► O processamento é assíncrono?│ ├─► Sim → 202 Accepted│ └─► Não ↓│└─► 200 OK
Árvore de Decisão de Resposta de Erro
Erro Ocorreu├─► É um erro do cliente?│ ├─► Autenticação ausente/inválida → 401 Unauthorized│ ├─► Permissão negada → 403 Forbidden│ ├─► Recurso não encontrado → 404 Not Found│ ├─► Método não suportado → 405 Method Not Allowed│ ├─► Formato de requisição inválido → 400 Bad Request│ ├─► Validação falhou → 422 Unprocessable Entity│ ├─► Limite de taxa excedido → 429 Too Many Requests│ └─► Conflito de recurso → 409 Conflict│└─► É um erro do servidor? ├─► Servidor sobrecarregado/manutenção → 503 Service Unavailable ├─► Erro de gateway/proxy → 502 Bad Gateway ├─► Timeout do gateway → 504 Gateway Timeout ├─► Funcionalidade não implementada → 501 Not Implemented └─► Erro inesperado → 500 Internal Server Error
Erros Comuns e Antipadrões {#erros-comuns}
1. Usar 200 OK para Tudo
❌ Errado:
HTTP/1.1 200 OK{ "sucesso": false, "erro": "Usuário não encontrado"}
✅ Correto:
HTTP/1.1 404 Not Found{ "erro": { "codigo": "NOT_FOUND", "mensagem": "Usuário não encontrado" }}
2. Retornar 404 para Problemas de Autorização
❌ Errado: Usar 404 para ocultar recursos de usuários não autorizados em todos os casos ✅ Correto: Use 401 para problemas de autenticação, 403 para problemas de autorização
3. Não Distinguir Entre 401 e 403
❌ Errado: Usar 401 para todos os cenários de acesso negado ✅ Correto:
- 401: Autenticação necessária/falhou
- 403: Autenticado mas não autorizado
4. Usar 500 para Erros de Validação
❌ Errado: Retornar 500 para falhas de validação de lógica de negócio ✅ Correto: Use 400 para erros de formato, 422 para erros de validação
5. Não Incluir Cabeçalhos Obrigatórios
❌ Errado:
HTTP/1.1 405 Method Not Allowed
✅ Correto:
HTTP/1.1 405 Method Not AllowedAllow: GET, POST
6. Formato de Resposta de Erro Inconsistente
❌ Errado: Formatos de erro diferentes entre endpoints ✅ Correto: Estrutura de resposta de erro padronizada
7. Expor Detalhes Internos de Implementação
❌ Errado:
{ "erro": "Falha na conexão com banco de dados: Timeout na conexão na linha 47"}
✅ Correto:
{ "erro": { "codigo": "INTERNAL_SERVER_ERROR", "mensagem": "Ocorreu um erro inesperado. Por favor, tente novamente mais tarde." }}
Padrões de Tratamento de Erros {#tratamento-erros}
Estrutura de Resposta de Erro Padrão
Baseado na RFC 7807 (Problem Details for HTTP APIs):
{ "erro": { "tipo": "https://exemplo.com/errors/validation-failed", "titulo": "Validação Falhou", "status": 422, "detalhe": "O corpo da requisição contém dados inválidos", "instancia": "/usuarios/criar", "erros": [ { "campo": "email", "mensagem": "Formato de email inválido", "codigo": "INVALID_EMAIL" }, { "campo": "idade", "mensagem": "Deve estar entre 0 e 120", "codigo": "INVALID_RANGE" } ] }}
Padrão de Erro do Google Cloud
{ "erro": { "codigo": 400, "mensagem": "Requisição inválida", "status": "INVALID_ARGUMENT", "detalhes": [ { "@type": "type.googleapis.com/google.rpc.ErrorInfo", "motivo": "INVALID_EMAIL", "dominio": "exemplo.com", "metadados": { "campo": "email", "valor": "email-invalido" } } ] }}
Padrão de Erro da Microsoft
{ "erro": { "codigo": "InvalidRequest", "mensagem": "A requisição é inválida", "alvo": "email", "detalhes": [ { "codigo": "InvalidEmailFormat", "mensagem": "O formato do email é inválido", "alvo": "email" } ], "erro_interno": { "codigo": "ValidationError", "mensagem": "Falha na validação do email" } }}
Padrão de Erro Mínimo
{ "erro": { "codigo": "VALIDATION_FAILED", "mensagem": "Falha na validação da requisição", "detalhes": [ { "campo": "email", "mensagem": "Formato de email inválido" } ] }}
Códigos de Status de Autenticação/Autorização {#codigos-auth}
Fluxo de Autenticação
- 401 Unauthorized: Autenticação ausente ou inválida
- 403 Forbidden: Autenticação válida mas permissões insuficientes
- 200 OK: Autenticação bem-sucedida com token válido
Cenários Comuns de Autenticação
Autenticação Ausente
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer realm="api"
{ "erro": { "codigo": "AUTHENTICATION_REQUIRED", "mensagem": "Autenticação necessária", "detalhes": "Por favor, forneça um token de acesso válido" }}
Token Inválido
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer error="invalid_token"
{ "erro": { "codigo": "INVALID_TOKEN", "mensagem": "Token de acesso inválido", "detalhes": "O token fornecido está expirado ou inválido" }}
Permissões Insuficientes
HTTP/1.1 403 Forbidden
{ "erro": { "codigo": "INSUFFICIENT_PERMISSIONS", "mensagem": "Acesso negado", "detalhes": "Você não tem permissão para acessar este recurso" }}
Códigos Específicos do OAuth 2.0
- 401: Token de acesso inválido ou expirado
- 403: Token válido mas escopo insuficiente
- 400: Formato de requisição inválido
- 429: Limite de taxa excedido
Melhores Práticas da Indústria {#melhores-praticas}
Diretrizes da API do Google
- Use códigos de status HTTP padrão
- Retorne informações de erro detalhadas no corpo da resposta
- Inclua códigos de erro legíveis por máquina
- Forneça mensagens de erro acionáveis
- Use formato de resposta de erro consistente
Diretrizes da API REST da Microsoft
- Use códigos de status HTTP apropriados
- Forneça informações de erro tanto legíveis por humanos quanto por máquinas
- Inclua IDs de correlação para depuração
- Suporte erros parciais em operações em lote
- Use formato de resposta de erro padrão
Melhores Práticas Gerais
- Seja Consistente: Use os mesmos códigos de status para cenários similares
- Seja Específico: Use o código de status mais específico disponível
- Seja Útil: Forneça mensagens de erro acionáveis
- Seja Seguro: Não exponha detalhes internos de implementação
- Siga Padrões: Siga especificações HTTP
- Seja Cacheável: Considere implicações de cache dos códigos de status
Melhores Práticas de Limitação de Taxa
HTTP/1.1 429 Too Many RequestsRetry-After: 3600X-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: 1640995200
{ "erro": { "codigo": "RATE_LIMIT_EXCEEDED", "mensagem": "Limite de taxa excedido", "repetir_apos": 3600, "limite": 100, "janela": 3600 }}
Exemplos de Respostas JSON {#exemplos-json}
Respostas de Sucesso
Sucesso de Requisição GET
HTTP/1.1 200 OKContent-Type: application/json
{ "id": 123, "nome": "João Silva", "email": "joao@exemplo.com", "criado_em": "2023-01-01T12:00:00Z"}
Sucesso de Requisição POST (Criado)
HTTP/1.1 201 CreatedLocation: /api/usuarios/123Content-Type: application/json
{ "id": 123, "nome": "João Silva", "email": "joao@exemplo.com", "criado_em": "2023-01-01T12:00:00Z"}
Sucesso de Requisição PUT (Atualizado)
HTTP/1.1 200 OKContent-Type: application/json
{ "id": 123, "nome": "João Santos", "email": "joao.santos@exemplo.com", "atualizado_em": "2023-01-01T12:30:00Z"}
Sucesso de Requisição DELETE
HTTP/1.1 204 No Content
Respostas de Erro
Erro de Validação (422)
HTTP/1.1 422 Unprocessable EntityContent-Type: application/json
{ "erro": { "codigo": "VALIDATION_FAILED", "mensagem": "Falha na validação da requisição", "detalhes": [ { "campo": "email", "mensagem": "Email é obrigatório", "codigo": "REQUIRED_FIELD" }, { "campo": "idade", "mensagem": "Idade deve estar entre 0 e 120", "codigo": "INVALID_RANGE" } ] }}
Erro de Autenticação (401)
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer realm="api"Content-Type: application/json
{ "erro": { "codigo": "AUTHENTICATION_REQUIRED", "mensagem": "Autenticação necessária", "detalhes": "Por favor, forneça um token de acesso válido" }}
Erro de Limite de Taxa (429)
HTTP/1.1 429 Too Many RequestsRetry-After: 3600Content-Type: application/json
{ "erro": { "codigo": "RATE_LIMIT_EXCEEDED", "mensagem": "Limite de taxa excedido", "repetir_apos": 3600, "limite": 100, "restante": 0, "resetar_em": "2023-01-01T13:00:00Z" }}
Erro do Servidor (500)
HTTP/1.1 500 Internal Server ErrorContent-Type: application/json
{ "erro": { "codigo": "INTERNAL_SERVER_ERROR", "mensagem": "Ocorreu um erro inesperado", "detalhes": "Por favor, tente novamente mais tarde ou contate o suporte", "timestamp": "2023-01-01T12:00:00Z", "trace_id": "abc123" }}
Padrões RFC e Referências {#padroes-rfc}
RFCs Primárias de HTTP
- RFC 9110: Semântica HTTP (atual)
- RFC 7231: HTTP/1.1 Semântica e Conteúdo (substituída pela 9110)
- RFC 2616: HTTP/1.1 (obsoleta, referência histórica)
- RFC 6585: Códigos de Status HTTP Adicionais
- RFC 7807: Problem Details for HTTP APIs
Registros de Códigos de Status
- Registro de Códigos de Status HTTP da IANA: Registro oficial de códigos de status HTTP
- Grupo de Trabalho HTTP: Especificações e atualizações atuais
Especificações Principais
-
Classes de Códigos de Status (RFC 9110 Seção 15):
- 1xx: Informativo
- 2xx: Sucesso
- 3xx: Redirecionamento
- 4xx: Erro do Cliente
- 5xx: Erro do Servidor
-
Comportamento de Cache (RFC 7234):
- 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501: Cacheáveis por padrão
- 302, 307: Cacheáveis com cabeçalhos de cache explícitos
- 4xx, 5xx: Geralmente não cacheáveis
-
Comportamento Específico por Método:
- GET: 200 para sucesso, 404 para não encontrado
- POST: 201 para criado, 200 para processado
- PUT: 200 para atualizado, 201 para criado
- DELETE: 204 para removido, 404 para não encontrado
Diretrizes de Implementação
- Idempotência: GET, PUT, DELETE devem ser idempotentes
- Segurança: GET, HEAD, OPTIONS devem ser seguros (sem efeitos colaterais)
- Cache: Considere comportamento de cache ao escolher códigos de status
- Segurança: Use códigos apropriados para evitar vazamento de informações
Conclusão
Este guia abrangente fornece uma referência completa para implementar códigos de status HTTP em APIs RESTful. Principais pontos-chave:
- Use códigos de status específicos em vez de genéricos
- Siga padrões estabelecidos de grandes empresas de tecnologia
- Forneça respostas de erro consistentes com informações acionáveis
- Considere implicações de segurança ao escolher códigos de status
- Inclua cabeçalhos apropriados para cada código de status
- Teste minuciosamente para garantir comportamento correto dos códigos de status
Ao seguir essas melhores práticas e diretrizes, você pode criar APIs robustas e amigáveis que se comunicam efetivamente com clientes e proporcionam excelente experiência ao desenvolvedor.
Anti-Padrões de Códigos de Status HTTP para Arquitetura RESTful: Guia Completo para Equipes de Desenvolvimento Brasileiras
Resumo Executivo
Este guia técnico abrangente examina os anti-padrões mais críticos relacionados ao uso inadequado de códigos de status HTTP em APIs RESTful, fornecendo orientação específica para arquitetos de solução, líderes técnicos e equipes de desenvolvimento no Brasil. O documento aborda violações de especificação, implicações de segurança, considerações de desempenho e padrões de governança empresarial.
1. Fundamentos da Arquitetura RESTful e Códigos de Status HTTP
1.1 Princípios Fundamentais REST
Representational State Transfer (REST) é um estilo arquitetural que define um conjunto de restrições para criar serviços web escaláveis e manuteníveis. Os princípios fundamentais incluem:
- Sem Estado (Stateless): Cada requisição deve conter todas as informações necessárias para processamento
- Interface Uniforme: Uso consistente de métodos HTTP (GET, POST, PUT, PATCH, DELETE)
- Arquitetura em Camadas: Suporte para componentes intermediários (proxies, gateways, caches)
- Recursos Identificáveis: Cada recurso deve ter uma URI única e bem definida
1.2 Categorias de Códigos de Status HTTP
1xx - Informacional: Comunica informações do protocolo de transferência 2xx - Sucesso: Indica que a requisição do cliente foi aceita com sucesso 3xx - Redirecionamento: Indica que o cliente deve tomar ação adicional 4xx - Erro do Cliente: Aponta problemas na requisição enviada pelo cliente 5xx - Erro do Servidor: Indica responsabilidade do servidor pelos erros
2. Anti-Padrões Críticos de Códigos de Status HTTP
2.1 Anti-Padrão “Sempre 200” (Always 200)
Problema: Usar HTTP 200 OK para todas as respostas, independentemente do resultado real, incorporando informações de erro no corpo da resposta.
Por que está errado:
- Viola semânticas HTTP e princípios REST
- Força clientes a analisar o corpo da resposta para determinar sucesso/falha
- Quebra mecanismos de cache HTTP
- Impede tratamento adequado de erros pela infraestrutura HTTP
- Cria mecanismos proprietários de sinalização de erro que reduzem interoperabilidade
Exemplo de má prática:
// INCORRETO - Sempre retorna 200 OK@GetMapping("/usuarios/{id}")fun buscarUsuario(@PathVariable id: Long): ResponseEntity<Map<String, Any>> { return try { val usuario = usuarioService.buscarPorId(id) ResponseEntity.ok(mapOf( "status" to "sucesso", "dados" to usuario )) } catch (e: UsuarioNaoEncontradoException) { // MAL: Retorna 200 para erro ResponseEntity.ok(mapOf( "status" to "erro", "codigo" to "USUARIO_NAO_ENCONTRADO", "mensagem" to "Usuário não existe" )) }}
Abordagem correta:
// CORRETO - Usa códigos de status apropriados@GetMapping("/usuarios/{id}")fun buscarUsuario(@PathVariable id: Long): ResponseEntity<Any> { return try { val usuario = usuarioService.buscarPorId(id) ResponseEntity.ok(usuario) // 200 OK para sucesso } catch (e: UsuarioNaoEncontradoException) { // BOM: Retorna 404 para recurso não encontrado ResponseEntity.status(HttpStatus.NOT_FOUND).body(mapOf( "erro" to "USUARIO_NAO_ENCONTRADO", "mensagem" to "Usuário não existe", "detalhes" to "Nenhum usuário encontrado com o identificador fornecido", "timestampErro" to Instant.now() )) }}
2.2 Anti-Padrão “500 Genérico” (Generic 500)
Problema: Usar HTTP 500 Internal Server Error para todas as condições de erro, incluindo erros do lado do cliente.
Uso correto por categoria:
@ControllerAdviceclass GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException::class) fun handleBadRequest(ex: IllegalArgumentException): ResponseEntity<ErrorResponse> { // 400: Erro de sintaxe da requisição return ResponseEntity.badRequest().body( ErrorResponse("REQUISICAO_MALFORMADA", ex.message) ) }
@ExceptionHandler(UsuarioNaoEncontradoException::class) fun handleNotFound(ex: UsuarioNaoEncontradoException): ResponseEntity<ErrorResponse> { // 404: Recurso não encontrado return ResponseEntity.notFound().build() }
@ExceptionHandler(AccessDeniedException::class) fun handleForbidden(ex: AccessDeniedException): ResponseEntity<ErrorResponse> { // 403: Acesso negado return ResponseEntity.status(HttpStatus.FORBIDDEN).body( ErrorResponse("ACESSO_NEGADO", "Permissões insuficientes") ) }}
data class ErrorResponse( val codigo: String, val mensagem: String, val idRequisicao: String? = null, val timestamp: Instant = Instant.now())
2.3 Anti-Padrões de Tunelamento
Tunelamento GET: Usar GET para operações com efeitos colaterais
// INCORRETO - GET com efeitos colaterais@GetMapping("/usuarios/{id}/deletar")fun deletarUsuario(@PathVariable id: Long) { usuarioService.deletar(id)}
// CORRETO - DELETE para remoção@DeleteMapping("/usuarios/{id}")fun deletarUsuario(@PathVariable id: Long): ResponseEntity<Void> { usuarioService.deletar(id) return ResponseEntity.noContent().build() // 204 No Content}
3. Uso Correto dos Códigos de Status HTTP
3.1 Códigos 2xx de Sucesso
@RestController@RequestMapping("/api/v1/pedidos")class PedidoController(private val pedidoService: PedidoService) {
// 200 OK - Requisição bem-sucedida com corpo de resposta @GetMapping("/{id}") fun buscarPedido(@PathVariable id: Long): ResponseEntity<Pedido> { val pedido = pedidoService.buscarPorId(id) return ResponseEntity.ok(pedido) }
// 201 Created - Recurso criado com sucesso (deve incluir header Location) @PostMapping fun criarPedido(@Valid @RequestBody novoPedido: NovoPedidoRequest): ResponseEntity<Pedido> { val pedido = pedidoService.criar(novoPedido) val uri = URI.create("/api/v1/pedidos/${pedido.id}") return ResponseEntity.created(uri).body(pedido) }
// 202 Accepted - Requisição aceita para processamento assíncrono @PostMapping("/{id}/processar") fun processarPedido(@PathVariable id: Long): ResponseEntity<Map<String, Any>> { val idProcessamento = pedidoService.iniciarProcessamento(id) return ResponseEntity.accepted().body(mapOf( "idProcessamento" to idProcessamento, "status" to "PROCESSAMENTO_INICIADO", "urlAcompanhamento" to "/api/v1/processamentos/$idProcessamento" )) }
// 204 No Content - Operação bem-sucedida sem corpo de resposta @DeleteMapping("/{id}") fun deletarPedido(@PathVariable id: Long): ResponseEntity<Void> { pedidoService.deletar(id) return ResponseEntity.noContent().build() }}
3.2 Códigos 4xx de Erro do Cliente
@RestControllerclass AutenticacaoController(private val authService: AuthService) {
@PostMapping("/login") fun login(@Valid @RequestBody credenciais: CredenciaisLogin): ResponseEntity<Any> { return try { val token = authService.autenticar(credenciais) ResponseEntity.ok(mapOf("token" to token, "tipo" to "Bearer")) } catch (e: CredenciaisInvalidasException) { // 401 Unauthorized - Credenciais inválidas ResponseEntity.status(HttpStatus.UNAUTHORIZED) .header("WWW-Authenticate", "Bearer realm=\"api\"") .body(mapOf( "erro" to "CREDENCIAIS_INVALIDAS", "mensagem" to "Email ou senha incorretos" )) } }
// 409 Conflict - Conflito com estado atual do recurso @PostMapping("/usuarios") fun criarUsuario(@Valid @RequestBody novoUsuario: NovoUsuarioRequest): ResponseEntity<Any> { return try { val usuario = usuarioService.criar(novoUsuario) ResponseEntity.status(HttpStatus.CREATED).body(usuario) } catch (e: EmailJaExisteException) { ResponseEntity.status(HttpStatus.CONFLICT).body(mapOf( "erro" to "EMAIL_JA_EXISTE", "mensagem" to "Já existe um usuário com este endereço de email", "recursoConflitante" to "/usuarios/email/${novoUsuario.email}" )) } }
// 429 Too Many Requests - Limite de taxa excedido @GetMapping("/api-publica/cotacao") fun obterCotacao(): ResponseEntity<Any> { val limite = rateLimitService.verificarLimite(getClientIP())
return if (limite.permitido) { val cotacao = cotacaoService.obterCotacaoAtual() ResponseEntity.ok(cotacao) } else { ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) .header("Retry-After", limite.tempoEspera.toString()) .header("X-RateLimit-Limit", limite.limitePorHora.toString()) .body(mapOf( "erro" to "LIMITE_EXCEDIDO", "mensagem" to "Limite de requisições por hora excedido", "tentarNovamenteEm" to limite.tempoEspera )) } }}
4. Implicações de Segurança
4.1 Riscos de Divulgação de Informações
// BOM - Mensagem genérica com ID para rastreamento@ExceptionHandler(SQLException::class)fun handleSqlException(ex: SQLException): ResponseEntity<Any> { val idRequisicao = gerarIdUnicoRequisicao()
// Log completo para investigação interna logger.error("Erro de banco de dados [ID: $idRequisicao]", ex)
// Resposta genérica para o cliente return ResponseEntity.internalServerError().body(mapOf( "erro" to "ERRO_INTERNO", "mensagem" to "Ocorreu um erro interno no sistema", "idRequisicao" to idRequisicao, "suporte" to "Entre em contato com o suporte técnico informando o ID da requisição" ))}
4.2 Monitoramento de Segurança
@Componentclass SecurityMonitoringService {
@EventListener fun monitorarPadroesAtaque(event: HttpResponseEvent) { when (event.statusCode) { 401 -> detectarTentativasBruteForce(event) 404 -> detectarEnumeracaoRecursos(event) 403 -> detectarEscalonamentoPrivilegio(event) 429 -> detectarPossibilidadeDoS(event) } }
private fun detectarTentativasBruteForce(event: HttpResponseEvent) { val tentativas = contarTentativas401PorIP(event.clientIP, Duration.ofMinutes(5))
if (tentativas > LIMITE_TENTATIVAS_LOGIN) { alertService.enviarAlerta( TipoAlerta.BRUTE_FORCE_DETECTADO, "Múltiplas tentativas de autenticação falharam para IP: ${event.clientIP}", mapOf( "ip" to event.clientIP, "tentativas" to tentativas, "endpoint" to event.endpoint ) )
// Implementar bloqueio temporário de IP firewallService.bloquearTemporariamente(event.clientIP, Duration.ofHours(1)) } }}
5. Considerações de Performance e Cache
5.1 Uso Correto para Cache
@RestControllerclass ProdutoController {
@GetMapping("/produtos/{id}") fun obterProduto( @PathVariable id: Long, @RequestHeader(value = "If-None-Match", required = false) ifNoneMatch: String?, @RequestHeader(value = "If-Modified-Since", required = false) ifModifiedSince: String? ): ResponseEntity<Any> {
val produto = produtoService.buscarPorId(id) val etag = calcularETag(produto) val ultimaModificacao = produto.ultimaModificacao
// Verificar se o cliente já tem a versão mais recente if (clienteTemVersaoAtualizada(etag, ifNoneMatch, ultimaModificacao, ifModifiedSince)) { // 304 Not Modified - Economiza largura de banda return ResponseEntity.notModified() .eTag(etag) .lastModified(ultimaModificacao) .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()) .build() }
// 200 OK com dados atualizados e headers de cache return ResponseEntity.ok() .eTag(etag) .lastModified(ultimaModificacao) .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()) .body(produto) }}
6. Considerações Específicas para o Mercado Brasileiro
6.1 Conformidade com LGPD (Lei Geral de Proteção de Dados)
@RestController@RequestMapping("/api/v1/dados-pessoais")class DadosPessoaisController( private val dadosService: DadosPessoaisService, private val auditService: AuditLGPDService) {
@GetMapping("/usuario/{id}") @PreAuthorize("hasRole('USER') and #id == authentication.name or hasRole('ADMIN')") fun obterDadosPessoais( @PathVariable id: Long, authentication: Authentication ): ResponseEntity<Any> {
// Auditar acesso aos dados pessoais (obrigação LGPD) auditService.registrarAcessoDados( usuarioSolicitante = authentication.name, usuarioAlvo = id, tipoAcesso = "CONSULTA_DADOS_PESSOAIS", finalidade = "FORNECIMENTO_SERVICO", baseJuridica = "CONSENTIMENTO" )
return try { val dados = dadosService.obterDadosAnonimizados(id)
ResponseEntity.ok() .header("X-LGPD-Compliant", "true") .header("X-Data-Processing-Purpose", "FORNECIMENTO_SERVICO") .body(dados)
} catch (e: AcessoNegadoException) { ResponseEntity.status(HttpStatus.FORBIDDEN).body(mapOf( "erro" to "ACESSO_NEGADO_LGPD", "mensagem" to "Acesso aos dados pessoais negado conforme LGPD", "direitosLGPD" to "Para exercer seus direitos LGPD, entre em contato com dpo@empresa.com.br" )) } }
@DeleteMapping("/usuario/{id}") @PreAuthorize("#id == authentication.name or hasRole('ADMIN')") fun exercerDireitoEsquecimento( @PathVariable id: Long, authentication: Authentication ): ResponseEntity<Any> {
auditService.registrarExercicioDireito( usuario = id, direito = "DIREITO_ESQUECIMENTO", solicitadoPor = authentication.name )
return try { dadosService.anonimizarDados(id)
// 204 No Content - Dados anonimizados com sucesso ResponseEntity.noContent() .header("X-LGPD-Action", "ANONIMIZACAO_EXECUTADA") .header("X-Retention-Policy", "DADOS_ANONIMIZADOS_CONFORME_LGPD") .build()
} catch (e: ProcessoAnonimizacaoException) { ResponseEntity.status(HttpStatus.ACCEPTED).body(mapOf( "status" to "PROCESSAMENTO_INICIADO", "mensagem" to "Solicitação de anonimização aceita e será processada", "prazoExecucao" to "15 dias úteis conforme LGPD", "protocoloSolicitacao" to e.protocoloProcessamento )) } }}
6.2 Open Banking Brasileiro
@RestController@RequestMapping("/open-banking/v1")class OpenBankingController( private val openBankingService: OpenBankingService, private val bacenComplianceService: BacenComplianceService) {
@GetMapping("/accounts/{accountId}") fun obterContaBancaria( @PathVariable accountId: String, @RequestHeader("Authorization") authorizationHeader: String, @RequestHeader("x-fapi-auth-date") fapiAuthDate: String, @RequestHeader("x-fapi-customer-ip-address") customerIpAddress: String ): ResponseEntity<Any> {
return try { // Validar conformidade BACEN bacenComplianceService.validarCabecalhosFAPI(authorizationHeader, fapiAuthDate, customerIpAddress)
val conta = openBankingService.obterDadosConta(accountId)
ResponseEntity.ok() .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("x-response-time", Instant.now().toString()) .body(conta)
} catch (e: ContaNaoEncontradaException) { // 404 para conta não encontrada - conforme especificação BACEN ResponseEntity.notFound() .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .build()
} catch (e: ConsentimentoInvalidoException) { // 403 para consentimento inválido - padrão Open Banking Brasil ResponseEntity.status(HttpStatus.FORBIDDEN).body(mapOf( "errors" to listOf(mapOf( "code" to "CONSENT_INVALID", "title" to "Consentimento inválido", "detail" to "O consentimento fornecido não é válido para esta operação" )) ))
} catch (e: RateLimitExceededException) { // 429 com headers específicos do BACEN ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) .header("Retry-After", "60") .header("x-rate-limit-limit", "100") .header("x-rate-limit-remaining", "0") .body(mapOf( "errors" to listOf(mapOf( "code" to "TOO_MANY_REQUESTS", "title" to "Limite de requisições excedido", "detail" to "Limite de 100 requisições por minuto foi excedido" )) )) } }}
7. Padrões de Governança Empresarial
7.1 Anotação para Governança de APIs
@Target(AnnotationTarget.FUNCTION)@Retention(AnnotationRetention.RUNTIME)annotation class GovernancaAPI( val nivel: NivelGovernanca = NivelGovernanca.PADRAO, val codigosStatusObrigatorios: Array<HttpStatus> = [ HttpStatus.OK, HttpStatus.BAD_REQUEST, HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN, HttpStatus.NOT_FOUND, HttpStatus.INTERNAL_SERVER_ERROR ], val documentacaoObrigatoria: Boolean = true, val monitoramentoObrigatorio: Boolean = true)
enum class NivelGovernanca { BASICO, // APIs internas simples PADRAO, // APIs de negócio padrão CRITICO, // APIs críticas para o negócio PUBLICO // APIs expostas externamente}
7.2 Validador de Conformidade
object CodeReviewValidator {
data class ValidacaoResultado( val conforme: Boolean, val violacoes: List<ViolacaoGovernanca> )
data class ViolacaoGovernanca( val tipo: TipoViolacao, val descricao: String, val gravidade: Gravidade, val sugestaoCorrecao: String )
enum class TipoViolacao { CODIGO_STATUS_INADEQUADO, ESTRUTURA_RESPOSTA_INCORRETA, DOCUMENTACAO_AUSENTE, TRATAMENTO_ERRO_INADEQUADO, VIOLACAO_SEGURANCA }
fun validarConformidadeAPI(controller: Class<*>): ValidacaoResultado { val violacoes = mutableListOf<ViolacaoGovernanca>()
controller.declaredMethods.forEach { metodo -> validarMetodoHTTP(metodo, violacoes) validarTratamentoExcecoes(metodo, violacoes) validarDocumentacao(metodo, violacoes) }
return ValidacaoResultado( conforme = violacoes.none { it.gravidade == Gravidade.CRITICA }, violacoes = violacoes ) }}
8. Monitoramento e Observabilidade
8.1 Métricas de Códigos de Status
@Componentclass MetricasAPIService {
private val meterRegistry: MeterRegistry = Metrics.globalRegistry
private val contadorStatusCode = Counter.builder("api.requests.total") .description("Total de requisições por código de status") .register(meterRegistry)
private val tempoResposta = Timer.builder("api.response.time") .description("Tempo de resposta das APIs") .register(meterRegistry)
@EventListener fun registrarMetricaResposta(event: HttpResponseEvent) { contadorStatusCode.increment( Tags.of( Tag.of("status_code", event.statusCode.toString()), Tag.of("endpoint", event.endpoint), Tag.of("method", event.method) ) )
tempoResposta.record( event.tempoResposta, Tags.of( Tag.of("endpoint", event.endpoint), Tag.of("status_category", obterCategoriaStatus(event.statusCode)) ) ) }}
9. Estratégias de Teste
9.1 Testes de Contrato para Códigos de Status
@SpringBootTestclass ContratoStatusCodesTest {
@Test fun `deve retornar 200 para busca de usuario existente`() { val usuarioId = 123L mockUsuarioExistente(usuarioId)
val resposta = restTemplate.getForEntity("/usuarios/$usuarioId", Usuario::class.java)
assertThat(resposta.statusCode).isEqualTo(HttpStatus.OK) assertThat(resposta.body?.id).isEqualTo(usuarioId) }
@Test fun `deve retornar 404 para usuario inexistente`() { val usuarioIdInexistente = 999L
val resposta = restTemplate.exchange( "/usuarios/$usuarioIdInexistente", HttpMethod.GET, null, String::class.java )
assertThat(resposta.statusCode).isEqualTo(HttpStatus.NOT_FOUND) }
@Test fun `deve retornar 201 com Location header ao criar usuario`() { val novoUsuario = NovoUsuarioRequest("João Silva", "joao@empresa.com.br")
val resposta = restTemplate.postForEntity("/usuarios", novoUsuario, Usuario::class.java)
assertThat(resposta.statusCode).isEqualTo(HttpStatus.CREATED) assertThat(resposta.headers.location).isNotNull() assertThat(resposta.headers.location?.path).matches("/usuarios/\\d+") }
@Test fun `deve retornar 409 ao tentar criar usuario com email duplicado`() { val emailExistente = "existente@empresa.com.br" mockUsuarioComEmailExistente(emailExistente) val novoUsuario = NovoUsuarioRequest("Outro Usuário", emailExistente)
val resposta = restTemplate.exchange( "/usuarios", HttpMethod.POST, HttpEntity(novoUsuario), String::class.java )
assertThat(resposta.statusCode).isEqualTo(HttpStatus.CONFLICT) }}
9.2 Testes de Segurança
@TestInstance(TestInstance.Lifecycle.PER_CLASS)class SegurancaStatusCodesTest {
@Test fun `deve retornar 401 para requisicoes sem autenticacao`() { val resposta = restTemplate.exchange( "/usuarios/perfil", HttpMethod.GET, null, String::class.java )
assertThat(resposta.statusCode).isEqualTo(HttpStatus.UNAUTHORIZED) assertThat(resposta.headers["WWW-Authenticate"]).isNotNull() }
@Test fun `deve retornar 403 para usuario autenticado sem permissoes`() { val tokenUsuarioComum = gerarTokenUsuario(perfil = "USER") val headers = HttpHeaders().apply { setBearerAuth(tokenUsuarioComum) }
val resposta = restTemplate.exchange( "/admin/usuarios", HttpMethod.GET, HttpEntity<Any>(headers), String::class.java )
assertThat(resposta.statusCode).isEqualTo(HttpStatus.FORBIDDEN) }
@Test fun `nao deve vazar informacoes sensiveis em erros 500`() { mockServicoParaFalhar()
val resposta = restTemplate.exchange( "/usuarios/1", HttpMethod.GET, null, String::class.java )
assertThat(resposta.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
val erro = objectMapper.readValue(resposta.body, ErrorResponse::class.java) assertThat(erro.mensagem).doesNotContain("SQLException") assertThat(erro.mensagem).doesNotContain("password") assertThat(erro.idRequisicao).isNotNull() }}
10. Lista de Verificação para Code Review
10.1 Checklist de Design de API
Design de Recursos
- Recursos usam substantivos, não verbos
- Nomenclatura plural consistente para coleções
- Uso adequado dos métodos HTTP (GET, POST, PUT, PATCH, DELETE)
- Códigos de status HTTP apropriados (200, 201, 400, 401, 404, 500)
Estrutura de Requisição/Resposta
- Headers de Content-Type definidos corretamente
- Validação de entrada implementada
- Respostas de erro seguem formato padrão
- Paginação implementada para grandes datasets
10.2 Checklist de Segurança
Autenticação e Autorização
- Tokens JWT validados adequadamente
- Implementação OAuth 2.0 segue padrões
- Chaves de API protegidas e rotacionadas regularmente
- Controle de acesso baseado em funções (RBAC) implementado
Proteção de Dados
- Dados sensíveis não expostos em URLs
- HTTPS obrigatório para todos os endpoints
- Sanitização de entrada previne ataques de injeção
- Rate limiting implementado
10.3 Checklist de Performance
Otimização
- Consultas de banco otimizadas (índices, planos de consulta)
- Estratégia de cache implementada
- Compressão de resposta habilitada (gzip)
- Processamento assíncrono para operações demoradas
10.4 Checklist de Qualidade de Código
Padrões de Implementação
- Tratamento adequado de erros e logging
- Testes unitários cobrem caminhos críticos
- Testes de integração validam contratos de API
- Código segue guias de estilo organizacionais
- Documentação atualizada e precisa
11. Padrões de Implementação para Equipes Brasileiras
11.1 Estrutura Padrão de Resposta
data class RespostaPadrao<T>( val dados: T? = null, val sucesso: Boolean = true, val mensagem: String? = null, val timestamp: Instant = Instant.now(), val idRequisicao: String = UUID.randomUUID().toString())
data class RespostaErro( val codigo: String, val mensagem: String, val detalhes: String? = null, val campoErros: Map<String, String>? = null, val timestamp: Instant = Instant.now(), val idRequisicao: String = UUID.randomUUID().toString(), val urlDocumentacao: String? = null)
11.2 Padrões de Nomenclatura Brasileiros
// Endpoints seguindo convenções brasileiras@RestController@RequestMapping("/api/v1")class EmpresaController {
@GetMapping("/empresas/{cnpj}") fun buscarPorCnpj(@PathVariable cnpj: String): ResponseEntity<Empresa>
@GetMapping("/ceps/{cep}") fun buscarEnderecoPorCep(@PathVariable cep: String): ResponseEntity<Endereco>
@GetMapping("/estados") fun listarEstadosBrasileiros(): ResponseEntity<List<Estado>>
@GetMapping("/municipios/{uf}") fun listarMunicipiosPorUF(@PathVariable uf: String): ResponseEntity<List<Municipio>>
@PostMapping("/pagamentos/pix") fun processarPagamentoPix(@RequestBody pagamento: PagamentoPix): ResponseEntity<RespostaPagamento>}
11.3 Integração com Sistemas Brasileiros
// Integração com APIs brasileiras (Correios, Receita Federal, etc.)@Serviceclass IntegracaoReceitalFederalService {
fun consultarCnpj(cnpj: String): ResponseEntity<Any> { return try { val empresa = receitaFederalClient.consultarCnpj(cnpj) ResponseEntity.ok(empresa) } catch (e: CNPJNaoEncontradoException) { ResponseEntity.notFound().build() } catch (e: ServicoIndisponivelException) { // 503 Service Unavailable - Serviço da Receita Federal indisponível ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(mapOf( "erro" to "SERVICO_RF_INDISPONIVEL", "mensagem" to "Serviço da Receita Federal temporariamente indisponível", "tentarNovamenteEm" to "5 minutos" )) } }}
12. Decisões Arquiteturais e Diagramas
12.1 Árvore de Decisão para Códigos de Status
Operação foi executada com sucesso?├── SIM│ ├── Recurso foi criado? → 201 Created│ ├── Operação sem retorno? → 204 No Content│ ├── Processamento assíncrono? → 202 Accepted│ └── Operação padrão → 200 OK└── NÃO ├── Erro do cliente? │ ├── Dados malformados? → 400 Bad Request │ ├── Não autenticado? → 401 Unauthorized │ ├── Sem permissão? → 403 Forbidden │ ├── Recurso não encontrado? → 404 Not Found │ ├── Método não permitido? → 405 Method Not Allowed │ ├── Conflito de dados? → 409 Conflict │ ├── Dados inválidos semanticamente? → 422 Unprocessable Entity │ └── Limite excedido? → 429 Too Many Requests └── Erro do servidor? ├── Erro interno? → 500 Internal Server Error ├── Não implementado? → 501 Not Implemented ├── Gateway com problema? → 502 Bad Gateway ├── Serviço indisponível? → 503 Service Unavailable └── Timeout de gateway? → 504 Gateway Timeout
12.2 Fluxograma de Tratamento de Erros
Exceção capturada├── É exceção de validação?│ ├── SIM → 400 Bad Request│ └── Contém detalhes de campo? → Incluir fieldErrors├── É exceção de segurança?│ ├── Não autenticado → 401 Unauthorized│ ├── Sem permissão → 403 Forbidden│ └── Token expirado → 401 com refresh_token_required├── É exceção de negócio?│ ├── Recurso não encontrado → 404 Not Found│ ├── Conflito de estado → 409 Conflict│ ├── Regra de negócio violada → 422 Unprocessable Entity│ └── Rate limit excedido → 429 Too Many Requests├── É exceção de infraestrutura?│ ├── Banco indisponível → 503 Service Unavailable│ ├── Timeout → 504 Gateway Timeout│ └── Serviço externo com falha → 502 Bad Gateway└── Exceção não mapeada → 500 Internal Server Error
13. Configurações de Produção
13.1 Configuração de API Gateway
# Configuração Nginx para APIs brasileirasserver { listen 443 ssl http2; server_name api.empresa.com.br;
# Configurações SSL/TLS ssl_certificate /path/to/certificate.crt; ssl_certificate_key /path/to/private.key; ssl_protocols TLSv1.2 TLSv1.3;
# Headers de segurança add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection "1; mode=block";
# Rate limiting específico por status code location /api/ { # Rate limiting mais rigoroso para endpoints que retornam 401/403 limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m; limit_req zone=auth burst=5 nodelay;
# Configuração para diferentes status codes location ~* "401|403" { access_log /var/log/nginx/security_events.log; limit_req zone=auth burst=2 nodelay; }
proxy_pass http://backend-api; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Request-ID $request_id; }}
13.2 Configuração de Monitoramento
# Prometheus config para métricas de APIglobal: scrape_interval: 15s evaluation_interval: 15s
rule_files: - "api_rules.yml"
scrape_configs: - job_name: 'api-brasileira' static_configs: - targets: ['api:8080'] metrics_path: /actuator/prometheus scrape_interval: 10s
# Alertas específicos para códigos de statusgroups: - name: api.status_codes rules: - alert: HighErrorRate expr: (rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])) > 0.01 for: 2m labels: severity: critical annotations: summary: "Taxa de erro 5xx alta na API" description: "Taxa de erro 5xx é {{ $value }} por mais de 2 minutos"
- alert: HighClientErrorRate expr: (rate(http_requests_total{status=~"4.."}[5m]) / rate(http_requests_total[5m])) > 0.05 for: 5m labels: severity: warning annotations: summary: "Taxa de erro 4xx alta na API" description: "Taxa de erro 4xx é {{ $value }} - possível problema na documentação ou validação"
14. Conclusões e Recomendações
14.1 Resumo dos Anti-Padrões Críticos
- Anti-Padrão “Sempre 200”: O mais prejudicial para arquiteturas RESTful, quebra contratos HTTP e impede cache adequado
- Uso Genérico do 500: Confunde responsabilidades e dificulta debugging
- Tunelamento de Métodos: Viola princípios REST e semânticas HTTP
- Confusão 401/403: Compromete segurança e experiência do usuário
- Vazamento de Informações: Expõe detalhes internos através de códigos de erro
14.2 Melhores Práticas para Implementação
Para Arquitetos de Solução:
- Estabelecer padrões de códigos de status em nível empresarial
- Implementar validação automatizada de conformidade em pipelines CI/CD
- Projetar monitoramento e alertas baseados em padrões de status codes
- Criar frameworks reutilizáveis de tratamento de erro
Para Líderes Técnicos:
- Usar matrizes de decisão de códigos de status para implementação consistente
- Implementar suítes abrangentes de teste de tratamento de erro
- Projetar estratégias de degradação baseadas em códigos de status
- Aplicar códigos de status adequados em todos os endpoints da API
Para Equipes de Desenvolvimento:
- Seguir guias de estilo específicos para APIs RESTful brasileiras
- Implementar logging e auditoria conforme LGPD
- Usar terminologia técnica brasileira consistente
- Considerar integrações com sistemas nacionais (PIX, CNPJ, CEP)
14.3 Fatores Críticos de Sucesso
- Implementação consistente em todas as APIs empresariais
- Documentação abrangente e treinamento de desenvolvedores
- Validação automatizada de conformidade
- Avaliações regulares de segurança de padrões de tratamento de erro
- Monitoramento de performance vinculado ao uso de códigos de status
14.4 Considerações para o Contexto Brasileiro
- Conformidade LGPD em tratamento de dados e auditoria
- Integração Open Banking seguindo padrões BACEN
- Terminologia técnica adaptada para equipes brasileiras
- Considerações regionais para performance e CDN
- Padrões regulatórios específicos do mercado brasileiro
14.5 Próximos Passos
- Avaliação atual: Auditar APIs existentes usando este framework
- Priorização de melhorias: Focar em violações críticas de segurança e performance
- Implementação incremental: Abordar anti-padrões identificados gradualmente
- Monitoramento contínuo: Estabelecer KPIs baseados em códigos de status
- Melhoria contínua: Revisão regular e refinamento de práticas
HTTP 204 vs 200: Guia Completo para Coleções Vazias em APIs RESTful
HTTP 200 OK com arrays vazios emerge como o claro vencedor baseado em especificações oficiais, consenso da indústria e benefícios práticos de implementação. Embora HTTP 204 No Content ofereça economia mínima de largura de banda, ele cria complexidade desnecessária e viola princípios fundamentais do HTTP quando usado incorretamente para respostas JSON.
A pesquisa revela que grandes provedores de API escolhem esmagadoramente 200 OK com arrays vazios, com o GitHub até mesmo normalizando automaticamente respostas 204 para arrays vazios em seu SDK. Esta abordagem alinha-se com semânticas HTTP, princípios arquiteturais REST e proporciona experiência superior ao desenvolvedor em todos os contextos de implementação.
Especificações HTTP estabelecem limites semânticos claros
RFC 9110 separa definitivamente os dois códigos de status: HTTP 204 No Content NÃO DEVE incluir qualquer corpo de mensagem e termina imediatamente após os cabeçalhos, enquanto HTTP 200 OK DEVE incluir conteúdo (que pode ter comprimento zero). Isso cria uma distinção crítica entre “nenhum conteúdo” (204) versus “conteúdo que por acaso está vazio” (200).
Para coleções vazias, um array vazio []
constitui conteúdo válido em termos HTTP. RFC 9110 Seção 6.4.2 afirma explicitamente que requisições GET com status 200 devem retornar “uma representação do recurso alvo.” Uma coleção vazia ainda é uma representação do recurso coleção - ela existe e pode ser acessada, simplesmente não contém itens.
Usar 204 com corpos de resposta JSON viola a especificação HTTP. A RFC proíbe explicitamente corpos de mensagem com respostas 204, tornando padrões comuns como retornar {"data": []}
com status 204 tecnicamente não-conformes. Esta violação de especificação cria comportamento imprevisível entre diferentes clientes e servidores HTTP.
Líderes da indústria convergem em práticas consistentes
Análise de grandes provedores de API revela consenso notável. GitHub, Microsoft Graph API, Stripe, AWS, Twitter e Slack todos implementam HTTP 200 com arrays vazios para coleções vazias. A API Graph da Microsoft retorna consistentemente respostas estruturadas como {"@odata.context": "...", "value": []}
com status 200, enquanto o Stripe mantém sua estrutura padrão de objeto lista com arrays data
vazios.
A abordagem do GitHub é particularmente instrutiva - sua documentação afirma explicitamente: “Alguns endpoints respondem com 204 No Content em vez de array vazio quando não há dados. Nesse caso, retorne um array vazio.” Seu SDK Octokit normaliza automaticamente qualquer resposta 204 para arrays vazios, demonstrando o reconhecimento da indústria de que consistência supera pureza teórica.
O padrão entre todos os grandes provedores enfatiza simplicidade do cliente e estruturas de resposta previsíveis. A resposta vazia do Stripe mantém a mesma forma de objeto que respostas populadas, o Twitter preserva metadados de paginação, e a Microsoft inclui informações de contexto OData mesmo para resultados vazios.
Princípios arquiteturais REST suportam representação de coleção
A dissertação REST de Roy Fielding estabelece fundamento teórico para tratar coleções vazias como representações válidas de recursos. A restrição arquitetural de “manipulação de recursos através de representações” explicitamente suporta coleções vazias tendo representação, com Fielding notando que “um recurso pode mapear para o conjunto vazio.”
Especialistas REST e praticantes da indústria consistentemente recomendam 200 OK para coleções vazias. A autoridade em design de APIs Arnaud Lauret (API Handyman) descobriu que 51% dos desenvolvedores de API preferem 200 OK para coleções vazias, comparado a 24% preferindo 204 e 25% escolhendo 404. O raciocínio centra-se em manter consistência semântica - o recurso coleção existe e é acessível independentemente de seu conteúdo.
Usar 404 Not Found para coleções vazias representa um anti-padrão claro, conflitando “recurso não existe” com “recurso existe mas não contém itens.” Isso viola o princípio fundamental REST de que identidade de recurso permanece independente do conteúdo.
Padrões de implementação revelam vantagens práticas
Exemplos de código em linguagens de programação demonstram benefícios consistentes da abordagem 200. Implementações de frameworks universalmente suportam 200 com arrays vazios como comportamento padrão:
# Flask - Manipulação natural de array vazio@app.route('/usuarios')def obter_usuarios(): usuarios = consultar_usuarios() # Retorna [] return jsonify(usuarios), 200 # Consistente independente do conteúdo
// Express - Manipulação uniforme de respostaapp.get('/api/usuarios', async (req, res) => { const usuarios = await obterUsuariosDoBanco(); res.status(200).json(usuarios); // Mesmo caminho de código para vazio ou populado});
Manipulação no lado cliente revela a carga de complexidade das respostas 204. A API fetch do JavaScript requer lógica especial para status 204:
// Manipulação complexa necessária para 204if (response.status === 204) { return []; // Deve criar manualmente array vazio} else if (response.ok) { return await response.json(); // Análise normal}
// Manipulação simples com 200return response.ok ? await response.json() : [];
Padrões de frameworks consistentemente favorecem 200 com arrays vazios. Spring Boot, .NET Core, Rails e outros naturalmente retornam 200 para operações bem-sucedidas com coleções vazias, requerendo configuração explícita para alcançar comportamento 204.
Análise de performance mostra impacto mínimo de largura de banda
Diferenças de largura de banda provam ser negligíveis na prática. Respostas HTTP 204 contêm apenas cabeçalhos (200-500 bytes), enquanto respostas 200 com arrays JSON vazios adicionam meramente 2-50 bytes. Estudos de performance concluem que essas diferenças são “geralmente negligíveis em aplicações do mundo real.”
Comportamento de cache favorece fortemente respostas 200. CloudFlare afirma explicitamente que NÃO fazem cache de respostas 204, enquanto AWS CloudFront fornece suporte limitado. Ambas CDNs oferecem capacidades completas de cache para respostas 200, potencialmente fornecendo maiores benefícios de performance que economias mínimas de largura de banda do 204.
Comportamento do navegador cria complicações de debug com respostas 204. Navegar para URLs retornando 204 no Chrome, Firefox e Safari resulta em requisições “canceladas” ou “abortadas” nas ferramentas de desenvolvedor, complicando workflows de teste e debug de API.
Considerações de experiência do desenvolvedor provam ser decisivas
Dados de pesquisa revelam forte preferência do desenvolvedor por consistência. Quando perguntados sobre manipulação de coleção vazia, desenvolvedores consistentemente citam “lógica simplificada do cliente” e “mesmo código de manipulação se lista está vazia ou populada” como vantagens primárias da abordagem 200.
Compatibilidade de biblioteca cliente varia significativamente. Embora a maioria dos clientes HTTP manipulem ambos códigos de status, respostas 200 funcionam uniformemente através de todas as bibliotecas sem requerer manipulação de caso especial. Bibliotecas populares como Axios requerem verificação explícita de status para respostas 204: response.status === 204 ? {} : response.data
.
Padrões de manipulação de erro tornam-se mais complexos com códigos de status mistos. APIs usando 204 para resultados vazios forçam clientes a manipular respostas bem-sucedidas diferentemente, quebrando o princípio de manipulação uniforme para operações bem-sucedidas.
Estratégias de documentação OpenAPI e teste
Recomendações da especificação OpenAPI 3.0 fortemente suportam 200 com arrays vazios. Melhores práticas incluem exemplos explícitos mostrando arrays vazios e definições consistentes de schema:
responses: '200': description: Lista de usuários (pode estar vazia) content: application/json: schema: type: array items: $ref: '#/components/schemas/Usuario' example: [] # Exemplo explícito de array vazio
Estratégias de teste beneficiam-se de estruturas de resposta consistentes. Suítes de teste podem validar schemas de resposta uniformemente sem lógica de ramificação para respostas vazias versus populadas. Testes de integração tornam-se mais simples quando as mesmas asserções funcionam independentemente da contagem de resultados.
Considerações de performance e largura de banda em contexto
Ambientes móveis e com restrição de largura de banda mostram resultados mistos. Embora respostas 204 eliminem overhead de análise JSON, a economia de largura de banda (2-50 bytes) empalidecem comparado aos padrões típicos de uso de dados móveis. Compressão HTTP reduz ainda mais o impacto prático de estruturas JSON vazias.
APIs de alto volume podem beneficiar-se de 204 em contextos específicos. APIs internas processando milhões de requisições diárias poderiam alcançar economias cumulativas significativas de largura de banda, mas apenas quando complexidade do cliente pode ser gerenciada através de implementação controlada.
Recomendações claras baseadas em evidência
Para APIs públicas e a maioria dos casos de uso: Use HTTP 200 com arrays vazios. Esta abordagem fornece:
- Consistência semântica com especificações HTTP
- Alinhamento com padrões da indústria
- Implementação simplificada do cliente
- Melhor suporte de cache CDN
- Experiência consistente do desenvolvedor
- Compatibilidade de framework
Reserve HTTP 204 para operações de escrita sem valores de retorno:
DELETE /api/usuarios/123 → 204 No ContentPUT /api/usuarios/123 → 204 No Content (se nenhum corpo de resposta necessário)
Padrão de implementação para coleções:
{ "dados": [], "meta": { "total_registros": 0, "pagina": 1, "por_pagina": 20 }}
Nunca use 404 para coleções vazias - reserve para recursos realmente ausentes onde o endpoint ou recurso específico não existe.
Conclusão
A evidência esmagadoramente suporta HTTP 200 OK com arrays vazios para coleções vazias em APIs RESTful. Esta abordagem satisfaz semânticas HTTP, segue princípios arquiteturais REST, alinha-se com consenso da indústria, simplifica implementação e fornece experiência superior ao desenvolvedor. Embora HTTP 204 ofereça apelo teórico e economia mínima de largura de banda, considerações práticas fazem 200 a escolha clara para design de API consistente, manutenível e amigável ao usuário.
O insight chave é que coleções são recursos com representações, mesmo quando vazias. Uma coleção vazia não é “nenhum conteúdo” - é conteúdo que por acaso está vazio, tornando 200 OK a escolha semanticamente correta e praticamente superior.
Sobre este Documento
Este guia serve como documentação viva que deve ser atualizada regularmente conforme a tecnologia evolui e as necessidades organizacionais mudam, mantendo as equipes brasileiras alinhadas com as melhores práticas internacionais adaptadas ao contexto local.
Este guia técnico foi desenvolvido especificamente para equipes de desenvolvimento brasileiras, considerando aspectos culturais, regulamentares e técnicos do mercado nacional. Combina melhores práticas internacionais com adaptações para o contexto brasileiro, fornecendo orientação prática e acionável para implementação de APIs RESTful robustas e conformes.
Fontes: Mozilla MDN, RFC 9110, APIs do Google Cloud, Diretrizes da API REST da Microsoft, REST API Tutorial e várias melhores práticas da indústria.