Express com TypeScript: Middlewares essenciais e composição de pipeline

Capítulo 6

Tempo estimado de leitura: 11 minutos

+ Exercício

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

Separar por domínio (parsing, correlation, access, errors) ajuda a manter o pipeline legível e evita “middlewares genéricos” que fazem coisas demais.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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.body manualmente; 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/catch para garantir que erros sejam encaminhados com next(err).
  • O middleware não assume como autenticação funciona; ele recebe getAuth como 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/catch ou asyncHandler.
  • Não responda duas vezes: se chamou res.json/res.send, finalize com return.
  • O errorHandler deve 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:

  1. Correlação (requestId) — cedo, para que logs e erros tenham ID.
  2. Parsing (jsonBody) — antes de rotas que dependem de req.body.
  3. Autenticação (se existir) — para popular req.auth.
  4. Autorização (authorize) — por rota ou por grupo de rotas.
  5. Rotas — handlers finais.
  6. 404 (notFound) — depois das rotas.
  7. 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.body com 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; use unknown para details e faça narrowing quando necessário.
  • Se o middleware adiciona campos em req, declare em src/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

ItemVerificação
Ordem no pipelineDepende de algo em req? Garanta que o middleware anterior popula isso.
Erros assíncronosUsa try/catch ou asyncHandler para encaminhar com next(err).
Resposta únicaApós res.status().json(), sempre return.
Sem estado globalNenhuma variável compartilhada entre requisições.
TestabilidadeDependências injetadas via factory; sem imports diretos de serviços difíceis de mockar.

Agora responda o exercício sobre o conteúdo:

Ao compor um pipeline de middlewares no Express com TypeScript, qual prática ajuda a manter o código reutilizável, testável e com menos efeitos colaterais?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Factories permitem injetar regras/dependências e configurar comportamento sem acoplamento, facilitando testes e reutilização. Evitar estado global reduz vazamento entre requisições, e erros devem ser encaminhados com next(err) em vez de serem ignorados.

Próximo capitúlo

Validação de dados no Express com TypeScript: DTOs, schemas e segurança de entrada

Arrow Right Icon
Capa do Ebook gratuito Node.js Essencial: Construindo um Back-end com Express e TypeScript
38%

Node.js Essencial: Construindo um Back-end com Express e TypeScript

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.