Métodos HTTP e status code: 200, 201, 400, 404 e 500
Qual verbo usar em cada rota e qual status devolver em cada resposta, com tabela de decisão e a saída real de curl -i em cada caso de uma API Express.
O método HTTP diz o que você quer fazer com um recurso. O status code diz o que aconteceu com o pedido. Errar um dos dois não quebra a sua API na hora — quebra o cliente dela, semanas depois, de um jeito difícil de rastrear.
Todos os exemplos desta lição rodam contra a mesma API: o acervo da Biblioteca Vila Nova, com livros e empréstimos. Ela é um Express sobre a base que você montou em criar um servidor HTTP no Node e nas regras de recurso e contrato de o que é uma API REST.
import express from 'express';
const app = express();
app.use(express.json());
const livros = [
{ id: 1, titulo: 'Dom Casmurro', autor: 'Machado de Assis', isbn: '9788535910663', ano: 1899, emprestado: false },
{ id: 2, titulo: 'Vidas Secas', autor: 'Graciliano Ramos', isbn: '9788503012263', ano: 1938, emprestado: true },
];
let proximoId = 3;
const acharLivro = (id) => livros.find((l) => l.id === Number(id));
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Biblioteca Vila Nova ouvindo na porta ${PORT}`));Método e status ocupam lados diferentes do contrato HTTP. Em palavras simples, o método declara a intenção da requisição; o status resume o resultado que o servidor produziu para aquela intenção.
O pedido na ida e o comprovante na volta
Numa biblioteca, a ficha “quero renovar este empréstimo” acompanha o pedido na ida. Na volta, um comprovante informa “renovado”, “livro não encontrado” ou “pedido sem autorização”. O método HTTP é a ficha de intenção; o status code é o comprovante do resultado. Um não substitui o outro, e ambos orientam o próximo passo do cliente.
Antes de abrir a implementação, classifique cada cenário com duas respostas: qual método representa a intenção e qual família de status descreve o resultado. Depois envie a requisição e confira método, status e corpo. Se o cliente precisa adivinhar pelo texto do JSON, o contrato ainda não está claro.
Cada verbo é uma promessa que você faz a quem consome a API. Duas colunas dessa tabela valem mais que o resto: seguro quer dizer que a requisição não muda nada no servidor, e idempotente quer dizer que repetir o mesmo pedido dez vezes deixa o servidor no mesmo estado que uma vez só.
| verbo | intenção | manda corpo | seguro | idempotente |
|---|---|---|---|---|
GET |
ler um recurso ou uma coleção | não | sim | sim |
POST |
criar um recurso novo na coleção | sim | não | não |
PUT |
substituir o recurso inteiro | sim | não | sim |
PATCH |
alterar alguns campos do recurso | sim | não | não garantido |
DELETE |
remover o recurso | não | não | sim |
No Express, cada verbo tem um método com o mesmo nome. As duas rotas de leitura da biblioteca ficam assim:
app.get('/livros', (req, res) => {
res.status(200).json(livros);
});
app.get('/livros/:id', (req, res) => {
const livro = acharLivro(req.params.id);
if (!livro) {
return res.status(404).json({ erro: 'Livro não encontrado', id: Number(req.params.id) });
}
res.status(200).json(livro);
});Chamando as duas — e uma terceira vez com um id que não existe:
curl -s -w '\n-- %{http_code}\n' http://localhost:5599/livros
curl -s -w '\n-- %{http_code}\n' http://localhost:5599/livros/1
curl -s -w '\n-- %{http_code}\n' http://localhost:5599/livros/99O -w '%{http_code}' é o jeito mais curto de ver o status sem poluir a tela com
todos os headers. Guarde esse comando: ele volta várias vezes aqui.
curl -i -s -X OPTIONS http://localhost:5599/livros/2DELETE, GET, HEAD, PATCH, PUT
Idempotência: a propriedade que decide se dá para tentar de novo
Isso não é teoria de prova. Quando a internet do celular cai no meio da requisição, o app não sabe se o servidor recebeu ou não. Se o verbo é idempotente, ele reenvia sem medo. Se não é, ele precisa perguntar antes.
O PUT é idempotente: mandar o mesmo livro inteiro duas vezes dá o mesmo
resultado.
LIVRO='{"titulo":"Vidas Secas","autor":"Graciliano Ramos","isbn":"9788503012263","ano":1938}'
curl -s -w '\n-- %{http_code}\n' -X PUT http://localhost:5599/livros/2 -H 'Content-Type: application/json' -d "$LIVRO"
curl -s -w '\n-- %{http_code}\n' -X PUT http://localhost:5599/livros/2 -H 'Content-Type: application/json' -d "$LIVRO"Já o POST cria um recurso novo a cada chamada. Repetir um POST de
empréstimo significaria emprestar o mesmo livro duas vezes — e é justamente
para impedir isso que existe o status 409, que aparece daqui a pouco.
PUT ou PATCH: a decisão que trava todo mundo
A regra é literal. PUT substitui o recurso pela representação que você
mandou; PATCH altera só os campos que vieram. Quem usa PUT mandando
metade do objeto acaba apagando a outra metade sem perceber.
Por isso, na biblioteca, o PUT exige o livro inteiro e recusa o resto:
app.put('/livros/:id', (req, res) => {
const livro = acharLivro(req.params.id);
if (!livro) return res.status(404).json({ erro: 'Livro não encontrado' });
const { titulo, autor, isbn, ano } = req.body ?? {};
if (!titulo || !autor || !isbn || !ano) {
return res.status(400).json({ erro: 'PUT exige a representação inteira do livro' });
}
Object.assign(livro, { titulo, autor, isbn, ano });
res.status(200).json(livro);
});
app.patch('/livros/:id', (req, res) => {
const livro = acharLivro(req.params.id);
if (!livro) return res.status(404).json({ erro: 'Livro não encontrado' });
Object.assign(livro, req.body ?? {});
res.status(200).json(livro);
});O mesmo corpo de um campo só, nos dois verbos:
curl -s -w '\n-- %{http_code}\n' -X PUT http://localhost:5599/livros/2 -H 'Content-Type: application/json' -d '{"ano":1939}'
curl -s -w '\n-- %{http_code}\n' -X PATCH http://localhost:5599/livros/2 -H 'Content-Type: application/json' -d '{"ano":1939}'Na dúvida: se a tela é um formulário de edição que carrega tudo e salva tudo,
PUT. Se é um botão que muda uma coisa só — arquivar, renovar, marcar como
lido — PATCH.
O PATCH ingênuo aceita campo que você nunca quis expor
Olhe de novo para o Object.assign(livro, req.body ?? {}) ali em cima: ele
copia qualquer chave que chegou. Quem chama a rota é quem decide o que
escrever no seu objeto — inclusive id e emprestado, que não aparecem em
formulário de edição nenhum.
curl -s -w '\n-- %{http_code}\n' -X PATCH http://localhost:5599/livros/2 \
-H 'Content-Type: application/json' \
-d '{"emprestado":false,"id":99}'O livro trocou de identidade e voltou sozinho para a estante, e a API respondeu
200 como se estivesse tudo certo. “Alterar alguns campos” só vira promessa
cumprível quando a rota diz quais campos:
const CAMPOS_EDITAVEIS = ['titulo', 'autor', 'isbn', 'ano'];
app.patch('/livros/:id', (req, res) => {
const livro = acharLivro(req.params.id);
if (!livro) return res.status(404).json({ erro: 'Livro não encontrado' });
const campos = Object.keys(req.body ?? {});
const recusados = campos.filter((c) => !CAMPOS_EDITAVEIS.includes(c));
if (campos.length === 0) {
return res.status(400).json({ erro: 'PATCH precisa de pelo menos um campo' });
}
if (recusados.length > 0) {
return res.status(400).json({
erro: 'PATCH aceita só os campos editáveis do livro',
editaveis: CAMPOS_EDITAVEIS,
recusados,
});
}
Object.assign(livro, req.body);
res.status(200).json(livro);
});O mesmo pedido, agora com a lista no lugar:
curl -s -w '\n-- %{http_code}\n' -X PATCH http://localhost:5599/livros/2 \
-H 'Content-Type: application/json' \
-d '{"emprestado":false,"id":99}'Repare que a resposta é 400 e não 403: o pedido não é proibido, ele está
malformado para esta rota. E o campo emprestado volta a mudar só por onde o
empréstimo passa — que é a rota do 409, algumas seções abaixo.
As cinco famílias de status, numa tabela só
O primeiro dígito já conta a história inteira. Decorar esses cinco significados resolve 90% das dúvidas antes de você abrir a documentação.
| faixa | significado | quem errou | exemplos do dia a dia |
|---|---|---|---|
1xx |
“recebi, continue” | ninguém | 100 Continue, 101 Switching Protocols |
2xx |
deu certo | ninguém | 200, 201, 204 |
3xx |
mudou de endereço — ou não mudou nada | ninguém | 301, 302, 304 Not Modified |
4xx |
o cliente mandou algo errado | quem chamou | 400, 401, 403, 404, 409, 422 |
5xx |
o servidor falhou | você | 500, 502, 503, 504 |
A fronteira que importa é entre 4xx e 5xx: ela decide quem é acordado de
madrugada. Um 4xx é comportamento esperado da API. Um 5xx é bug seu, e
precisa aparecer no alerta.
201 não é 200: o Location diz onde o recurso nasceu
Criação bem-sucedida devolve 201 Created, não 200. E o 201 fica muito
melhor com o header Location apontando para o endereço do recurso novo:
app.post('/livros', (req, res) => {
const { titulo, autor, isbn, ano } = req.body ?? {};
// ...validações que vêm nas próximas seções...
const livro = { id: proximoId++, titulo, autor, isbn, ano, emprestado: false };
livros.push(livro);
res.status(201).location(`/livros/${livro.id}`).json(livro);
});curl -i -s -X POST http://localhost:5599/livros \
-H 'Content-Type: application/json' \
-d '{"titulo":"Grande Sertao: Veredas","autor":"Guimaraes Rosa","isbn":"9788520925171","ano":1956}'{“id”:3,“titulo”:“Grande Sertao: Veredas”,“autor”:“Guimaraes Rosa”,“isbn”:“9788520925171”,“ano”:1956,“emprestado”:false}
O front-end lê o Location e já sabe para onde navegar. Sem ele, resta ao
cliente adivinhar como você monta a URL do item.
204 é sucesso mudo — e ele engole o seu JSON sem avisar
204 No Content quer dizer “deu certo, e não tenho nada para te devolver”. É o
status certo para o DELETE da biblioteca:
app.delete('/livros/:id', (req, res) => {
const i = livros.findIndex((l) => l.id === Number(req.params.id));
if (i === -1) return res.status(404).json({ erro: 'Livro não encontrado' });
livros.splice(i, 1);
res.status(204).end();
});curl -i -s -X DELETE http://localhost:5599/livros/3Repare no que não está aí: nem Content-Type, nem Content-Length. Uma
resposta 204 não tem corpo nenhum, e por isso o Node nem escreve o cabeçalho
de tamanho. Muita gente espera ver Content-Length: 0 — não aparece.
E aqui está a armadilha. Se você escrever res.status(204).json(...), o Express
aceita sem reclamar e o corpo simplesmente some:
app.delete('/livros/:id', (req, res) => {
res.status(204).json({ mensagem: 'Livro removido do acervo' });
});O ETag prova que o Express chegou a calcular o JSON — e a resposta saiu vazia
mesmo assim. Sem erro, sem aviso, sem log. Se você precisa devolver alguma
mensagem, o status é 200, não 204.
400, 404 e 409: três jeitos diferentes de o cliente errar
400 Bad Request é “o pedido está malformado”: falta campo obrigatório, o tipo
está errado, o JSON não abre. 404 Not Found é “esse recurso não existe”.
409 Conflict é o mais esquecido dos três: o pedido está correto, mas briga
com o estado atual do servidor.
if (!titulo || !isbn) {
return res.status(400).json({
erro: 'Campos obrigatórios ausentes',
faltando: ['titulo', 'isbn'].filter((c) => !req.body?.[c]),
});
}
if (livros.some((l) => l.isbn === isbn)) {
return res.status(409).json({ erro: 'Já existe um livro com esse ISBN no acervo', isbn });
}curl -s -w '\n-- %{http_code}\n' -X POST http://localhost:5599/livros -H 'Content-Type: application/json' -d '{"autor":"Clarice Lispector"}'
curl -s -w '\n-- %{http_code}\n' -X POST http://localhost:5599/livros -H 'Content-Type: application/json' -d '{"titulo":"Dom Casmurro","autor":"Machado de Assis","isbn":"9788535910663","ano":1899}'O segundo pedido está perfeito: JSON válido, todos os campos, tipos certos.
Devolver 400 ali seria mentira. O problema é o acervo, não o pedido — e é
exatamente isso que o 409 comunica.
O caso mais bonito de 409 é o empréstimo. O livro existe, o leitor existe, o
pedido é impecável — só que o livro já saiu com outra pessoa:
app.post('/emprestimos', (req, res) => {
const livro = acharLivro(req.body?.livroId);
if (!livro) return res.status(404).json({ erro: 'Livro não encontrado' });
if (livro.emprestado) {
return res.status(409).json({ erro: 'Este livro já está emprestado', livroId: livro.id });
}
livro.emprestado = true;
res.status(201).location(`/emprestimos/${livro.id}`).json({ livroId: livro.id, leitor: req.body.leitor });
});curl -s -w '\n-- %{http_code}\n' -X POST http://localhost:5599/emprestimos -H 'Content-Type: application/json' -d '{"livroId":1,"leitor":"Marina"}'
curl -s -w '\n-- %{http_code}\n' -X POST http://localhost:5599/emprestimos -H 'Content-Type: application/json' -d '{"livroId":1,"leitor":"Joao"}'Mesmo endereço, mesmo formato, dois resultados diferentes — porque o estado do
acervo mudou entre uma chamada e outra. É a assinatura do 409.
401 é “não sei quem você é”; 403 é “sei, e não pode”
Os dois são recusa, e trocar um pelo outro atrapalha quem está integrando.
401 Unauthorized significa que faltou (ou falhou) a autenticação — e a
resposta precisa trazer o header WWW-Authenticate dizendo qual esquema
usar. 403 Forbidden significa que a identidade chegou, foi aceita, e mesmo
assim não tem permissão. Mandar credencial melhor não resolve um 403.
app.get('/relatorios', (req, res) => {
const token = req.get('Authorization');
if (!token) {
return res.status(401).set('WWW-Authenticate', 'Bearer').json({
erro: 'Envie o token no header Authorization',
});
}
if (token !== 'Bearer bibliotecaria') {
return res.status(403).json({ erro: 'Só a bibliotecária vê o relatório do acervo' });
}
res.status(200).json({ total: livros.length, emprestados: livros.filter((l) => l.emprestado).length });
});curl -i -s http://localhost:5599/relatorios{“erro”:“Envie o token no header Authorization”}
Com um token de leitor comum, a resposta muda de faixa dentro do mesmo 4xx:
curl -s -w '\n-- %{http_code}\n' http://localhost:5599/relatorios -H 'Authorization: Bearer leitor'422 existe mesmo? Quando ele ganha do 400
422 Unprocessable Content é para o pedido sintaticamente perfeito que não
passa numa regra de negócio. O JSON abriu, os campos estão lá, os tipos batem —
e mesmo assim o conteúdo não faz sentido. Um livro publicado em 2049, por
exemplo:
if (ano && ano > new Date().getFullYear()) {
return res.status(422).json({
erro: 'O ano de publicação não pode estar no futuro',
campo: 'ano',
recebido: ano,
});
}curl -s -w '\n-- %{http_code}\n' -X POST http://localhost:5599/livros \
-H 'Content-Type: application/json' \
-d '{"titulo":"O Livro do Futuro","autor":"Ninguem","isbn":"9781234567897","ano":2049}'Vale a pena? A minha resposta: use 422 se a sua API tem validação de
formulário e o front precisa marcar campo a campo; use 400 para todo o
resto. Um cliente que trata 400 genérico não quebra ao receber 422, mas um
422 bem usado deixa claro que o problema é o valor, não o formato — e essa
distinção poupa muita conversa. Se você for validar com uma biblioteca de
schema, o assunto continua em
validar a entrada da API com Zod.
500 e 503: o erro é seu, e o cliente não precisa saber por quê
500 é a resposta honesta para “quebrou aqui dentro e eu não previ”. A rota
abaixo tem um bug de propósito — o livro não tem a propriedade capa:
app.get('/livros/:id/capa', (req, res) => {
const livro = acharLivro(req.params.id);
res.status(200).json({ capa: livro.capa.url });
});
app.use((err, req, res, next) => {
const status = err.status ?? err.statusCode ?? 500;
if (status >= 500) {
console.error('[erro 500]', err.stack);
return res.status(500).json({ erro: 'Erro interno na biblioteca', id: 'log-7f21' });
}
res.status(status).json({ erro: err.message });
});Do lado de fora, o cliente recebe uma mensagem genérica com um identificador:
curl -s -w '\n-- %{http_code}\n' http://localhost:5599/livros/2/capaDo lado de dentro, o log guarda o que interessa:
Essa separação é regra, não estilo: stack trace na resposta HTTP entrega caminho de arquivo, versão de biblioteca e estrutura interna para qualquer pessoa na internet. O handler centralizado tem mais detalhes em tratamento de erro no Express.
O 503 Service Unavailable é diferente do 500: ele é planejado. Serve para
deploy, manutenção e sobrecarga — e aceita o header Retry-After dizendo em
quantos segundos vale a pena tentar de novo.
let emManutencao = true;
app.get('/busca', (req, res) => {
if (emManutencao) {
return res.status(503).set('Retry-After', '120').json({ erro: 'Índice de busca em reconstrução' });
}
res.status(200).json([]);
});curl -i -s http://localhost:5599/busca{“erro”:“Índice de busca em reconstrução”}
O erro mais comum: JSON quebrado saindo como 500
Este é o defeito que quase toda API iniciante tem, e ele nasce de um handler de erro de três linhas que parece inofensivo:
app.use((err, req, res, next) => {
console.error('[erro]', err.message);
res.status(500).json({ erro: 'Erro interno na biblioteca', id: 'log-7f21' });
});Agora mande um JSON com vírgula sobrando — erro do cliente, não seu:
curl -s -w '\n-- %{http_code}\n' -X POST http://localhost:5599/livros \
-H 'Content-Type: application/json' \
-d '{"titulo":"Capitaes da Areia",}'Sua API acabou de assumir a culpa por um erro de digitação de outra pessoa. E o
estrago é maior do que parece: o monitoramento vai acusar taxa de erro 5xx,
alguém vai ser acordado, e o cliente vai reenviar o mesmo JSON quebrado porque
5xx é o tipo de erro que se costuma tentar de novo.
A causa é simples. O express.json() lança um erro que já vem com o status
certo — err.status vale 400 —, e o handler acima joga esse número fora.
Basta respeitá-lo:
app.use((err, req, res, next) => {
const status = err.status ?? err.statusCode ?? 500;
if (status >= 500) {
console.error('[erro 500]', err.stack);
return res.status(500).json({ erro: 'Erro interno na biblioteca', id: 'log-7f21' });
}
res.status(status).json({ erro: err.message });
});O mesmo curl, com o handler corrigido:
Mesma requisição, mesma exceção, outra faixa de status. A diferença toda é uma
linha que lê err.status antes de decidir.
A tabela de decisão das sete rotas da biblioteca
Este mapa organiza as decisões de um CRUD. Cada linha foi conferida contra a API rodando — os status da coluna da direita saíram do terminal, não de uma estimativa.
| rota | sucesso | erro do cliente | quando acontece |
|---|---|---|---|
GET /livros |
200 |
— | coleção vazia também é 200, com [] |
GET /livros/:id |
200 |
404 |
id que não está no acervo |
POST /livros |
201 + Location |
400 · 409 · 422 |
campo faltando · ISBN repetido · ano no futuro |
PUT /livros/:id |
200 |
400 · 404 |
representação incompleta · id inexistente |
PATCH /livros/:id |
200 |
400 · 404 |
campo fora da lista editável · id inexistente |
DELETE /livros/:id |
204 |
404 |
id já removido |
POST /emprestimos |
201 + Location |
404 · 409 |
livro inexistente · livro já emprestado |
Três decisões dessa tabela são minhas, e valem ser ditas em voz alta: coleção
vazia é 200 e não 404 (a coleção existe, só está vazia); DELETE
repetido devolve 404 e não 204 (a segunda chamada realmente não encontrou
nada, e essa informação ajuda quem depura); e POST duplicado devolve 409
em vez de recriar em silêncio — é o 409 que compensa o POST não ser
idempotente.
Provando a tabela inteira num script só
Não confie na tabela: rode. Este script tem uma chamada para cada célula
dela — as sete rotas, com os sucessos e os erros de cliente de cada uma — e
imprime só o status. Dá para colar num npm run smoke e rodar depois de todo
deploy.
#!/bin/bash
API=http://localhost:5599
COMPLETO='{"titulo":"Dom Casmurro","autor":"Machado de Assis","isbn":"9788535910663","ano":1899}'
MACUNAIMA='{"titulo":"Macunaima","autor":"Mario de Andrade","isbn":"9788526017504","ano":1928}'
confere() {
local metodo=$1 rota=$2 corpo=$3
local codigo
if [ -n "$corpo" ]; then
codigo=$(curl -s -o /dev/null -w '%{http_code}' -X "$metodo" "$API$rota" \
-H 'Content-Type: application/json' -d "$corpo")
else
codigo=$(curl -s -o /dev/null -w '%{http_code}' -X "$metodo" "$API$rota")
fi
printf '%-6s %-16s -> %s\n' "$metodo" "$rota" "$codigo"
}
confere GET /livros
confere GET /livros/1
confere GET /livros/404
confere POST /livros "$MACUNAIMA"
confere POST /livros "$MACUNAIMA"
confere POST /livros '{"autor":"Mario de Andrade"}'
confere POST /livros '{"titulo":"Anuario","autor":"IBGE","isbn":"9781234567897","ano":2049}'
confere PUT /livros/1 "$COMPLETO"
confere PUT /livros/1 "$COMPLETO"
confere PUT /livros/1 '{"ano":1900}'
confere PUT /livros/404 "$COMPLETO"
confere PATCH /livros/1 '{"ano":1900}'
confere PATCH /livros/1 '{"emprestado":false}'
confere PATCH /livros/404 '{"ano":1900}'
confere POST /emprestimos '{"livroId":1,"leitor":"Marina"}'
confere POST /emprestimos '{"livroId":1,"leitor":"Joao"}'
confere POST /emprestimos '{"livroId":404,"leitor":"Joao"}'
confere DELETE /livros/1
confere DELETE /livros/1Dezenove chamadas, e nenhuma linha da tabela ficou por conta da memória. Repare
nos dois pares do começo: o mesmo POST duas vezes dá 201 e depois 409; o
mesmo PUT duas vezes dá 200 e 200. É a idempotência aparecendo no
terminal.
No front, 404 não cai no catch
Uma última consequência que pega muita gente. O fetch só rejeita a promessa
quando a rede falha. Status de erro chega como resposta normal, e você
precisa olhar o ok ou o status na mão:
let caiuNoCatch = false;
let resposta;
try {
resposta = await fetch('http://localhost:5599/livros/404');
} catch (erro) {
caiuNoCatch = true;
}
console.log('caiu no catch?', caiuNoCatch);
console.log('status:', resposta.status);
console.log('ok:', resposta.ok);
console.log('corpo:', await resposta.json());O try/catch sozinho não protege ninguém de um 404. É por isso que o status
importa tanto: ele é a única parte da resposta que o cliente consegue
interpretar sem entender o seu JSON. Esse tratamento no navegador está detalhado
em como consumir uma API com a Fetch API.
O que vem depois
Agora que cada rota devolve o status certo, o próximo passo é parar de montar tudo na mão: Express do zero mostra como subir o servidor, registrar as rotas e ler o corpo com muito menos código. O mapa completo até o deploy está no guia de Node.js e a ordem de estudo, na trilha de Node.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Posso devolver 200 em tudo e sinalizar o erro dentro do JSON?
response.ok do fetch dá true. O status é a parte da resposta que as ferramentas leem sem conhecer a sua API.Qual status devolvo quando o recurso não existe no DELETE?
404 conta a verdade do momento e ajuda quem está depurando; 204 sempre torna o DELETE completamente idempotente do ponto de vista do cliente. Escolha uma, documente, e não misture as duas na mesma API.Preciso do header Location no 201?
Location apontando para o recurso criado, o cliente sabe para onde redirecionar sem adivinhar como você monta a URL do item.Quando uso 405 em vez de 404?
405 Method Not Allowed é para quando o caminho existe mas o verbo não. DELETE /livros numa API que só aceita GET e POST na coleção é 405, e a resposta precisa trazer o header Allow com os verbos aceitos. 404 é para o caminho que não existe de jeito nenhum.502, 503 e 504 são a mesma coisa?
502 é o proxy recebendo lixo de quem está atrás dele, 503 é o serviço indisponível de propósito (deploy, manutenção, sobrecarga) e 504 é o proxy cansando de esperar. Só o 503 costuma sair da sua aplicação; os outros dois quase sempre vêm do Nginx ou do balanceador.Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 com Express 5.2.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — HTTP request methods — developer.mozilla.org
- MDN — HTTP response status codes — developer.mozilla.org
- RFC 9110 — HTTP Semantics — rfc-editor.org
- Express 5 — API Reference: res.status() — expressjs.com



