Voltar para Artigos
Back-end12 min de leitura

Arquitetura de Rotas: Dominando Middlewares no Express

O pipeline de middlewares do Express: autenticação JWT, autorização por role, validação com Zod, rate limiting por rota e request logging com AsyncLocalStorage para correlação por requestId.

12 de agosto de 2026

O Express processa uma requisição HTTP através de um pipeline sequencial de middlewares: cada função recebe (req, res, next), faz seu trabalho e chama next() para passar ao próximo middleware — ou next(error) para pular direto ao error handler. Essa arquitetura é o que permite separar autenticação, autorização, validação e logging em camadas independentes e testáveis.

A grande vantagem dessa abordagem sobre colocar toda a lógica dentro do Controller é a composição: você declara exatamente quais middlewares uma rota precisa e na ordem correta, e cada camada tem uma única responsabilidade. Um Controller que recebe uma requisição já autenticada, já autorizada e já validada pode se concentrar exclusivamente em orquestrar o Use Case — sem condicionais de segurança misturadas com lógica de negócio.

Middleware de Autenticação JWT

O middleware de autenticação tem uma única responsabilidade: verificar o token JWT no header Authorization, decodificar o payload e injetar os dados do usuário no objeto req. Ele nunca deveria fazer uma consulta ao banco de dados — o JWT já contém as informações necessárias. Para que o TypeScript saiba que req.user existe nos middlewares seguintes, usamos module augmentation para estender a interface Request do Express com o campo user.

authenticate.ts
import type { Request, Response, NextFunction } from 'express';
import { verify, type JwtPayload } from 'jsonwebtoken';
import { AppError } from '../errors/AppError';

// Extensão do tipo Request do Express para incluir o usuário autenticado
declare global {
  namespace Express {
    interface Request {
      user: {
        id: string;
        email: string;
        role: 'USER' | 'MANAGER' | 'ADMIN';
      };
    }
  }
}

export function authenticate(
  req: Request,
  _res: Response,
  next: NextFunction
): void {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    throw new AppError('Token de autenticação não fornecido.', 401);
  }

  const token = authHeader.slice(7); // Remove 'Bearer '

  try {
    const payload = verify(token, process.env.JWT_SECRET as string) as JwtPayload;

    // Injeta os dados do usuário na requisição
    // O Controller não precisa decodificar o token novamente
    req.user = {
      id: payload.sub as string,
      email: payload.email as string,
      role: payload.role as 'USER' | 'MANAGER' | 'ADMIN',
    };

    next();
  } catch (err) {
    // jwt.verify lança TokenExpiredError, JsonWebTokenError, NotBeforeError
    const message = err instanceof Error && err.name === 'TokenExpiredError'
      ? 'Token expirado. Faça login novamente.'
      : 'Token inválido.';

    throw new AppError(message, 401);
  }
}

Middleware de Autorização por Role

Autenticação e autorização são conceitos distintos e devem viver em middlewares separados. Autenticação responde 'quem é você?'. Autorização responde 'você tem permissão para fazer isso?'. O authorize usa o padrão factory function: ele recebe os roles permitidos e retorna um middleware — isso permite declarar a restrição diretamente na rota de forma legível e reutilizável. O middleware pressupõe que o authenticate já rodou e injetou req.user.

authorize.ts
import type { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/AppError';

type Role = 'USER' | 'MANAGER' | 'ADMIN';

// Factory: retorna um middleware para os roles permitidos
export function authorize(...allowedRoles: Array<Role>) {
  return function (req: Request, _res: Response, next: NextFunction): void {
    // Deve ser usado APÓS o middleware authenticate
    if (!allowedRoles.includes(req.user.role)) {
      throw new AppError(
        `Acesso negado. Requer role: ${allowedRoles.join(' ou ')}.`,
        403
      );
    }
    next();
  };
}

Middleware de Validação com Zod

Validar o body da requisição no middleware antes de chegar ao Controller elimina a necessidade de checar if (!req.body.email) dentro do Use Case. O Zod faz a validação e ao mesmo tempo transforma os dados: strings numéricas viram números (z.coerce.number()), campos opcionais ganham valores padrão. Após o validate, o req.body já está no formato exato que o Use Case espera — tipado e sanitizado.

validate.ts
import type { Request, Response, NextFunction } from 'express';
import { type ZodSchema, ZodError } from 'zod';

interface ValidateTargets {
  body?: ZodSchema;
  query?: ZodSchema;
  params?: ZodSchema;
}

// Valida body, query e params com schemas Zod individuais
export function validate(schemas: ValidateTargets) {
  return function (req: Request, _res: Response, next: NextFunction): void {
    try {
      if (schemas.body) {
        req.body = schemas.body.parse(req.body);
      }
      if (schemas.query) {
        req.query = schemas.query.parse(req.query) as typeof req.query;
      }
      if (schemas.params) {
        req.params = schemas.params.parse(req.params) as typeof req.params;
      }
      next();
    } catch (err) {
      if (err instanceof ZodError) {
        // Formata os erros do Zod de forma legível
        const errors = err.errors.map((e) => ({
          field: e.path.join('.'),
          message: e.message,
        }));
        _res.status(422).json({ error: 'Dados inválidos.', details: errors });
        return;
      }
      next(err);
    }
  };
}

Encadeamento nas Rotas

O encadeamento de middlewares por rota torna a declaração do arquivo de rotas auto-documentada: lendo apenas as linhas do router.get('/'), você sabe exatamente quais camadas de segurança e validação aquele endpoint possui. Cada rota tem exatamente os middlewares que precisa — sem herança implícita de um middleware global que protege mais do que deveria.

users.routes.ts
import { Router } from 'express';
import { z } from 'zod';
import { authenticate } from '../middlewares/authenticate';
import { authorize } from '../middlewares/authorize';
import { validate } from '../middlewares/validate';
import { loginRateLimiter, createRateLimitMiddleware } from '../infra/rateLimiting';
import { UsersController } from '../controllers/UsersController';

const router = Router();
const controller = new UsersController();

const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  password: z.string().min(8),
});

const ListUsersQuerySchema = z.object({
  page: z.coerce.number().default(1),
  pageSize: z.coerce.number().max(100).default(20),
  search: z.string().optional(),
});

const UserIdParamSchema = z.object({
  id: z.string().uuid('ID inválido.'),
});

// Público
router.post(
  '/',
  validate({ body: CreateUserSchema }),
  controller.create
);

// Autenticado: qualquer role
router.get(
  '/me',
  authenticate,
  controller.getMe
);

// Autenticado + ADMIN ou MANAGER
router.get(
  '/',
  authenticate,
  authorize('ADMIN', 'MANAGER'),
  validate({ query: ListUsersQuerySchema }),
  controller.list
);

// Autenticado + ADMIN + validação de params
router.delete(
  '/:id',
  authenticate,
  authorize('ADMIN'),
  validate({ params: UserIdParamSchema }),
  controller.delete
);

export { router as usersRouter };

RequestId com AsyncLocalStorage

Um desafio comum em aplicações com muitas requisições concorrentes é correlacionar os logs de uma mesma requisição. Quando 50 requisições processam simultaneamente, logs do tipo 'Usuário criado com sucesso' não dizem a qual fluxo pertencem. O AsyncLocalStorage do Node.js resolve isso de forma elegante: ele cria um 'contexto' que persiste ao longo de toda a cadeia de chamadas assíncronas de uma requisição, sem precisar passar o requestId como argumento para cada função.

requestContext.ts
import { AsyncLocalStorage } from 'async_hooks';
import type { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';

interface RequestContext {
  requestId: string;
  userId?: string;
  startTime: number;
}

// Armazena contexto da requisição acessível em qualquer parte do código
// sem precisar passar req como argumento
export const requestContext = new AsyncLocalStorage<RequestContext>();

export function requestContextMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const requestId = (req.headers['x-request-id'] as string) ?? randomUUID();

  // Adiciona o requestId na resposta para rastreabilidade
  res.setHeader('X-Request-Id', requestId);

  // Executa o restante do pipeline dentro do contexto
  requestContext.run({ requestId, startTime: Date.now() }, () => {
    next();
  });
}

// Em qualquer Use Case, Service ou Provider:
// const ctx = requestContext.getStore();
// logger.info('Operação concluída', { requestId: ctx?.requestId });

A ordem dos middlewares importa: requestContext e logging devem vir primeiro (antes de qualquer rota), depois validação do corpo (express.json()), depois rate limiting global, depois as rotas. Middlewares de autenticação e validação específicos de rota ficam inline na declaração da rota — não globais.

Conclusão

O pipeline de middlewares do Express transforma o que seria um bloco monolítico de lógica em camadas composáveis: o middleware authenticate decodifica o token e injeta o usuário, authorize verifica o role, validate normaliza e valida os dados. O Controller recebe a requisição já processada e confiável. Cada middleware é testável isoladamente — e podem ser reutilizados em qualquer rota que precise da mesma proteção.