Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori12 min de leitura

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.

bash
node -v
npm ls express
v24.16.0 biblioteca@ /private/tmp/biblioteca └── express@5.2.1

A 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.

cronometro express.json() exigirCarteirinha GET /livros next() next() next() a resposta sai da rota: a fila termina aqui req

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().

js
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:

bash
curl -s http://localhost:3321/livros
[{"id":1,"titulo":"Grande Sertão: Veredas","autor":"Guimarães Rosa"}]

E o terminal do servidor mostra a fila andando:

biblioteca ouvindo na porta 3321 1 — chegou: GET /livros 2 — ainda na fila 3 — a rota respondeu

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 é.

js
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:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client at ServerResponse.setHeader (node:_http_outgoing:647:11) at ServerResponse.header (/private/tmp/biblioteca/node_modules/express/lib/response.js:686:10) at ServerResponse.send (/private/tmp/biblioteca/node_modules/express/lib/response.js:163:12) at ServerResponse.json (/private/tmp/biblioteca/node_modules/express/lib/response.js:252:15) at file:///private/tmp/biblioteca/servidor.js:11:7 at Layer.handleRequest (/private/tmp/biblioteca/node_modules/router/lib/layer.js:152:17) at next (/private/tmp/biblioteca/node_modules/router/lib/route.js:157:13) at Route.dispatch (/private/tmp/biblioteca/node_modules/router/lib/route.js:117:3) at handle (/private/tmp/biblioteca/node_modules/router/index.js:435:11) at Layer.handleRequest (/private/tmp/biblioteca/node_modules/router/lib/layer.js:152:17)

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.

js
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:

bash
curl -sS --max-time 5 http://localhost:3322/livros
curl: (28) Operation timed out after 5011 milliseconds with 0 bytes received

E o servidor, olhando de dentro, acha que está tudo bem:

biblioteca ouvindo na porta 3322 middleware rodou

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.

js
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:

bash
curl -s http://localhost:3323/livros/42
curl -s http://localhost:3323/admin/relatorio
{"id":42,"titulo":"Vidas Secas","autor":"Graciliano Ramos"} {"emprestimosNoMes":137}

O log do servidor separa os três escopos:

biblioteca ouvindo na porta 3323 [global] GET /livros/42 [rota] tombo pedido: 42 [global] GET /admin/relatorio [grupo] conferindo o crachá do balcão

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.

js
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:

js
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:

GET /livros -> 200 em 5.7ms GET /livros -> 200 em 0.9ms GET /livros -> 200 em 0.2ms GET /livros/1 -> 200 em 43.2ms GET /livros/2 -> 200 em 41.0ms GET /livros/99 -> 404 em 41.2ms GET /dvds -> 404 em 0.8ms POST /emprestimos -> 201 em 6.2ms POST /emprestimos -> 201 em 0.3ms GET /livros -> 200 em 0.2ms

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.

js
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:

bash
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/501
{"erro":"carteirinha da biblioteca obrigatória"} -> 401 {"id":501,"livro":"Vidas Secas","retiradoPor":"BM-2048"} -> 200

O log do servidor mostra que, no caso barrado, a rota nem chegou a rodar:

biblioteca ouvindo na porta 3325 [porta] barrado em /emprestimos/501 [porta] liberado para BM-2048 [rota] montando o comprovante

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.

js
app.post('/emprestimos', (req, res) => {
  console.log('req.body =', req.body);
  res.status(201).json({ registrado: req.body });
});

app.use(express.json()); // tarde demais

O curl manda o JSON certinho, com o Content-Type certo:

bash
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:

req.body = undefined

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:

js
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 });
});
{"registrado":{"livroId":2,"leitor":"Marina"}}

E, no terminal do servidor, o corpo já vem como objeto JavaScript:

req.body = { livroId: 2, leitor: 'Marina' }

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.

js
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' });
});
bash
curl -s -w '\n-> %{http_code}\n' http://localhost:3328/livros/1
{"erro":"o acervo não respondeu agora"} -> 500

O handler de erro recebeu o problema, e o processo continuou no ar:

biblioteca ouvindo na porta 3328 [handler de erro] conexão com o banco do acervo recusada

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.

js
function carregarLivro(req, res, next) {
  setTimeout(() => {
    throw new Error('conexão com o banco do acervo caiu');
  }, 10);
}
file:///private/tmp/biblioteca/servidor.js:7 throw new Error('conexão com o banco do acervo caiu'); ^

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:

js
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' });
});
bash
curl -s -w '\n-> %{http_code}\n' http://localhost:3330/livros/1
{"erro":"o acervo não respondeu agora"} -> 503

Com 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:

bash
npm i express@4   # instalou a 4.22.2
node servidor.js
bash
curl -sS --max-time 5 http://localhost:3341/livros/1
curl: (52) Empty reply from server

O servidor não demorou: ele deixou de existir no meio da requisição.

biblioteca ouvindo na porta 3341 file:///private/tmp/biblioteca-4/servidor.js:6 return Promise.reject(new Error('conexão com o banco do acervo recusada')); ^

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.

Ver todos os vídeos do canal
  • express
  • middleware
  • next
  • logger
  • node

Perguntas frequentes

Middleware precisa sempre chamar next()?
Não. Ele precisa fazer exatamente uma das duas coisas: chamar next() para passar a bola, ou responder com res.json(), res.send() ou res.end(). O que não pode é não fazer nenhuma das duas — aí a requisição fica pendurada até o cliente desistir.
Qual a diferença entre app.use e passar a função na própria rota?
app.use registra para todas as requisições que baterem no caminho informado, inclusive as que não têm rota nenhuma. Passar a função como argumento de app.get só a executa naquela rota específica, e ali dentro req.params já está preenchido.
Como o Express sabe que uma função é handler de erro?
Pela quantidade de parâmetros declarados. Função com quatro parâmetros (erro, req, res, next) é tratada como handler de erro e fica fora do fluxo normal. Se você apagar o quarto parâmetro por achar que não usa, ela vira middleware comum e para de capturar erro.
Middleware roda também nas requisições que caem em 404?
Os globais, sim. Eles são registrados antes do roteamento, então rodam mesmo quando nenhuma rota casa com a URL — foi assim que o logger deste artigo registrou o GET /dvds com status 404.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. Express — Using middleware — expressjs.com
  2. Express — Migrating to Express 5 — expressjs.com
  3. Node.js — http.ServerResponse: evento finish — nodejs.org

Continue por aqui