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