Middleware no Express: next(), ordem e escopo de rota
O que é middleware, o que acontece quando você esquece o next(), como aplicar só em um grupo de rotas e como escrever um logger que mede o tempo real.
Middleware é uma função que o Express chama antes da sua rota, com a
requisição na mão. Ela pode ler, enriquecer, medir, barrar ou deixar passar — e
só passa a bola adiante quando chama next(). Esquecer essa chamada é o que
mais trava API de quem está começando.
Todos os exemplos deste artigo rodam na API de uma biblioteca municipal:
/livros é o acervo, /emprestimos é o balcão e /admin é a sala dos
funcionários. O ponto de partida é o servidor da lição
Express do zero.
node -v
npm ls expressA fila de balcões: o modelo mental do middleware
Pense na requisição como uma pessoa entrando numa biblioteca. Antes de chegar ao balcão de empréstimos, ela passa pela identificação, pela conferência do cartão e, talvez, por um funcionário que anota quanto tempo o atendimento levou. Cada balcão faz uma parte do trabalho e decide se a pessoa continua ou para ali.
No Express, cada balcão é um middleware. A ordem em que você registra as
funções vira a ordem da fila; req é a ficha que segue viagem, res é a
resposta que pode encerrar o atendimento e next() chama o próximo balcão. Para
ver isso sem decorar, acompanhe no primeiro exemplo a sequência dos logs: ela é
a fila real sendo percorrida.
O Express guarda uma lista ordenada de funções. Quando uma requisição chega, ela entra no começo dessa lista e desce, uma função por vez. Cada função decide se continua ou se para ali respondendo.
A rota é só o último item da fila. Ela não tem nada de especial: é uma função
que costuma responder em vez de chamar next().
import express from 'express';
const app = express();
app.use((req, res, next) => {
console.log('1 — chegou:', req.method, req.url);
next();
});
app.use((req, res, next) => {
console.log('2 — ainda na fila');
next();
});
app.get('/livros', (req, res) => {
console.log('3 — a rota respondeu');
res.json([{ id: 1, titulo: 'Grande Sertão: Veredas', autor: 'Guimarães Rosa' }]);
});
app.listen(3321, () => console.log('biblioteca ouvindo na porta 3321'));Com o servidor no ar, um curl do outro lado:
curl -s http://localhost:3321/livrosE o terminal do servidor mostra a fila andando:
Três funções para uma requisição. Nenhuma delas sabe da existência das outras — o que as encadeia é a ordem de registro, e nada mais.
A assinatura (req, res, next) e o papel de cada argumento
Todo middleware recebe os mesmos três argumentos, sempre nessa ordem.
| argumento | o que é | uso típico |
|---|---|---|
req |
a requisição que chegou | ler cabeçalho, corpo e parâmetro; pendurar dados para a rota usar |
res |
a resposta que vai sair | responder e encerrar a fila ali mesmo |
next |
a função que passa a bola | next() segue para o próximo; next(erro) pula para o handler de erro |
O req é o mesmo objeto do começo ao fim da requisição. Isso é o que permite um
middleware preparar alguma coisa e a rota simplesmente encontrar pronto — é o
mesmo req de req.params, req.query e req.body.
A regra que resume tudo: ou você responde, ou você chama next(). Fazer as
duas coisas parece inofensivo e não é.
app.use((req, res, next) => {
res.json({ aviso: 'biblioteca em manutenção' });
next(); // respondeu E mandou a fila continuar
});
app.get('/livros', (req, res) => {
res.json([{ id: 1, titulo: 'Grande Sertão: Veredas' }]);
});O cliente recebe a primeira resposta, e o servidor explode ao tentar mandar a segunda:
O diagnóstico completo desse caso está em
Cannot set headers after they are sent to the client.
Para o middleware, a lição é curta: quando responder, escreva return res.json(...).
O return existe justamente para garantir que nada mais rode depois.
Esquecer o next(): a requisição que fica pendurada
Este é o erro que assusta porque não gera erro. Nada estoura, nada aparece no console, o servidor continua no ar. A requisição simplesmente nunca volta.
app.use((req, res, next) => {
console.log('middleware rodou');
// esqueci o next()
});
app.get('/livros', (req, res) => {
console.log('esta linha nunca é impressa');
res.json([{ id: 1, titulo: 'Grande Sertão: Veredas' }]);
});Do lado do cliente, o curl fica esperando até o limite que você der a ele:
curl -sS --max-time 5 http://localhost:3322/livrosE o servidor, olhando de dentro, acha que está tudo bem:
Repare que a rota nunca imprimiu nada. O next() faltando não pulou a rota: ele
parou a fila antes dela.
Global, por grupo e por rota: três escopos de registro
O mesmo middleware pode valer para a API inteira, para um pedaço dela ou para uma única rota. Muda só onde você registra.
function registrarAcesso(req, res, next) {
console.log('[global]', req.method, req.url);
next();
}
function exigirCracha(req, res, next) {
console.log('[grupo] conferindo o crachá do balcão');
next();
}
function conferirTombo(req, res, next) {
console.log('[rota] tombo pedido:', req.params.id);
next();
}
app.use(registrarAcesso); // global: toda requisição
app.use('/admin', exigirCracha); // grupo: só o que começa com /admin
app.get('/livros/:id', conferirTombo, (req, res) => {
res.json({ id: Number(req.params.id), titulo: 'Vidas Secas', autor: 'Graciliano Ramos' });
});
app.get('/admin/relatorio', (req, res) => {
res.json({ emprestimosNoMes: 137 });
});Duas requisições, para ver quem roda em cada caso:
curl -s http://localhost:3323/livros/42
curl -s http://localhost:3323/admin/relatorioO log do servidor separa os três escopos:
Leia com calma: o registrarAcesso rodou nas duas. O exigirCracha rodou só na
segunda, porque a URL começa com /admin. E o conferirTombo rodou só na
primeira, porque foi registrado ali dentro — e é justamente por estar registrado
na rota que ele enxerga req.params.id já preenchido. Um middleware global não
enxerga: naquele momento o Express ainda nem escolheu a rota.
| forma | alcance | quando usar |
|---|---|---|
app.use(fn) |
toda requisição, inclusive as que dão 404 | log, CORS, corpo em JSON |
app.use('/admin', fn) |
tudo que começa com aquele caminho | autenticação de uma área inteira |
app.get('/rota', fn, handler) |
só aquela rota | validação de um recurso específico |
Um logger que mede o tempo de cada resposta
Middleware pronto resolve muita coisa, mas escrever um do zero é o que faz o conceito assentar. O logger é o melhor primeiro exercício, porque ele precisa agir em dois momentos: quando a requisição entra e quando a resposta sai.
O truque é o evento finish do objeto res. Ele dispara quando a resposta foi
inteiramente entregue ao sistema operacional — e só aí o res.statusCode final
é confiável.
function cronometro(req, res, next) {
const inicio = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - inicio) / 1e6;
console.log(
`${req.method} ${req.originalUrl} -> ${res.statusCode} em ${ms.toFixed(1)}ms`
);
});
next();
}process.hrtime.bigint() devolve nanossegundos em BigInt, que não sofre com
arredondamento de ponto flutuante. A divisão por 1e6 converte para
milissegundos. Registrado antes de tudo, ele mede a fila inteira:
app.use(cronometro);
app.use(express.json());
const acervo = [
{ id: 1, titulo: 'Grande Sertão: Veredas', autor: 'Guimarães Rosa' },
{ id: 2, titulo: 'Vidas Secas', autor: 'Graciliano Ramos' },
];
app.get('/livros', (req, res) => {
res.json(acervo);
});
app.get('/livros/:id', async (req, res) => {
await new Promise((r) => setTimeout(r, 40)); // consulta no banco do acervo
const livro = acervo.find((l) => l.id === Number(req.params.id));
if (!livro) return res.status(404).json({ erro: 'livro fora do acervo' });
res.json(livro);
});
app.post('/emprestimos', (req, res) => {
res.status(201).json({ id: 501, ...req.body });
});Dez requisições depois, num MacBook com Node 24.16.0 e Express 5.2.1, o log real é este:
Essas dez linhas contam três histórias.
A primeira: a primeira requisição sempre custa mais. 5,7 ms contra 0,2 ms nas
seguintes. Não é a sua rota que está lenta — é o Node compilando e aquecendo o
caminho na primeira passagem. O mesmo salto reaparece no primeiro POST
(6,2 ms contra 0,3 ms no segundo) — e ali o suspeito é o express.json(), que
só entra em ação quando aparece a primeira requisição com corpo. Quando for
medir desempenho, descarte sempre a primeira execução de cada caminho.
A segunda: os 40 ms do banco aparecem inteiros. As rotas com /livros/:id
ficaram em torno de 42 ms, e a de id inexistente também. Um 404 que custa 41 ms
é uma pista valiosa: o tempo foi gasto antes de descobrir que não existia.
A terceira: GET /dvds foi registrado mesmo sem rota nenhuma. O log saiu
porque o cronometro é global e roda antes do roteamento. Um 404 barato assim,
em massa, geralmente é bot ou front-end pedindo um caminho que você renomeou.
Middleware que decide: responder agora ou seguir a fila
Um logger só observa. O middleware fica realmente poderoso quando ele decide: verifica alguma coisa e, dependendo do resultado, responde na hora ou libera a passagem. É assim que autenticação funciona.
function exigirCarteirinha(req, res, next) {
const carteirinha = req.get('x-carteirinha');
if (!carteirinha) {
console.log('[porta] barrado em', req.originalUrl);
return res.status(401).json({ erro: 'carteirinha da biblioteca obrigatória' });
}
req.leitor = { carteirinha, unidade: 'centro' };
console.log('[porta] liberado para', carteirinha);
next();
}
app.get('/emprestimos/:id', exigirCarteirinha, (req, res) => {
console.log('[rota] montando o comprovante');
res.json({
id: Number(req.params.id),
livro: 'Vidas Secas',
retiradoPor: req.leitor.carteirinha,
});
});Duas chamadas, uma sem o cabeçalho e outra com ele. O -w '%{http_code}' faz o
curl imprimir o status logo depois do corpo:
curl -s -w '\n-> %{http_code}\n' http://localhost:3325/emprestimos/501
curl -s -w '\n-> %{http_code}\n' -H 'x-carteirinha: BM-2048' http://localhost:3325/emprestimos/501O log do servidor mostra que, no caso barrado, a rota nem chegou a rodar:
Duas coisas para guardar. A primeira é o return antes do res.status(401):
sem ele, o código continuaria e chamaria next() logo abaixo. A segunda é a
linha req.leitor = { ... } — o middleware pendurou o leitor no req, e a rota
achou pronto. Trocar o cabeçalho por um token assinado é exatamente o que a
lição de autenticação com JWT faz; a
estrutura do middleware é esta mesma.
Ordem de registro é ordem de execução — o bug do req.body vazio
Middleware não tem prioridade, não tem peso, não tem configuração de ordem. Vale
a linha em que você escreveu. E o sintoma mais comum disso é o req.body chegar
vazio num POST que você jura estar mandando JSON.
app.post('/emprestimos', (req, res) => {
console.log('req.body =', req.body);
res.status(201).json({ registrado: req.body });
});
app.use(express.json()); // tarde demaisO curl manda o JSON certinho, com o Content-Type certo:
curl -s -X POST http://localhost:3326/emprestimos \
-H 'Content-Type: application/json' \
-d '{"livroId":2,"leitor":"Marina"}'E o servidor confirma que não recebeu nada:
Nenhum erro, nenhum aviso. O express.json() está registrado — só que depois
da rota, e a requisição já tinha respondido antes de chegar nele. Sobe uma linha
e o mesmo curl funciona:
app.use(express.json()); // antes de qualquer rota
app.post('/emprestimos', (req, res) => {
console.log('req.body =', req.body);
res.status(201).json({ registrado: req.body });
});E, no terminal do servidor, o corpo já vem como objeto JavaScript:
A mesma armadilha derruba CORS (“por que meu front continua bloqueado?”) e
autenticação (“por que a rota pública pede token?”). Sempre a mesma causa: a
linha está no lugar errado. A ordem que funciona na prática é registrar primeiro
o que observa (logger), depois o que interpreta a requisição
(CORS, express.json()), depois o que autoriza, e só
então as rotas.
Middleware assíncrono: o que o Express 5 captura e o que não
Aqui mora uma diferença que virou o Express do avesso. No Express 4, uma
Promise rejeitada dentro de middleware async escapava do Express inteiro: ele
chamava a função, ignorava a promessa devolvida e seguia em frente. Todo mundo
instalava pacote ou embrulhava cada rota em try/catch. No Express 5, a
rejeição é capturada e vira next(erro) automaticamente.
function buscarNoAcervo() {
return Promise.reject(new Error('conexão com o banco do acervo recusada'));
}
async function carregarLivro(req, res, next) {
req.livro = await buscarNoAcervo(req.params.id);
next();
}
app.get('/livros/:id', carregarLivro, (req, res) => {
res.json(req.livro);
});
app.use((erro, req, res, next) => {
console.log('[handler de erro]', erro.message);
res.status(500).json({ erro: 'o acervo não respondeu agora' });
});curl -s -w '\n-> %{http_code}\n' http://localhost:3328/livros/1O handler de erro recebeu o problema, e o processo continuou no ar:
Aquele último app.use com quatro parâmetros não é um middleware comum: o
Express reconhece o handler de erro pela quantidade de parâmetros declarados.
Apagar o next final por achar que não usa faz a função virar middleware normal
e parar de capturar erro.
Agora o caso que o Express 5 não resolve: erro lançado dentro de um callback
que ninguém está esperando, como o de um setTimeout.
function carregarLivro(req, res, next) {
setTimeout(() => {
throw new Error('conexão com o banco do acervo caiu');
}, 10);
}Error: conexão com o banco do acervo caiu at Timeout._onTimeout (file:///private/tmp/biblioteca/servidor.js:7:11) at listOnTimeout (node:internal/timers:605:17) at process.processTimers (node:internal/timers:541:7)
Node.js v24.16.0
Não é um 500: é o processo inteiro morrendo. O curl do outro lado recebe
curl: (52) Empty reply from server, e todos os outros usuários da API caem
junto. O throw aconteceu numa volta futura do event loop, quando a fila do
Express já tinha ido embora — não existe ninguém ali para capturar.
A correção não é try/catch em volta do setTimeout, que não pegaria nada. É
transformar o callback em Promise e esperar por ela:
function buscarNoAcervo() {
return new Promise((_, rejeitar) =>
setTimeout(() => rejeitar(new Error('conexão com o banco do acervo caiu')), 10)
);
}
async function carregarLivro(req, res, next) {
req.livro = await buscarNoAcervo(req.params.id);
next();
}
app.use((erro, req, res, next) => {
console.log('[handler de erro]', erro.message);
// 503, e não 500: quem falhou foi o banco, não o pedido que chegou
res.status(503).json({ erro: 'o acervo não respondeu agora' });
});curl -s -w '\n-> %{http_code}\n' http://localhost:3330/livros/1Com o await no meio, a rejeição volta a ser um problema do Express — e o
servidor continua vivo para a próxima pessoa. Nesta versão o handler de erro
devolve 503 em vez de 500, porque a falha é do banco e não do pedido que
chegou — o mesmo middleware, um status mais honesto.
Se você mantém um projeto em Express 4, confira o que ele faz hoje antes de
confiar no que os tutoriais antigos dizem. Num diretório à parte, com o mesmo
carregarLivro da primeira rejeição desta seção — aquele que no Express 5
devolveu 500 — e só a dependência trocada:
npm i express@4 # instalou a 4.22.2
node servidor.jscurl -sS --max-time 5 http://localhost:3341/livros/1O servidor não demorou: ele deixou de existir no meio da requisição.
Error: conexão com o banco do acervo recusada at buscarNoAcervo (file:///private/tmp/biblioteca-4/servidor.js:6:25) at carregarLivro (file:///private/tmp/biblioteca-4/servidor.js:10:21) at Layer.handle [as handle_request] (/private/tmp/biblioteca-4/node_modules/express/lib/router/layer.js:95:5) at next (/private/tmp/biblioteca-4/node_modules/express/lib/router/route.js:149:13) at Route.dispatch (/private/tmp/biblioteca-4/node_modules/express/lib/router/route.js:119:3) at Layer.handle [as handle_request] (/private/tmp/biblioteca-4/node_modules/express/lib/router/layer.js:95:5) at /private/tmp/biblioteca-4/node_modules/express/lib/router/index.js:284:15 at param (/private/tmp/biblioteca-4/node_modules/express/lib/router/index.js:365:14) at param (/private/tmp/biblioteca-4/node_modules/express/lib/router/index.js:376:14) at router.process_params (/private/tmp/biblioteca-4/node_modules/express/lib/router/index.js:421:3)
Node.js v24.16.0
Não é a requisição pendurada que a maioria dos textos descreve. Como o Express 4
não olha para a Promise devolvida, ninguém trata a rejeição, e ela chega ao
Node como unhandled rejection — que desde o Node 15 derruba o processo
inteiro, exatamente como o throw dentro do setTimeout ali em cima. A
requisição pendurada era o comportamento do Node 14 para trás, quando rejeição
sem dono rendia só um aviso no terminal. Em Express 4 sobre Node moderno,
try/catch com next(erro) em cada middleware assíncrono não é capricho de
estilo: é o que mantém a API de pé.
O que vem depois
Você já sabe escrever middleware, escolher o escopo dele e reconhecer os dois
sintomas clássicos — requisição pendurada e req.body vazio. O próximo passo é
parar de empilhar tudo num arquivo só: o
express.Router separa a API por recurso e
deixa cada grupo com a sua própria fila de middlewares. Depois dele, junte todos
os try/catch num
handler de erro centralizado. A ordem
das lições até lá está no índice da trilha de Node.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Middleware precisa sempre chamar next()?
Qual a diferença entre app.use e passar a função na própria rota?
Como o Express sabe que uma função é handler de erro?
Middleware roda também nas requisições que caem em 404?
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 — Using middleware — expressjs.com
- Express — Migrating to Express 5 — expressjs.com
- Node.js — http.ServerResponse: evento finish — nodejs.org



