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ódigoQuando 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:

Um 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ódigoMensagem realO 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ódigoSituaçã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ódigoOnde 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ódigoNomeEm uma linha
200OKDeu certo, segue o conteúdo
201CreatedCriei — o endereço está no Location
204No ContentDeu certo, não há o que devolver
301Moved PermanentlyMudou de endereço para sempre (fica em cache)
302FoundMudou por ora, continue perguntando
304Not ModifiedSeu cache ainda vale
400Bad RequestA requisição está malformada
401UnauthorizedNão sei quem você é
403ForbiddenSei quem você é, e você não pode
404Not FoundIsso não existe aqui
409ConflictConflita com o estado atual
415Unsupported Media TypeContent-Type errado ou ausente
418I'm a teapotPiada de 1998 que sobreviveu
422Unprocessable EntityBem formado, mas viola uma regra
429Too Many RequestsDevagar — veja o Retry-After
451Unavailable For Legal ReasonsBloqueado por exigência legal
500Internal Server ErrorQuebrou na aplicação
502Bad GatewayO proxy recebeu lixo do upstream
503Service UnavailableFora do ar ou sobrecarregado
504Gateway TimeoutO 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.