Construindo middlewares reutilizáveis em Express e Fastify
1. Fundamentos de Middleware em Node.js
Middleware é o coração do pipeline de requisição-resposta em aplicações Node.js. No Express, middlewares são funções callback que recebem (req, res, next) e controlam o fluxo chamando next() para passar ao próximo middleware. No Fastify, o modelo é baseado em plugins e hooks, onde cada middleware recebe (request, reply) e utiliza reply.send() para encerrar a resposta.
A diferença arquitetural crucial: Express utiliza uma cadeia linear de middlewares, enquanto Fastify emprega um sistema de plugins hierárquicos com encapsulamento de contexto. O ciclo de vida no Express depende exclusivamente da ordem de registro com app.use(), enquanto no Fastify os hooks (preHandler, onRequest) seguem a estrutura de plugins.
// Express: middleware callback-based
function loggerMiddleware(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}
app.use(loggerMiddleware);
// Fastify: middleware via plugin
async function loggerPlugin(fastify, opts) {
fastify.addHook('onRequest', async (request, reply) => {
console.log(`${request.method} ${request.url}`);
});
}
fastify.register(loggerPlugin);
2. Estruturando Middlewares Reutilizáveis
O padrão de fábrica de funções (factory pattern) permite criar middlewares configuráveis que retornam uma interface uniforme. Para Express, a fábrica retorna (req, res, next). Para Fastify, retorna uma função assíncrona (request, reply) ou um objeto de plugin.
// Factory pattern para middleware configurável
function createRateLimiter(options = {}) {
const { windowMs = 60000, max = 100 } = options;
const requests = new Map();
// Interface Express
function expressMiddleware(req, res, next) {
const ip = req.ip;
const now = Date.now();
// lógica de rate limiting
next();
}
// Interface Fastify
async function fastifyMiddleware(request, reply) {
const ip = request.ip;
const now = Date.now();
// lógica de rate limiting
}
return { express: expressMiddleware, fastify: fastifyMiddleware };
}
// Uso
const limiter = createRateLimiter({ windowMs: 30000, max: 50 });
app.use(limiter.express);
fastify.addHook('onRequest', limiter.fastify);
Parâmetros dinâmicos permitem customização por contexto: diferentes limites para rotas de autenticação versus API pública.
3. Middleware de Autenticação e Autorização
Um middleware de JWT reutilizável deve extrair o token, validar a assinatura e disponibilizar o payload para handlers posteriores.
function createAuthMiddleware(secretKey, options = {}) {
const { blacklist = new Set() } = options;
function expressAuth(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token || blacklist.has(token)) {
return res.status(401).json({ error: 'Token inválido' });
}
try {
const payload = jwt.verify(token, secretKey);
req.user = payload;
next();
} catch (err) {
res.status(401).json({ error: 'Token expirado' });
}
}
async function fastifyAuth(request, reply) {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token || blacklist.has(token)) {
return reply.status(401).send({ error: 'Token inválido' });
}
try {
const payload = jwt.verify(token, secretKey);
request.user = payload;
} catch (err) {
reply.status(401).send({ error: 'Token expirado' });
}
}
return { express: expressAuth, fastify: fastifyAuth };
}
// Verificação de escopos
function requireScope(scope) {
return {
express: (req, res, next) => {
if (!req.user?.scopes?.includes(scope)) {
return res.status(403).json({ error: 'Permissão negada' });
}
next();
},
fastify: async (request, reply) => {
if (!request.user?.scopes?.includes(scope)) {
return reply.status(403).send({ error: 'Permissão negada' });
}
}
};
}
Estratégias de cache com Redis para sessões e blacklist distribuída de tokens invalidados.
4. Middleware de Validação e Sanitização de Dados
Schemas de validação (Zod, Joi) podem ser adaptados para ambos os frameworks com tratamento unificado de erros.
const { z } = require('zod');
function createValidationMiddleware(schema, source = 'body') {
function expressValidator(req, res, next) {
try {
const data = schema.parse(req[source]);
req[source] = data;
next();
} catch (err) {
const errors = err.errors.map(e => ({ field: e.path.join('.'), message: e.message }));
res.status(400).json({ error: 'Dados inválidos', details: errors });
}
}
async function fastifyValidator(request, reply) {
try {
const data = schema.parse(request[source]);
request[source] = data;
} catch (err) {
const errors = err.errors.map(e => ({ field: e.path.join('.'), message: e.message }));
reply.status(400).send({ error: 'Dados inválidos', details: errors });
}
}
return { express: expressValidator, fastify: fastifyValidator };
}
// Sanitização contra XSS e SQL injection
function sanitizeInput(data) {
if (typeof data === 'string') {
return data.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/'/g, "''");
}
if (Array.isArray(data)) return data.map(sanitizeInput);
if (data && typeof data === 'object') {
return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, sanitizeInput(v)]));
}
return data;
}
const sanitizeMiddleware = {
express: (req, res, next) => { req.body = sanitizeInput(req.body); next(); },
fastify: async (request, reply) => { request.body = sanitizeInput(request.body); }
};
5. Middleware de Logging e Monitoramento
Log estruturado com contextos enriquecidos (request ID, duração, usuário) usando Pino (padrão Fastify) ou Winston (comum no Express).
const { v4: uuidv4 } = require('uuid');
function createLoggingMiddleware(logger) {
function expressLogger(req, res, next) {
const requestId = uuidv4();
req.requestId = requestId;
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
requestId,
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
user: req.user?.id || 'anonymous'
});
});
next();
}
async function fastifyLogger(request, reply) {
const requestId = uuidv4();
request.requestId = requestId;
const start = Date.now();
reply.then(() => {
const duration = Date.now() - start;
logger.info({
requestId,
method: request.method,
url: request.url,
status: reply.statusCode,
duration: `${duration}ms`,
user: request.user?.id || 'anonymous'
});
});
}
return { express: expressLogger, fastify: fastifyLogger };
}
// Uso com Pino
const pino = require('pino');
const logger = pino({ level: 'info' });
const loggingMiddleware = createLoggingMiddleware(logger);
Métricas de performance: contagem de requisições por rota e tempo médio de resposta.
6. Middleware de Tratamento de Erros e Resiliência
Captura global de exceções com formato padronizado de resposta de erro.
function createErrorHandler(options = {}) {
const { includeStack = false } = options;
function expressErrorHandler(err, req, res, next) {
const status = err.status || 500;
const body = {
error: err.message || 'Erro interno',
status,
requestId: req.requestId
};
if (includeStack && err.stack) body.stack = err.stack;
res.status(status).json(body);
}
async function fastifyErrorHandler(error, request, reply) {
const status = error.statusCode || 500;
const body = {
error: error.message || 'Erro interno',
status,
requestId: request.requestId
};
if (includeStack && error.stack) body.stack = error.stack;
reply.status(status).send(body);
}
return { express: expressErrorHandler, fastify: fastifyErrorHandler };
}
// Rate limiting com circuit breaker
function createCircuitBreaker(options = {}) {
const { threshold = 5, timeout = 30000 } = options;
let failures = 0;
let lastFailure = 0;
return {
express: (req, res, next) => {
if (failures >= threshold) {
if (Date.now() - lastFailure < timeout) {
return res.status(503).json({ error: 'Serviço temporariamente indisponível' });
}
failures = 0;
}
next();
},
recordFailure: () => { failures++; lastFailure = Date.now(); },
reset: () => { failures = 0; }
};
}
7. Empacotamento e Distribuição como Pacote NPM
Estrutura de diretórios para um pacote de middlewares reutilizáveis:
meus-middlewares/
├── src/
│ ├── auth/
│ │ ├── index.js
│ │ ├── express.js
│ │ └── fastify.js
│ ├── validation/
│ │ ├── index.js
│ │ ├── express.js
│ │ └── fastify.js
│ ├── logging/
│ ├── error-handler/
│ └── index.js
├── test/
│ ├── auth.test.js
│ ├── validation.test.js
│ └── integration/
├── package.json
└── README.md
Exportação unificada no index.js:
module.exports = {
createAuthMiddleware: require('./auth'),
createValidationMiddleware: require('./validation'),
createLoggingMiddleware: require('./logging'),
createErrorHandler: require('./error-handler'),
createRateLimiter: require('./rate-limiter'),
createCircuitBreaker: require('./circuit-breaker')
};
Testes unitários com Jest para cada middleware isoladamente e testes de integração simulando requisições HTTP em ambos os frameworks. Documentação no README com exemplos de uso para Express e Fastify, incluindo cenários de configuração avançada.
Referências
- Documentação Oficial do Express - Middleware — Guia completo sobre como usar e escrever middlewares no Express, incluindo exemplos de middleware de aplicação e rota.
- Documentação Oficial do Fastify - Hooks — Referência detalhada sobre hooks no Fastify, incluindo onRequest, preHandler e onError para implementar middlewares.
- Zod - Validação de Schemas — Biblioteca de validação TypeScript-first com exemplos de schemas para requisições HTTP.
- Pino - Logger para Node.js — Logger de baixa sobrecarga usado nativamente pelo Fastify, com guia de integração em aplicações Express.
- JSON Web Tokens (JWT) - RFC 7519 — Especificação oficial do JWT, base para implementação de middlewares de autenticação.
- Joi - Validação de Dados — Biblioteca popular de validação de schemas para JavaScript, com exemplos de uso em middlewares Express.
- Rate Limiting com express-rate-limit — Pacote NPM para rate limiting em Express, com exemplos de configuração e boas práticas.
- Circuit Breaker Pattern - Martin Fowler — Artigo conceitual sobre o padrão circuit breaker, aplicável a middlewares de resiliência.