Middleware chains: criando pipelines de processamento de requests

1. Conceitos Fundamentais de Middleware Chains

Middleware é um componente de software que intercepta e processa requests antes que eles atinjam o manipulador final de uma aplicação. Baseado no padrão de projeto Chain of Responsibility (Cadeia de Responsabilidade), cada middleware executa uma função específica e decide se passa o controle adiante ou interrompe o fluxo.

A diferença fundamental entre middleware síncrono e assíncrono está no impacto sobre a performance. Middleware síncrono bloqueia a execução até que sua tarefa seja concluída, enquanto o assíncrono permite que outras operações ocorram em paralelo, essencial para operações de I/O como acesso a banco de dados ou chamadas a APIs externas.

O conceito de "próximo" (next) é o mecanismo que conecta os elos da cadeia. Cada middleware recebe uma referência para o próximo middleware na pipeline, podendo:
- Chamá-lo para continuar o processamento
- Ignorá-lo para interromper a cadeia
- Capturar exceções e decidir como propagá-las

// Exemplo conceitual de encadeamento
function middleware1(request, next) {
    console.log("Middleware 1: iniciando")
    const resultado = next(request) // chama o próximo
    console.log("Middleware 1: finalizando")
    return resultado
}

function middleware2(request, next) {
    console.log("Middleware 2: processando")
    return "resposta final"
}

2. Estrutura Típica de uma Pipeline de Middleware

Uma pipeline de middleware é composta por três elementos essenciais: o contexto (que carrega estado compartilhado), o objeto request (dados da requisição) e o objeto response (dados da resposta). As funções middleware são executadas em ordem sequencial, mas é possível implementar execução paralela ou condicional.

// Pipeline de validação, autenticação e logging
function loggingMiddleware(request, next) {
    console.log(`[${new Date().toISOString()}] ${request.method} ${request.path}`)
    return next(request)
}

function validationMiddleware(request, next) {
    if (!request.body || !request.body.email) {
        return { status: 400, body: "Email é obrigatório" }
    }
    return next(request)
}

function authMiddleware(request, next) {
    const token = request.headers.authorization
    if (!token || token !== "token-valido") {
        return { status: 401, body: "Não autorizado" }
    }
    request.user = { id: 123, name: "Usuário" }
    return next(request)
}

// Montagem da pipeline
const pipeline = [loggingMiddleware, validationMiddleware, authMiddleware]

3. Implementando Middleware com Estado e Contexto Compartilhado

O uso de contexto (context.Context em Go ou objetos similares em outras linguagens) permite propagar dados entre middlewares sem poluir as assinaturas das funções. É crucial gerenciar o estado mutável com cuidado, especialmente em ambientes concorrentes.

// Middleware de tracing com contexto compartilhado
function tracingMiddleware(request, next) {
    const traceId = request.headers["x-trace-id"] || crypto.randomUUID()
    request.context.traceId = traceId
    request.context.startTime = Date.now()

    const response = next(request)

    console.log(`[TRACE ${traceId}] Duração: ${Date.now() - request.context.startTime}ms`)
    return response
}

// Middleware de injeção de dependências
function dependencyInjectionMiddleware(request, next) {
    request.context.db = new DatabaseConnection()
    request.context.cache = new RedisClient()
    request.context.logger = new Logger(request.context.traceId)

    return next(request)
}

4. Padrões de Design para Middleware Reutilizáveis

Middlewares paramétricos são fábricas que retornam funções middleware configuráveis. Isso permite criar middlewares genéricos que se adaptam a diferentes contextos sem duplicação de código.

// Fábrica de middleware de rate limiting
function createRateLimiter(maxRequests, windowMs) {
    const requests = new Map()

    return function rateLimiterMiddleware(request, next) {
        const ip = request.ip
        const now = Date.now()

        if (!requests.has(ip)) {
            requests.set(ip, [])
        }

        const timestamps = requests.get(ip).filter(t => now - t < windowMs)

        if (timestamps.length >= maxRequests) {
            return { status: 429, body: "Muitas requisições" }
        }

        timestamps.push(now)
        requests.set(ip, timestamps)

        return next(request)
    }
}

// Uso: const rateLimiter = createRateLimiter(100, 60000)

// Composição de middlewares
function compose(...middlewares) {
    return function composedMiddleware(request) {
        let index = 0

        function next(req) {
            const middleware = middlewares[index++]
            if (!middleware) return { status: 404, body: "Not Found" }
            return middleware(req, next)
        }

        return next(request)
    }
}

5. Tratamento de Erros e Interrupção da Cadeia

Estratégias robustas de tratamento de erros são essenciais para pipelines resilientes. A interrupção precoce permite abortar a cadeia quando uma condição crítica não é satisfeita.

// Middleware de autorização que interrompe a cadeia
function authorizationMiddleware(request, next) {
    if (!request.user) {
        return { status: 403, body: "Acesso negado" }
    }

    if (!request.user.roles.includes("admin")) {
        return { status: 403, body: "Permissão insuficiente" }
    }

    return next(request) // continua apenas se autorizado
}

// Middleware de captura de erros
function errorHandlerMiddleware(request, next) {
    try {
        return next(request)
    } catch (error) {
        console.error(`Erro no pipeline: ${error.message}`)
        return { status: 500, body: "Erro interno do servidor" }
    }
}

6. Monitoramento e Observabilidade em Pipelines

Inserir métricas e logs em cada middleware permite rastrear gargalos de performance e diagnosticar problemas. O rastreamento distribuído (tracing) através da cadeia é fundamental para sistemas complexos.

// Middleware de logging estruturado
function structuredLoggingMiddleware(request, next) {
    const logEntry = {
        timestamp: new Date().toISOString(),
        method: request.method,
        path: request.path,
        query: request.query,
        userId: request.user?.id
    }

    console.log(JSON.stringify(logEntry))
    return next(request)
}

// Middleware de medição de latência
function latencyMeasurementMiddleware(request, next) {
    const start = process.hrtime.bigint()

    const response = next(request)

    const end = process.hrtime.bigint()
    const duration = Number(end - start) / 1e6 // converte para milissegundos

    metrics.recordLatency(request.path, duration)

    if (duration > 1000) {
        console.warn(`Alerta: ${request.path} demorou ${duration}ms`)
    }

    return response
}

7. Boas Práticas e Armadilhas Comuns

O equilíbrio entre reuso e coesão é crucial. Middlewares muito genéricos tendem a acumular responsabilidades excessivas, enquanto muito específicos dificultam a manutenção. Efeitos colaterais e dependências ocultas entre middlewares podem causar bugs difíceis de rastrear.

// Boa prática: middleware coeso e testável
function sanitizeInputMiddleware(request, next) {
    const sanitized = {}
    for (const [key, value] of Object.entries(request.body)) {
        sanitized[key] = typeof value === 'string' ? value.trim() : value
    }
    request.body = sanitized
    return next(request)
}

// Teste isolado
function testSanitizeInput() {
    const request = { body: { name: "  João  ", email: " joao@email.com " } }
    const next = (req) => req

    const result = sanitizeInputMiddleware(request, next)
    console.assert(result.body.name === "João", "Nome deve ser trimado")
    console.assert(result.body.email === "joao@email.com", "Email deve ser trimado")
}

Armadilhas comuns:
- Modificar o objeto request em um middleware e assumir o estado em outro
- Não tratar erros em middlewares assíncronos
- Criar middlewares que dependem da ordem específica de execução
- Ignorar a limpeza de recursos em middlewares que abrem conexões

Referências