O que é um middleware no Express (e por que ele é a base do pipeline)
No Express, um middleware é uma função que participa do processamento de uma requisição antes (ou depois) do handler final da rota. Ele pode: ler/modificar req e res, encerrar a resposta, ou delegar para o próximo passo chamando next(). A composição de vários middlewares forma um pipeline (cadeia ordenada) que deve ser pensado como uma sequência de responsabilidades pequenas e reutilizáveis.
Assinaturas principais:
- Middleware comum:
(req, res, next) => void - Middleware de erro:
(err, req, res, next) => void(tem 4 parâmetros)
Em TypeScript, vale tipar bem o que você adiciona ao req (ex.: correlação, usuário autenticado, permissões) para manter segurança e ergonomia.
Estrutura recomendada de pastas para middlewares
Uma organização simples e escalável:
src/ app.ts server.ts routes/ index.ts middlewares/ parsing/ jsonBody.ts correlation/ requestId.ts access/ authorize.ts errors/ notFound.ts errorHandler.ts types/ express.d.ts utils/ httpError.ts asyncHandler.ts __tests__/ middlewares/ requestId.test.ts authorize.test.ts errorHandler.test.tsSeparar por domínio (parsing, correlation, access, errors) ajuda a manter o pipeline legível e evita “middlewares genéricos” que fazem coisas demais.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Tipando extensões do Express (Request) no TypeScript
Quando um middleware adiciona campos em req, faça module augmentation para evitar any e manter autocomplete.
// src/types/express.d.tsimport 'express-serve-static-core';declare module 'express-serve-static-core' { interface Request { requestId?: string; auth?: { userId: string; roles: string[]; }; }}Garanta que esse arquivo seja incluído no tsconfig.json (via include ou typeRoots), conforme sua configuração do projeto.
Middleware de parsing (JSON) e validação de Content-Type
O Express já oferece express.json(), mas em projetos reais é comum encapsular parsing + regras (limite, content-type, etc.) em um middleware reutilizável.
Factory middleware: JSON parser com opções
Use factory pattern quando o middleware precisa de dependências/configurações (ex.: limite de payload, lista de content-types aceitos).
// src/middlewares/parsing/jsonBody.tsimport express, { RequestHandler } from 'express';type JsonBodyOptions = { limit?: string; requireJsonContentType?: boolean;};export function jsonBody(options: JsonBodyOptions = {}): RequestHandler { const { limit = '1mb', requireJsonContentType = true } = options; const parser = express.json({ limit }); return (req, res, next) => { if (requireJsonContentType) { const hasBody = req.headers['content-length'] !== '0' && req.headers['content-length'] !== undefined; const contentType = req.headers['content-type'] ?? ''; const isJson = contentType.includes('application/json'); if (hasBody && !isJson) { return res.status(415).json({ error: 'Unsupported Media Type. Use application/json.' }); } } return parser(req, res, next); };}Boas práticas:
- Evite mutar
req.bodymanualmente; deixe o parser fazer isso. - Não capture exceções e “engula” erros; se algo falhar, responda adequadamente ou chame
next(err). - Se você retornar resposta dentro do middleware, não chame
next()depois.
Correlação de request: requestId e propagação
Correlação permite rastrear uma requisição ponta a ponta em logs e integrações. Um padrão comum é aceitar um header de entrada (ex.: X-Request-Id) e, se ausente, gerar um novo. Depois, devolver o mesmo valor na resposta.
Middleware síncrono: requestId
// src/middlewares/correlation/requestId.tsimport { RequestHandler } from 'express';import { randomUUID } from 'crypto';type RequestIdOptions = { headerName?: string; generator?: () => string;};export function requestId(options: RequestIdOptions = {}): RequestHandler { const headerName = (options.headerName ?? 'x-request-id').toLowerCase(); const generator = options.generator ?? (() => randomUUID()); return (req, res, next) => { const incoming = req.headers[headerName]; const id = (Array.isArray(incoming) ? incoming[0] : incoming) || generator(); req.requestId = id; res.setHeader(headerName, id); next(); };}Evite efeitos colaterais:
- Não use variáveis globais para armazenar o requestId (isso vaza entre requisições).
- Se precisar usar o requestId em logs, prefira passar explicitamente (ex.:
logger.info({ requestId: req.requestId }, ...)) ou usar uma solução de contexto (fora do escopo aqui).
Controle de acesso por regra (authorize) com factory e dependências
Um middleware de autorização geralmente depende de uma regra (função) e, às vezes, de serviços (ex.: carregador de permissões). O ideal é modelar isso como factory para manter reutilização e testabilidade.
Modelando regra de autorização
// src/middlewares/access/authorize.tsimport { RequestHandler } from 'express';type AuthContext = { userId: string; roles: string[];};type Rule = (ctx: AuthContext, req: Express.Request) => boolean | Promise<boolean>;type AuthorizeDeps = { getAuth: (req: Express.Request) => AuthContext | null;};export function authorize(rule: Rule, deps: AuthorizeDeps): RequestHandler { return async (req, res, next) => { try { const ctx = deps.getAuth(req); if (!ctx) return res.status(401).json({ error: 'Unauthorized' }); const allowed = await rule(ctx, req); if (!allowed) return res.status(403).json({ error: 'Forbidden' }); req.auth = ctx; return next(); } catch (err) { return next(err); } };}Repare em pontos importantes:
- Middleware é assíncrono e usa
try/catchpara garantir que erros sejam encaminhados comnext(err). - O middleware não assume como autenticação funciona; ele recebe
getAuthcomo dependência (facilita testes). - A regra pode ser síncrona ou assíncrona.
Exemplos de regras reutilizáveis
// src/middlewares/access/rules.tsimport type { Request } from 'express';type AuthContext = { userId: string; roles: string[] };export const hasRole = (role: string) => (ctx: AuthContext) => ctx.roles.includes(role);export const isOwnerFromParam = (paramName: string) => (ctx: AuthContext, req: Request) => { const ownerId = req.params[paramName]; return ownerId === ctx.userId;};export const anyOf = (...rules: Array<(ctx: AuthContext, req: Request) => boolean | Promise<boolean>>) => async (ctx: AuthContext, req: Request) => { for (const r of rules) if (await r(ctx, req)) return true; return false;};Uso em rotas (composição)
// src/routes/index.tsimport { Router } from 'express';import { authorize } from '../middlewares/access/authorize';import { hasRole, isOwnerFromParam, anyOf } from '../middlewares/access/rules';const router = Router();const getAuth = (req: Express.Request) => req.auth ?? null; // exemplo: em projetos reais, auth viria de um middleware de autenticaçãorouter.get( '/users/:id', authorize(anyOf(hasRole('admin'), isOwnerFromParam('id')), { getAuth }), (req, res) => { res.json({ userId: req.params.id, requestId: req.requestId }); });export default router;Observação: em um projeto completo, normalmente existe um middleware anterior que popula req.auth (autenticação). Aqui focamos na autorização por regra e na composição.
Tratamento de erros: propagação correta em middlewares síncronos e assíncronos
Erros em middlewares síncronos podem ser lançados (throw) e o Express tende a capturar. Já em middlewares assíncronos (async), versões antigas do Express não capturam automaticamente rejeições; por isso, é uma boa prática padronizar um wrapper para encaminhar erros com next.
Wrapper asyncHandler (padronização)
// src/utils/asyncHandler.tsimport type { Request, Response, NextFunction, RequestHandler } from 'express';export const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>): RequestHandler => { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };};Uso:
import { asyncHandler } from '../utils/asyncHandler';router.get('/health', asyncHandler(async (req, res) => { // se lançar erro aqui, vai para o errorHandler res.json({ ok: true, requestId: req.requestId });}));Padronizando erros HTTP (opcional, mas útil)
// src/utils/httpError.tsexport class HttpError extends Error { constructor( public status: number, message: string, public details?: unknown ) { super(message); }}Middleware 404 (notFound)
// src/middlewares/errors/notFound.tsimport type { RequestHandler } from 'express';export const notFound: RequestHandler = (req, res) => { res.status(404).json({ error: 'Not Found', path: req.path, requestId: req.requestId });};Middleware de erro (errorHandler) com TypeScript
// src/middlewares/errors/errorHandler.tsimport type { ErrorRequestHandler } from 'express';import { HttpError } from '../../utils/httpError';export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { if (res.headersSent) return next(err); const requestId = req.requestId; if (err instanceof HttpError) { return res.status(err.status).json({ error: err.message, details: err.details, requestId, }); } return res.status(500).json({ error: 'Internal Server Error', requestId, });};Regras de ouro:
- Se você está em middleware assíncrono, use
try/catchouasyncHandler. - Não responda duas vezes: se chamou
res.json/res.send, finalize comreturn. - O
errorHandlerdeve ser o último middleware registrado.
Ordenação correta do pipeline (e por quê)
A ordem define o que estará disponível em req e quais garantias cada etapa pode assumir. Um pipeline típico:
- Correlação (requestId) — cedo, para que logs e erros tenham ID.
- Parsing (jsonBody) — antes de rotas que dependem de
req.body. - Autenticação (se existir) — para popular
req.auth. - Autorização (authorize) — por rota ou por grupo de rotas.
- Rotas — handlers finais.
- 404 (notFound) — depois das rotas.
- Error handler — por último.
Exemplo de app.ts com pipeline bem ordenado
// src/app.tsimport express from 'express';import router from './routes';import { requestId } from './middlewares/correlation/requestId';import { jsonBody } from './middlewares/parsing/jsonBody';import { notFound } from './middlewares/errors/notFound';import { errorHandler } from './middlewares/errors/errorHandler';export const app = express();app.use(requestId({ headerName: 'x-request-id' }));app.use(jsonBody({ limit: '1mb', requireJsonContentType: true }));app.use('/api', router);app.use(notFound);app.use(errorHandler);Como evitar efeitos colaterais em middlewares reutilizáveis
- Sem estado global: não armazene dados de request em variáveis fora do escopo do middleware.
- Mutação mínima e explícita: se precisar anexar algo ao
req, faça isso em um único campo bem nomeado (ex.:req.requestId,req.auth). - Não sobrescreva campos existentes sem necessidade (ex.: não reescreva
req.bodycom outro formato). - Factories para dependências: injete serviços, regras e configurações via parâmetros; isso evita acoplamento e facilita testes.
- Idempotência: sempre que possível, o middleware deve ser seguro se aplicado duas vezes (ou falhar de forma previsível).
Exemplos: middleware síncrono vs assíncrono (com erro)
Síncrono: validação simples com throw
// src/middlewares/access/requireHeader.tsimport type { RequestHandler } from 'express';import { HttpError } from '../../utils/httpError';export const requireHeader = (name: string): RequestHandler => { const headerName = name.toLowerCase(); return (req, _res, next) => { const value = req.headers[headerName]; if (!value) throw new HttpError(400, `Missing header: ${name}`); next(); };};Mesmo sendo síncrono, prefira lançar HttpError para padronizar a resposta no errorHandler.
Assíncrono: checagem em serviço com next(err)
// src/middlewares/access/requireApiKey.tsimport type { RequestHandler } from 'express';import { HttpError } from '../../utils/httpError';type Deps = { isValidKey: (key: string) => Promise<boolean>;};export const requireApiKey = (deps: Deps): RequestHandler => { return async (req, _res, next) => { try { const key = String(req.headers['x-api-key'] ?? ''); if (!key) return next(new HttpError(401, 'Missing API key')); const ok = await deps.isValidKey(key); if (!ok) return next(new HttpError(403, 'Invalid API key')); return next(); } catch (err) { return next(err); } };};Note o padrão: return next(...) para evitar continuar fluxo após um erro.
Guia de padronização: nomes, arquivos e testes de middlewares
Nomenclatura
- Arquivos:
camelCase.ts(ex.:requestId.ts,errorHandler.ts). - Factories: verbo/ação (ex.:
authorize,requireApiKey,jsonBody). - Middlewares diretos (sem factory): substantivo claro (ex.:
notFound,errorHandler). - Regras: predicados (ex.:
hasRole,isOwnerFromParam).
Assinaturas e tipos
- Exporte sempre tipos de opções/deps quando forem parte do contrato público.
- Evite
any; useunknownparadetailse faça narrowing quando necessário. - Se o middleware adiciona campos em
req, declare emsrc/types/express.d.ts.
Testes (foco em comportamento)
Teste middlewares como unidades: dado um req/res fake, verifique se chama next, se define headers/campos e se responde com status correto. Para isso, crie helpers simples de mock.
// src/__tests__/helpers/httpMocks.tsimport type { Request, Response, NextFunction } from 'express';export function mockReq(partial: Partial<Request> = {}): Request { return partial as Request;}export function mockRes() { const headers: Record<string, string> = {}; const res = { statusCode: 200, headersSent: false, setHeader: (k: string, v: string) => { headers[k.toLowerCase()] = v; }, status(code: number) { this.statusCode = code; return this; }, json(_body: unknown) { this.headersSent = true; return this; }, } as unknown as Response; return { res, headers };}export function mockNext() { const next: NextFunction = ((err?: unknown) => err) as NextFunction; return next;}Exemplo de teste: requestId
// src/__tests__/middlewares/requestId.test.tsimport { requestId } from '../../middlewares/correlation/requestId';import { mockReq, mockRes, mockNext } from '../helpers/httpMocks';test('requestId should set req.requestId and response header', () => { const mw = requestId({ generator: () => 'fixed-id' }); const req = mockReq({ headers: {} }); const { res, headers } = mockRes(); const next = mockNext(); mw(req, res, next); expect(req.requestId).toBe('fixed-id'); expect(headers['x-request-id']).toBe('fixed-id');});Exemplo de teste: authorize (nega acesso)
// src/__tests__/middlewares/authorize.test.tsimport { authorize } from '../../middlewares/access/authorize';import { mockReq, mockRes, mockNext } from '../helpers/httpMocks';test('authorize should return 401 when no auth context', async () => { const mw = authorize(() => true, { getAuth: () => null }); const req = mockReq({ headers: {} }); const { res } = mockRes(); const next = mockNext(); await mw(req, res, next); expect((res as any).statusCode).toBe(401);});Checklist de qualidade para middlewares
| Item | Verificação |
|---|---|
| Ordem no pipeline | Depende de algo em req? Garanta que o middleware anterior popula isso. |
| Erros assíncronos | Usa try/catch ou asyncHandler para encaminhar com next(err). |
| Resposta única | Após res.status().json(), sempre return. |
| Sem estado global | Nenhuma variável compartilhada entre requisições. |
| Testabilidade | Dependências injetadas via factory; sem imports diretos de serviços difíceis de mockar. |