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