Middlewares

Intermedio
Nest
Nest
Actualizado: 05/05/2026

Diagrama: tutorial-nest-middleware

Qué es un middleware en NestJS

Un middleware en NestJS es una función que se ejecuta durante el ciclo de vida de una petición HTTP, entre la recepción del request y la entrega al controlador. NestJS hereda el modelo de middlewares de Express (o Fastify, según el adaptador), por lo que tiene acceso completo a request, response y la función next() que cede el control al siguiente eslabón.

Los middlewares son el punto ideal para responsabilidades transversales que no pertenecen al dominio: logging estructurado, cabeceras de seguridad, correlación de trazas, rate limiting o parsing de cookies. Separar estas operaciones del controlador mantiene el código de negocio limpio y reutilizable.

Un middleware puede terminar el ciclo llamando a response.send() o response.status().json() sin invocar next(). Esto se usa en middlewares de autenticación para devolver 401 sin llegar al controlador.

Middleware funcional vs clase

NestJS admite dos formatos. El funcional es una función con la firma (req, res, next); el de clase implementa la interfaz NestMiddleware y se decora con @Injectable(), lo que permite inyectar dependencias.

import { Request, Response, NextFunction } from 'express';

export function loggerRequest(req: Request, res: Response, next: NextFunction) {
  const inicio = Date.now();
  res.on('finish', () => {
    const duracionMs = Date.now() - inicio;
    console.log(`${req.method} ${req.originalUrl} -> ${res.statusCode} (${duracionMs} ms)`);
  });
  next();
}

Para middlewares que necesitan servicios (logger estructurado, cliente Redis para rate limiting) conviene usar la variante de clase.

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class CorrelationIdMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const correlationId = req.headers['x-correlation-id'] ?? crypto.randomUUID();
    req['correlationId'] = correlationId;
    res.setHeader('x-correlation-id', correlationId as string);
    this.logger.log(`[${correlationId}] ${req.method} ${req.originalUrl}`);
    next();
  }
}

El correlationId viaja después en los logs de los servicios downstream y en las trazas de OpenTelemetry, haciendo posible recorrer una petición de extremo a extremo en plataformas con decenas de microservicios.

Registro en módulos con MiddlewareConsumer

Los middlewares se registran en un módulo implementando NestModule. El MiddlewareConsumer permite filtrar rutas, excluir patrones y encadenar varios middlewares en orden.

import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { CorrelationIdMiddleware } from './correlation-id.middleware';
import { RateLimitMiddleware } from './rate-limit.middleware';

@Module({
  controllers: [PedidosController, UsuariosController],
})
export class ApiModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(CorrelationIdMiddleware)
      .forRoutes('*');

    consumer
      .apply(RateLimitMiddleware)
      .exclude({ path: 'health', method: RequestMethod.GET })
      .forRoutes(
        { path: 'pedidos', method: RequestMethod.POST },
        { path: 'pedidos/:id', method: RequestMethod.PUT },
      );
  }
}

El orden es importante. NestJS ejecuta los middlewares en el orden de registro dentro de un mismo módulo. Para aplicar un middleware a toda la aplicación se registra en main.ts con app.use(...) antes del listen.

Caso real: rate limiting en un API de banca

En un API de banca expuesto a clientes móviles, cada endpoint tiene un límite por IP y cliente para prevenir abuso y ataques de fuerza bruta sobre el login. Un middleware de rate limiting con Redis aplica la regla y registra métricas por cliente.

import { Injectable, NestMiddleware, HttpStatus } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  constructor(private readonly redis: Redis) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const clave = `rl:${req.ip}:${req.path}`;
    const contador = await this.redis.incr(clave);
    if (contador === 1) {
      await this.redis.expire(clave, 60);
    }
    if (contador > 60) {
      res.status(HttpStatus.TOO_MANY_REQUESTS).json({
        codigo: 'RATE-LIMIT-001',
        mensaje: 'Demasiadas peticiones, intente de nuevo en 1 minuto',
      });
      return;
    }
    res.setHeader('x-ratelimit-remaining', String(60 - contador));
    next();
  }
}

El contador se resetea cada 60 segundos gracias a EXPIRE, y la respuesta devuelve un código de negocio (RATE-LIMIT-001) que el front usa para mostrar el mensaje al usuario sin tener que interpretar el HTTP status.

Orden dentro del pipeline de NestJS

flowchart LR
    A[Request HTTP] --> B[Middlewares globales]
    B --> C[Middlewares de módulo]
    C --> D[Guards]
    D --> E[Interceptors]
    E --> F[Pipes]
    F --> G[Controlador]
    G --> H[Interceptors salida]
    H --> I[Exception Filters]

Los middlewares son la primera capa tras el servidor HTTP. Si uno de ellos rechaza el request, ni guards ni pipes llegan a ejecutarse. Este orden es clave cuando se decide qué responsabilidad asignar a cada componente: autorización por rol suele ir en guards, validación de DTOs en pipes, y todo lo que ocurre antes de conocer la ruta concreta va en middleware.

Middleware funcional global con app.use

Para librerías de Express/Fastify que vienen como middlewares (helmet, compressión, cookie-parser), se registran en main.ts antes del listen:

import helmet from 'helmet';
import compression from 'compression';
import cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(helmet());                    // cabeceras de seguridad
  app.use(compression());               // gzip de respuestas grandes
  app.use(cookieParser(process.env.COOKIE_SECRET));
  app.enableCors({ origin: ['https://app.example.com'], credentials: true });
  await app.listen(3000);
}

helmet añade Content-Security-Policy, Strict-Transport-Security y otras cabeceras críticas para auditorías ENS / ISO 27001. Es el primer paso en cualquier proyecto B2B.

Diferencias con guards e interceptors

| Aspecto | Middleware | Guard | Interceptor | |---------|-----------|-------|-------------| | Acceso al ExecutionContext | No (req/res nativo) | Sí | Sí | | Modificar respuesta | Sí | No | Sí (RxJS pipe) | | Decisión de continuar | next() o respuesta | boolean | Observable | | Conocimiento del handler | Genérico (path) | Sí (getHandler()) | Sí | | Inyección de dependencias | Solo en clase | Sí | Sí | | Buen uso | Logging HTTP, helmet, parsing | Autorización por rol | Logging timings, cache, transformación |

La regla práctica: si la lógica depende solo de la URL y los headers, usa middleware. Si depende del rol o del handler concreto, usa guard. Si necesita transformar la respuesta, interceptor.

Middleware con AsyncLocalStorage para tracing

AsyncLocalStorage (Node.js nativo) permite mantener contexto durante todo el ciclo del request sin pasar argumentos por todos los servicios:

import { AsyncLocalStorage } from 'node:async_hooks';

export const traceContext = new AsyncLocalStorage<{ traceId: string; userId?: string }>();

@Injectable()
export class TracingMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const traceId = req.headers['x-trace-id'] as string ?? crypto.randomUUID();
    res.setHeader('x-trace-id', traceId);
    traceContext.run({ traceId, userId: undefined }, () => next());
  }
}

// En cualquier servicio downstream:
@Injectable()
export class OrderService {
  private readonly logger = new Logger('OrderService');

  async create(dto: CreateOrderDto) {
    const ctx = traceContext.getStore();
    this.logger.log(`[${ctx?.traceId}] Creando orden`);
    // El traceId aparece en todos los logs sin pasarlo manualmente
  }
}

Combinado con OpenTelemetry, esto permite correlacionar logs, métricas y trazas distribuidas sin modificar las firmas de los servicios. Es la práctica enterprise estándar.

Cuando NO usar middleware

  • Validación de DTOs: usar ValidationPipe (acceso al schema del DTO, errores tipados).
  • Autorización por rol: usar guards (getHandler() para leer metadatos @Roles).
  • Transformación de respuesta: usar interceptor (RxJS pipe sobre Observable<T>).
  • Cache de respuesta: usar CacheInterceptor que es consciente del handler.

Forzar estas responsabilidades en middleware lleva a código frágil que duplica trabajo del framework.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Nest

Documentación oficial de Nest
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Nest es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Nest

Explora más contenido relacionado con Nest y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender qué es un middleware y su función en el ciclo de vida HTTP.
  • Diferenciar middlewares funcionales y basados en clases.
  • Configurar middlewares sobre rutas y métodos concretos.
  • Identificar casos de uso típicos (logging, auth, rate limiting).
  • Combinar middlewares con guards, interceptors y pipes.