req.params, req.query e req.body no Express: qual usar
De onde vem cada dado que chega na rota, como declarar parâmetro na URL, ler filtros da query string e receber JSON no corpo sem quebrar nada.
Todo dado que chega numa rota do Express vem de um destes lugares: o caminho da
URL, a query string ou o corpo da requisição. Cada lugar tem seu objeto —
req.params, req.query e req.body — e pegar no objeto errado é a causa
número um de rota que responde undefined.
Os exemplos todos são a API da Escola Girassol, que guarda turmas, alunos e
tarefas de casa. Tudo aqui rodou em Node 24.16.0 com Express 5.2.1, na porta
3210, e cada resposta foi conferida no curl. Se você ainda não subiu um
servidor, comece pela lição de
Express do zero e volte aqui.
Endereço, bilhete e envelope: três lugares para o dado chegar
Uma entrega pode trazer informação no endereço da caixa, num bilhete preso do lado de fora ou dentro de um envelope. Misturar os lugares faz você procurar o número da casa dentro do pacote e concluir que ele não veio.
No Express, o trecho variável do caminho vai para req.params, a query
string vai para req.query e o corpo vai para req.body. São três
origens, não três nomes para a mesma coisa. Na rota espelho, preveja em qual
objeto cada valor do curl aparecerá e depois confira o JSON devolvido; ele
funciona como raio-X da requisição.
Uma requisição HTTP é um envelope com partes separadas. O Express abre o
envelope e coloca cada parte num objeto diferente do req:
A regra que decide o objeto é curta:
| objeto | de onde vem | serve para |
|---|---|---|
req.params |
trecho do caminho que você declarou com : |
dizer qual recurso: a tarefa 7, a turma 9A |
req.query |
tudo depois do ?, que você não declara |
filtrar, ordenar e paginar uma lista |
req.body |
o corpo da requisição, em POST, PUT e PATCH | mandar dados novos ou alterados |
req.headers |
os cabeçalhos HTTP | token, formato do corpo, idioma |
req.params: o que você declarou com dois-pontos
req.params só tem as chaves que você escreveu na rota. Nada aparece ali
por acaso: cada : no caminho vira uma propriedade com esse nome.
import express from 'express';
const app = express();
const alunos = [
{ matricula: '2024031', nome: 'Ana Prado', turma: '9A' },
{ matricula: '2024097', nome: 'Bruno Lima', turma: '9A' },
{ matricula: '2024104', nome: 'Carla Dias', turma: '8B' },
];
app.get('/alunos/:matricula', (req, res) => {
console.log('req.params ->', req.params);
const aluno = alunos.find((a) => a.matricula === req.params.matricula);
if (!aluno) return res.status(404).json({ erro: 'Aluno não encontrado' });
res.json(aluno);
});
app.get('/turmas/:turma/alunos/:matricula', (req, res) => {
console.log('req.params ->', req.params);
res.json({ turma: req.params.turma, matricula: req.params.matricula });
});
app.listen(3210, () => console.log('Escola Girassol na porta 3210'));Com o servidor no ar, três chamadas:
curl -s http://localhost:3210/alunos/2024031
curl -s http://localhost:3210/alunos/9999999
curl -s http://localhost:3210/turmas/9A/alunos/2024097E, no terminal do servidor:
Duas coisas para guardar. A primeira: req.params nunca fica com o nome do
parâmetro errado — se a rota diz :matricula, é req.params.matricula, e
req.params.id seria undefined. A segunda: esse [Object: null prototype] do
console não é defeito, é proteção — e vale entender contra o quê.
Um objeto comum herda de Object.prototype, então ele já nasce respondendo a
constructor, toString e hasOwnProperty sem que ninguém tenha mandado nada.
O Express monta req.params e req.query sem protótipo nenhum, e aí só existe
ali o que chegou na requisição:
app.get('/alunos/:matricula', (req, res) => {
console.log('valor recebido ->', req.params.matricula);
console.log('req.params.constructor ->', req.params.constructor);
console.log('req.query.constructor ->', req.query.constructor);
console.log('objeto comum ->', typeof {}.constructor);
res.json({ ok: true });
});curl -s "http://localhost:3210/alunos/constructor?constructor=oi"Compare a última linha com as outras. Em /alunos/constructor, a palavra
constructor é só o valor de matricula — o nome da chave quem escolheu foi
você, na rota, então não há como o cliente esbarrar em nada herdado. O risco mora
em req.query, onde quem batiza a chave é o cliente: o ?constructor=oi criou
uma chave chamada constructor. Num objeto comum, ler .constructor devolveria
a função herdada e a sua checagem passaria por cima do que o usuário mandou; sem
protótipo, constructor é só mais uma chave, e o que não existe é
hasOwnProperty e companhia. As chaves de verdade você lê normalmente.
O :id? do Express 4 não existe mais
Muito tutorial na internet ainda ensina parâmetro opcional com interrogação. No Express 5 isso derruba o servidor na hora de subir, não na requisição:
app.get('/tarefas/:id?', (req, res) => {
res.json(req.params);
});O rastro segue por mais algumas linhas dentro do path-to-regexp, mas a
primeira já entrega o diagnóstico. A forma nova é chave: /tarefas{/:id}. Ela
aparece funcionando no fim desta lição.
req.query: filtro, paginação e a chave repetida
req.query é o oposto de req.params: você não declara nada. Tudo que vier
depois do ? chega ali, na hora, sem mudar a rota.
import express from 'express';
const app = express();
const tarefas = [
{ id: 1, titulo: 'Lista de frações', turma: '9A', status: 'pendente' },
{ id: 2, titulo: 'Resenha do livro', turma: '9A', status: 'entregue' },
{ id: 3, titulo: 'Mapa do Brasil', turma: '8B', status: 'pendente' },
{ id: 4, titulo: 'Experimento de densidade', turma: '9A', status: 'pendente' },
];
app.get('/tarefas', (req, res) => {
console.log('req.query ->', req.query);
const { turma, status, pagina = '1', porPagina = '2' } = req.query;
let lista = tarefas;
if (turma) lista = lista.filter((t) => t.turma === turma);
if (status) lista = lista.filter((t) => t.status === status);
const inicio = (Number(pagina) - 1) * Number(porPagina);
res.json({
total: lista.length,
pagina: Number(pagina),
itens: lista.slice(inicio, inicio + Number(porPagina)),
});
});
app.listen(3210, () => console.log('Escola Girassol na porta 3210'));Repare no valor padrão pagina = '1': como nada é obrigatório na query string,
a rota precisa funcionar quando o cliente não manda nada.
curl -s "http://localhost:3210/tarefas?turma=9A&status=pendente"
curl -s "http://localhost:3210/tarefas?turma=9A&pagina=2"
curl -s "http://localhost:3210/tarefas"No terminal do servidor, as três chamadas na mesma ordem. Repare na última: sem
query nenhuma, req.query chega como objeto vazio — e não como undefined.
A mesma chave duas vezes vira array
Aqui mora uma armadilha que custa uma tarde. Se o front montar a URL num laço e repetir uma chave, o valor deixa de ser string e vira array. O filtro compara string com array, e a lista volta vazia:
curl -s "http://localhost:3210/tarefas?turma=9A&turma=9A"A turma existe, o filtro está certo, e mesmo assim deu zero. Sempre que um valor
da query alimentar uma comparação, normalize antes:
const turma = Array.isArray(req.query.turma) ? req.query.turma[0] : req.query.turma.
Quando o parâmetro é mesmo uma lista (?status=pendente&status=entregue), faça o
contrário e force o array com [].concat(req.query.status ?? []).
Chave aninhada depende do parser
No Express 5 o interpretador padrão da query string é o simple, que não monta
objeto aninhado. filtro[turma]=9A chega como uma chave literal, com colchete e
tudo:
const app = express();
// app.set('query parser', 'extended'); ← ligue para comparar
app.get('/relatorio', (req, res) => {
console.log('req.query ->', req.query);
res.json(req.query);
});Com o padrão, batendo em /relatorio?filtro[turma]=9A&filtro[status]=pendente:
Descomentando a linha do extended, a mesma URL vira objeto de verdade:
req.body: sem parser, ele chega undefined
O corpo da requisição não é um objeto pronto: ele chega como uma sequência de bytes que alguém precisa juntar e converter. No Express, esse alguém é um middleware. Sem ele, a rota quebra:
import express from 'express';
const app = express();
app.post('/tarefas', (req, res) => {
console.log('req.body ->', req.body);
res.status(201).json({ criada: req.body.titulo });
});
app.listen(3210, () => console.log('Escola Girassol na porta 3210'));curl -s -X POST http://localhost:3210/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo":"Lista de frações","turma":"9A"}'Esse é o TypeError: Cannot read properties of undefined mais comum do back-end iniciante — e o cliente recebe um 500 com uma página HTML de erro. A correção é uma linha, montada antes das rotas:
import express from 'express';
const app = express();
app.use(express.json());
const tarefas = [];
app.post('/tarefas', (req, res) => {
console.log('req.body ->', req.body);
const { titulo, turma, nota } = req.body;
if (!titulo || !turma) {
return res.status(400).json({ erro: 'titulo e turma são obrigatórios' });
}
const tarefa = { id: tarefas.length + 1, titulo, turma, nota: nota ?? null };
tarefas.push(tarefa);
res.status(201).json(tarefa);
});
app.listen(3210, () => console.log('Escola Girassol na porta 3210'));Agora a mesma chamada de antes, com uma nota junto:
curl -s -X POST http://localhost:3210/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo":"Lista de frações","turma":"9A","nota":10}'express.json() é um middleware como qualquer
outro: ele roda antes da rota, lê o corpo e preenche req.body. Se você montar
depois das rotas, é como não ter montado.
O parser olha o Content-Type antes de agir
express.json() não converte tudo: ele só age quando o cabeçalho Content-Type
diz application/json. Mande o mesmo JSON sem o cabeçalho e o corpo é ignorado
em silêncio. Como o curl usa application/x-www-form-urlencoded quando você
passa -d sem -H, este erro acontece o tempo todo:
curl -s -X POST http://localhost:3210/tarefas \
-d '{"titulo":"Mapa do Brasil","turma":"8B"}'Para aceitar formulário HTML clássico, monte o segundo parser:
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/tarefas', (req, res) => {
console.log('content-type ->', req.headers['content-type']);
console.log('req.body ->', req.body);
res.status(201).json({ recebido: req.body });
});curl -s -X POST http://localhost:3210/tarefas -d "titulo=Mapa do Brasil&turma=8B¬a=10"
curl -s -X POST http://localhost:3210/tarefas -H "Content-Type: application/json" \
-d '{"titulo":"Mapa do Brasil","turma":"8B","nota":10}'Compare o nota das duas linhas. Vindo de formulário, é a string '10'. Vindo
de JSON, é o número 10. Mesmo campo, mesma rota, tipos diferentes — e é isso
que leva à próxima seção.
O id que virou '7'
Nada que vem da URL é número. Caminho e query string são texto, e o Express entrega texto. Este bug parece impossível quando acontece:
const tarefas = [
{ id: 7, titulo: 'Lista de frações', turma: '9A', nota: 8.5 },
{ id: 8, titulo: 'Mapa do Brasil', turma: '8B', nota: 9 },
];
app.get('/tarefas/:id', (req, res) => {
console.log('req.params.id ->', req.params.id, '| typeof:', typeof req.params.id);
console.log('comparando com ===:', tarefas[0].id === req.params.id);
const tarefa = tarefas.find((t) => t.id === req.params.id);
if (!tarefa) return res.status(404).json({ erro: 'Tarefa não encontrada' });
res.json(tarefa);
});
app.get('/turmas/:turma/media', (req, res) => {
const bonus = req.query.bonus ?? 0;
console.log('bonus ->', bonus, '| typeof:', typeof bonus);
const daTurma = tarefas.filter((t) => t.turma === req.params.turma);
const media = daTurma.reduce((s, t) => s + t.nota, 0) / daTurma.length;
res.json({ media, comBonus: media + bonus });
});curl -s http://localhost:3210/tarefas/7
curl -s "http://localhost:3210/turmas/9A/media?bonus=1"A tarefa 7 existe e voltou 404. A média com bônus de 1 ponto deu "8.51" — uma
string, porque 8.5 + '1' concatena em vez de somar. O log explica:
A correção é converter na entrada da rota, uma vez, e nunca mais pensar nisso.
Converter também é a hora de validar — o cliente pode mandar /tarefas/abc:
app.get('/tarefas/:id', (req, res) => {
const id = Number(req.params.id);
if (!Number.isInteger(id)) {
return res.status(400).json({ erro: 'id precisa ser um número inteiro' });
}
const tarefa = tarefas.find((t) => t.id === id);
if (!tarefa) return res.status(404).json({ erro: 'Tarefa não encontrada' });
res.json(tarefa);
});
app.get('/turmas/:turma/media', (req, res) => {
const bonus = Number(req.query.bonus ?? 0);
const daTurma = tarefas.filter((t) => t.turma === req.params.turma);
const media = daTurma.reduce((s, t) => s + t.nota, 0) / daTurma.length;
res.json({ media, comBonus: media + bonus });
});As quatro linhas são, na ordem: /tarefas/7, /tarefas/abc,
/turmas/9A/media?bonus=1 e /turmas/9A/media sem bônus. Os detalhes de
Number, parseInt e o que cada um faz com '12abc' estão em
converter string em número.
req.headers: minúsculo, sempre
Cabeçalho não é dado do recurso — é dado sobre a requisição: quem está chamando, em que formato, em que idioma. No Node, os nomes chegam sempre em minúsculas, independente de como o cliente escreveu:
app.get('/boletim/:matricula', (req, res) => {
console.log('req.headers.authorization ->', req.headers.authorization);
console.log('req.headers.Authorization ->', req.headers.Authorization);
console.log("req.get('AUTHORIZATION') ->", req.get('AUTHORIZATION'));
console.log("req.get('X-Escola') ->", req.get('X-Escola'));
const token = req.get('authorization')?.replace('Bearer ', '');
if (token !== 'token-da-secretaria') {
return res.status(401).json({ erro: 'Token inválido' });
}
res.json({ matricula: req.params.matricula, media: 8.5 });
});curl -s http://localhost:3210/boletim/2024031 \
-H "Authorization: Bearer token-da-secretaria" -H "X-Escola: girassol"O cliente mandou Authorization com A maiúsculo, e mesmo assim
req.headers.Authorization é undefined. Por isso existe req.get(): ele
ignora maiúsculas e minúsculas e é o jeito seguro de ler cabeçalho.
Quando /tarefas/nova cai no handler de /tarefas/:id
O Express testa as rotas na ordem em que você registrou e para na primeira que casar. Um parâmetro casa com qualquer texto — inclusive com uma palavra que você queria tratar à parte:
const tarefas = [{ id: 7, titulo: 'Lista de frações', turma: '9A' }];
app.get('/tarefas/:id', (req, res) => {
console.log('caiu em /tarefas/:id ->', req.params);
const tarefa = tarefas.find((t) => t.id === Number(req.params.id));
if (!tarefa) return res.status(404).json({ erro: 'Tarefa não encontrada' });
res.json(tarefa);
});
app.get('/tarefas/nova', (req, res) => {
console.log('caiu em /tarefas/nova');
res.json({ titulo: '', turma: '', prazoSugerido: '2026-07-30' });
});curl -s http://localhost:3210/tarefas/novaNote o que o log mostra: a requisição chegou, entrou no handler errado e a
palavra nova virou um id. Como Number('nova') é NaN, o find não achou
nada e o cliente recebeu um 404 mentiroso — a rota /tarefas/nova existe, nunca
foi consultada. O console.log no começo de cada handler é o que transforma
esse mistério em diagnóstico de dez segundos.
A correção não mexe no código das rotas, só na ordem: o caminho fixo vem antes do caminho com parâmetro.
-app.get('/tarefas/:id', (req, res) => { /* ... */ });
-app.get('/tarefas/nova', (req, res) => { /* ... */ });
+app.get('/tarefas/nova', (req, res) => { /* ... */ });
+app.get('/tarefas/:id', (req, res) => { /* ... */ });curl -s http://localhost:3210/tarefas/nova
curl -s http://localhost:3210/tarefas/7Agora /tarefas/nova devolve o modelo em branco e /tarefas/7 devolve a tarefa.
Guarde a regra: rota específica antes de rota genérica, sempre.
A rota espelho: os quatro objetos numa requisição só
Esta é a ferramenta que vale o artigo inteiro. Uma rota que não faz nada além de devolver tudo que chegou. Cole num arquivo, suba e bata nela sempre que estiver em dúvida sobre de onde vem um dado:
import express from 'express';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.all('/espelho{/:recurso}{/:id}', (req, res) => {
const espelho = {
metodo: req.method,
caminho: req.path,
params: { ...req.params },
query: { ...req.query },
body: req.body ?? null,
contentType: req.get('content-type') ?? null,
};
console.log(JSON.stringify(espelho, null, 2));
res.json(espelho);
});
app.listen(3210, () => console.log('Escola Girassol na porta 3210'));app.all responde a qualquer método, e {/:recurso}{/:id} é a sintaxe do
Express 5 para trecho opcional — a que substituiu o :id?. Quatro chamadas
diferentes, para ver os quatro objetos se mexendo:
curl -s http://localhost:3210/espelho/tarefas/7
curl -s "http://localhost:3210/espelho/tarefas?turma=9A&status=pendente"
curl -s -X POST http://localhost:3210/espelho/tarefas \
-H "Content-Type: application/json" -d '{"titulo":"Lista de frações","nota":8.5}'
curl -s -X POST "http://localhost:3210/espelho/tarefas/7?revisar=sim" -d "nota=9"Leia as quatro linhas de cima para baixo, comparando as colunas:
- na primeira, o
7entrou emparamsporque estava no caminho;queryveio vazio ebodyveionull; - na segunda, o mesmo recurso sem id:
paramsperdeu oidequeryganhou os dois filtros; - na terceira, o
notaveio8.5, número, porque oContent-Typeera JSON; - na quarta, o mesmo
notaveio"9", string, porque o corpo era formulário — e ainda assimparamsequeryforam preenchidos ao mesmo tempo.
Essa última linha responde a pergunta que todo iniciante faz: sim, uma requisição pode carregar as três origens de uma vez. Elas não competem.
Onde colocar cada dado quando você é quem decide
Quando você desenha a API, a escolha é sua. O critério que uso:
| o dado é… | vai em | exemplo na Escola Girassol |
|---|---|---|
| a identidade do recurso | caminho (req.params) |
/tarefas/7, /turmas/9A/alunos/2024031 |
| um recorte da lista | query (req.query) |
?turma=9A&status=pendente&pagina=2 |
| conteúdo que vai ser gravado | corpo (req.body) |
{ "titulo": "Lista de frações" } |
| credencial ou metadado | cabeçalho (req.headers) |
Authorization: Bearer … |
Dois testes rápidos resolvem quase toda dúvida. Primeiro: dá para mandar essa URL no WhatsApp e a pessoa ver a mesma coisa? Se sim, o dado pertence à URL — caminho ou query. Segundo: você se incomodaria de ver esse valor no log do servidor? Se sim, ele é corpo ou cabeçalho, nunca query — senha e token na URL vão parar no histórico do navegador e no log de acesso.
E nada disso substitui validação. Os três objetos são texto que veio da rua:
quem garante que nota é um número entre 0 e 10 é a sua rota, não o Express.
É o assunto de validar a entrada da API.
O que vem depois
Com params, query e body na mão, sua rota já sabe receber. O próximo passo é
organizar o que acontece antes dela — autenticação, log, o próprio
express.json() — na lição de
middleware no Express. Se quiser rever o
desenho da API antes disso, o que é uma API REST
explica por que a tarefa mora em /tarefas/7 e não em /pegarTarefa?id=7. O
mapa inteiro está na trilha de Node e no
guia de Node.js.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Posso mandar corpo numa requisição GET?
Existe limite de tamanho para o corpo que o express.json() aceita?
Qual a diferença entre req.query e req.params quando os dois são opcionais?
Preciso de body-parser instalado à parte?
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
- Express 5 — API de Request — expressjs.com
- Express — Migrating to Express 5 — expressjs.com
- MDN — Content-Type — developer.mozilla.org



