Toda resposta HTTP começa com um número. Antes do JSON, antes de qualquer campo, a API já disse alguma coisa — e quem consome precisa decidir o que fazer com base nisso: mostrar o dado, pedir login, corrigir o formulário ou tentar de novo daqui a pouco.
É por isso que o status code não é um detalhe cosmético. Ele é a primeira instrução que o cliente recebe. Devolver o código errado não deixa a resposta feia: faz o cliente tomar a decisão errada.
🧭 O primeiro dígito aponta a origem
Antes de decorar códigos, vale internalizar a lógica das faixas. O primeiro dígito responde a uma pergunta só: de onde veio o problema?
| Faixa | Significado | O cliente deve... |
|---|---|---|
| 1xx | Informativo — a requisição continua | esperar |
| 2xx | Sucesso | seguir com a resposta |
| 3xx | Redirecionamento — está em outro lugar | ir ao endereço indicado |
| 4xx | Erro do cliente | corrigir a requisição — repetir igual não resolve |
| 5xx | Erro do servidor | tentar de novo mais tarde |
Essa distinção entre 4xx e 5xx é a mais importante de todas, e é onde mora o erro mais comum em APIs jovens. Um exemplo clássico em Spring Data:
// busca um aluno por id
return repository.findById(id).get();
Se o id não existir, o Optional.get() lança
NoSuchElementException, e sem tratamento o Spring converte isso em
500. Ou seja: a API responde "eu falhei, tente de novo"
quando a informação correta seria "esse recurso não existe, pare de tentar".
A diferença é prática. Um cliente bem escrito faz retry automático em 5xx e não faz em 4xx. Com o código errado, ele vai insistir indefinidamente em uma busca que nunca vai dar certo.
✅ A faixa 2xx: nem todo sucesso é 200
É tentador responder 200 OK para tudo que deu certo. Funciona, mas joga fora informação
que o cliente poderia usar.
| Código | Quando usar |
|---|---|
| 200 OK | Sucesso com corpo de resposta. O caso geral de GET e PUT. |
| 201 Created | Um recurso novo foi criado. Deve vir acompanhado do header
Location apontando para ele. |
| 204 No Content | Deu certo e não há nada para devolver. O retorno natural de um DELETE. |
O 201 com Location economiza uma requisição inteira: em vez de o cliente
vasculhar o JSON atrás do id gerado para montar a próxima URL, ele já recebe o endereço
pronto no header.
POST /alunos → 201 Created
Location: /alunos/42
DELETE /alunos/42 → 204 No Content (corpo vazio)
Um DELETE que devolve 200 com corpo vazio comunica a mesma
coisa, mas o 204 diz explicitamente que a ausência de corpo é intencional — e não uma
resposta que veio truncada.
↪️ 301 e 302: uma delas é quase definitiva
Os dois redirecionam, mas com consequências bem diferentes:
301 Moved Permanently— o navegador guarda esse redirecionamento em cache, e de forma agressiva. Nas próximas visitas ele nem consulta o servidor: vai direto ao novo endereço.302 Found— redirecionamento temporário. O cliente continua perguntando ao servidor a cada requisição.
301 publicado por engano é difícil de desfazer: quem já acessou tem o redirecionamento
gravado no navegador, e você não tem como limpar esse cache remotamente. Na dúvida, comece com
302 — ele é reversível.
Vale citar ainda o 304 Not Modified, que não é bem um redirecionamento: é a resposta a uma
requisição condicional dizendo "o que você já tem em cache continua válido, pode usar".
🔐 401 e 403: o par mais trocado
Esses dois se confundem por um motivo justo — o 401 se chama
Unauthorized, mas trata de autenticação, não de autorização. A distinção
correta é:
| Código | Mensagem real | O cliente deve... |
|---|---|---|
| 401 Unauthorized | "não sei quem você é" — falta credencial, ou ela expirou | fazer login e repetir |
| 403 Forbidden | "sei quem você é, e você não pode" | desistir — repetir com a mesma credencial não vai mudar nada |
A regra prática: se refazer o login resolveria, é 401. Se nem o login resolve, é
403.
Há ainda uma consideração de segurança. Responder 403 confirma que o recurso
existe — você só não tem acesso. Em endpoints sensíveis, isso vira uma forma de
enumerar dados: basta variar o id e observar quem responde 403 e quem responde
404. Por isso, em certos casos, devolver 404 mesmo para algo que existe é a
escolha mais defensiva.
🚫 400, 404, 422 e companhia
Dentro da faixa 4xx, a diferença está em qual camada rejeitou a requisição:
| Código | Situação |
|---|---|
| 400 Bad Request | A requisição está malformada: JSON inválido, campo obrigatório ausente, tipo errado. |
| 404 Not Found | A rota ou o recurso não existe. |
| 409 Conflict | A requisição é válida, mas conflita com o estado atual — um CPF já cadastrado, por exemplo. |
| 415 Unsupported Media Type | Falta o Content-Type: application/json, ou ele veio errado. |
| 422 Unprocessable Entity | O JSON está impecável, mas viola uma regra de negócio — uma data de nascimento no futuro, um valor negativo. |
| 429 Too Many Requests | Limite de requisições estourado. Acompanhe do header Retry-After. |
A fronteira entre 400 e 422 é a que mais gera debate. Um critério simples
que funciona bem: se o erro foi detectado ao ler a requisição, é 400; se foi detectado
ao interpretá-la, com todos os campos já válidos, é 422.
💥 500, 502, 503 e 504: onde exatamente quebrou
Essa faixa tem um valor de diagnóstico que costuma passar despercebido: ela indica em que ponto da infraestrutura a coisa falhou.
| Código | Onde está o problema |
|---|---|
| 500 Internal Server Error | Na sua aplicação. Ela respondeu — e a resposta foi uma exceção não tratada. |
| 502 Bad Gateway | No proxy à frente da aplicação. Ele recebeu algo inválido do upstream, ou não recebeu nada. |
| 503 Service Unavailable | O serviço está fora do ar de propósito ou sobrecarregado. Combina com
Retry-After. |
| 504 Gateway Timeout | O proxy cansou de esperar. A aplicação está de pé, mas demorou demais. |
Na prática isso corta caminho na investigação: um 500 manda você direto para os logs da
aplicação. Já 502 e 504 geralmente significam que a aplicação
nem chegou a responder — vale olhar antes se o processo subiu, se a porta está certa
e quanto tempo a requisição está levando.
🫖 418 e 451: os dois com história
418 I'm a teapot nasceu na RFC 2324, o
Hyper Text Coffee Pot Control Protocol, publicada em 1º de abril de 1998 como piada. O código
indica que o aparelho é um bule de chá e, portanto, não pode preparar café. Nunca entrou no padrão
sério de HTTP — mas sobreviveu, e alguns serviços passaram a usá-lo como resposta discreta para
tráfego automatizado detectado.
451 Unavailable For Legal Reasons, por outro lado, é oficial: está na
RFC 7725 e sinaliza conteúdo bloqueado por ordem judicial ou exigência legal. O número é uma referência
a Fahrenheit 451, o romance de Ray Bradbury sobre queima de livros. A diferença em relação a
um 403 genérico é a transparência — o 451 deixa explícito que existe censura,
em vez de fingir que a página sumiu.
⚙️ Devolvendo o código certo no Spring Boot
Saber o código correto resolve metade; a outra metade é fazer o framework devolvê-lo. O Spring oferece alguns caminhos, do mais pontual ao mais estruturado.
1. ResponseEntity — controle total
@PostMapping("/alunos")
public ResponseEntity<Aluno> criar(@RequestBody @Valid AlunoForm form) {
Aluno aluno = service.criar(form);
URI local = URI.create("/alunos/" + aluno.getId());
return ResponseEntity.created(local).body(aluno); // 201 + Location
}
2. @ResponseStatus — quando o código é fixo
@DeleteMapping("/alunos/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204
public void deletar(@PathVariable Long id) {
service.deletar(id);
}
3. ResponseStatusException — no fluxo do serviço
return repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Aluno " + id + " não encontrado"));
Trocar .get() por .orElseThrow() é a menor mudança possível para converter
aquele 500 do começo do artigo em um 404 honesto.
4. @ControllerAdvice + ProblemDetail
Para padronizar o formato de erro em toda a API, o Spring 6 traz o ProblemDetail, que
implementa a RFC 7807 — o mesmo application/problem+json que uso no
Budgeting:
@RestControllerAdvice
public class TratadorDeErros {
@ExceptionHandler(RegraDeNegocioException.class)
public ProblemDetail regraViolada(RegraDeNegocioException e) {
ProblemDetail p = ProblemDetail.forStatusAndDetail(
HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage()); // 422
p.setTitle("Regra de negócio violada");
return p;
}
}
A vantagem é que o cliente passa a receber sempre a mesma estrutura de erro, com
status, title, detail e instance — em vez de um
formato diferente para cada exceção. Percorro esse retorno na prática, campo por campo, no artigo
sobre Swagger UI.
📊 Duas convenções, lado a lado
Convenções diferentes servem a momentos diferentes. Comparando dois projetos meus:
| Situação | Convenção enxuta | Convenção estrita |
|---|---|---|
| Recurso criado | 200 com o objeto | 201 + Location |
| Remoção concluída | 200 com corpo vazio | 204 |
id inexistente |
500 | 404 |
| Validação de campo | 400 | 400 em
problem+json |
| Regra de negócio violada | — | 422 |
A da esquerda é a da Academia Digital, e aparece em detalhe no artigo sobre o Postman. Ela é mais simples de escrever e suficiente quando o cliente é você mesmo. A da direita é a do Budgeting: exige mais código, e entrega em troca um contrato que qualquer cliente consegue seguir sem ler documentação.
A única linha em que não há troca a ponderar é a do id inexistente. Ali o
500 não é uma escolha de estilo: é uma informação incorreta sobre o que aconteceu.
🗂️ Referência rápida
| Código | Nome | Em uma linha |
|---|---|---|
| 200 | OK | Deu certo, segue o conteúdo |
| 201 | Created | Criei — o endereço está no Location |
| 204 | No Content | Deu certo, não há o que devolver |
| 301 | Moved Permanently | Mudou de endereço para sempre (fica em cache) |
| 302 | Found | Mudou por ora, continue perguntando |
| 304 | Not Modified | Seu cache ainda vale |
| 400 | Bad Request | A requisição está malformada |
| 401 | Unauthorized | Não sei quem você é |
| 403 | Forbidden | Sei quem você é, e você não pode |
| 404 | Not Found | Isso não existe aqui |
| 409 | Conflict | Conflita com o estado atual |
| 415 | Unsupported Media Type | Content-Type errado ou ausente |
| 418 | I'm a teapot | Piada de 1998 que sobreviveu |
| 422 | Unprocessable Entity | Bem formado, mas viola uma regra |
| 429 | Too Many Requests | Devagar — veja o Retry-After |
| 451 | Unavailable For Legal Reasons | Bloqueado por exigência legal |
| 500 | Internal Server Error | Quebrou na aplicação |
| 502 | Bad Gateway | O proxy recebeu lixo do upstream |
| 503 | Service Unavailable | Fora do ar ou sobrecarregado |
| 504 | Gateway Timeout | O upstream demorou demais |
✅ Conclusão
Não é preciso cobrir a lista inteira para ter uma API bem-comportada. Com meia dúzia de códigos usados
com critério — 200, 201, 204, 400,
404 e 500 — já se comunica quase tudo que uma aplicação precisa dizer.
O que faz diferença é o hábito: antes de escrever o return, perguntar o que aquele número
está instruindo o cliente a fazer. Se a resposta for "tentar de novo" quando não há nada a tentar, o
código está errado — por mais correto que o JSON esteja.