🚨 ¡Nueva review! ¡Mi teclado ideal! ⌨️ Perfecto para programar, el Logitech MX Keys S . ¡Échale un ojo! 👀

Middleware y el ciclo de vida de una request en NestJS

Serie NestJS #5 — Middleware, MiddlewareConsumer, y el pipeline completo de una petición

Escrito por domin el 24 de marzo de 2026

Vamos con el quinto post de la serie NestJS. Ya tenemos el entorno con Docker (post 1), controllers con rutas y DTOs (post 2), inyección de dependencias (post 3) y módulos organizados como un arquitecto profesioná (post 4). Hoy toca entender qué es lo que pasa desde que una petición HTTP llega a tu servidor hasta que se envía la respuesta.

Porque hay un pipeline entero que la request recorre antes de llegar a tu controller. Y si no entiendes ese pipeline, cuando metas Guards, Interceptors, Pipes o Exception Filters en posts futuros vas a estar pegándote contra la pared sin saber por qué algo se ejecuta antes o después en el runtime.

El middleware es la primera pieza de ese pipeline, vamos a echarle un ogt.

EA, al lío.

Diagrama del ciclo de vida de una request en NestJS mostrando el pipeline desde Middleware hasta el Response.

1. ¿Qué es un Middleware?

Un es una función que se ejecuta antes de que la petición llegue al route handler de tu controller. Es exactamente el mismo concepto que en Express, porque por debajo NestJS usa Express o Fastify.

Cada middleware recibe tres cosas:

Si no llamas a next(), la request se queda ahí muerta y no avanza. Es como un segurata en la puerta: o te deja pasar o al carrer.

¿Para qué se usan? Logging, autenticación, validación de headers, CORS personalizado, rate limiting, parsear cuerpos especiales, modificar la request… cualquier lógica transversal que no pertenece a un controller concreto.


2. Middleware como clase: NestMiddleware

La forma más completa de crear un middleware en NestJS es como una clase que implementa la interfaz :

// src/common/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { type Request, type Response, type NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction): void {
        const { method, originalUrl } = req;
        const start = Date.now();

        res.on('finish', () => {
            const duration = Date.now() - start;
            const { statusCode } = res;
            console.warn(`[${method}] ${originalUrl} → ${statusCode} (${duration}ms)`);
        });

        next();
    }
}

Vamos a ver las cosas importantes aquí:

  1. @Injectable() — Un middleware de clase es un provider más. Esto significa que puede inyectar dependencias. ¿Necesitas un ConfigService para leer un header configurable? Inyéctalo en el constructor.
  2. implements NestMiddleware — Te obliga a implementar el método use(), TypeScript te pega si no lo haces.
  3. res.on('finish') — Escuchamos el evento finish del response para loguear cuando la respuesta realmente se envía, incluyendo el status code y la duración.
  4. next() — Sin esto, la request no llega nunca al controller.

Middleware con inyección de dependencias

La ventaja real de usar clases para middleware es poder inyectar providers:

// src/common/middleware/auth-header.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { type Request, type Response, type NextFunction } from 'express';
import { ConfigService } from '../../config/config.service';

@Injectable()
export class AuthHeaderMiddleware implements NestMiddleware {
    constructor(private readonly configService: ConfigService) {}

    use(req: Request, res: Response, next: NextFunction): void {
        const apiKey = req.headers['x-api-key'];
        const expectedKey = this.configService.get('API_KEY');

        if (!apiKey || apiKey !== expectedKey) {
            res.status(401).json({ message: 'API key inválida o ausente' });
            return; // No llamamos next() → la request muere aquí
        }

        next();
    }
}

[!IMPORTANT] Fíjate que cuando la API key no es válida, enviamos la respuesta directamente y no llamamos a next() por lo que La request nunca llega al controller.


3. Middleware funcional

Si tu middleware es simple y no necesita inyectar dependencias, puedes usar una función en vez de una clase, más ligero y sin malabares:

// src/common/middleware/cors-logger.middleware.ts
import { type Request, type Response, type NextFunction } from 'express';

export function corsLoggerMiddleware(req: Request, res: Response, next: NextFunction): void {
    if (req.headers.origin) {
        console.warn(`[CORS] Request from origin: ${req.headers.origin}`);
    }
    next();
}

Sin @Injectable(), sin interfaz, sin clase. Solo una función con la misma firma de siempre: (req, res, next).

Úsalo cuando...
  • Middleware funcional cuando no necesitas inyectar dependencias y la lógica es directa
  • Middleware clase cuando necesitas DI, estado, o lógica más compleja que se beneficie de tener métodos auxiliares
Evítalo cuando...
  • Crear una clase con @Injectable() para un middleware que solo hace un console.warn y llama a next()
  • Usar middleware funcional cuando necesitas acceder a ConfigService, un logger inyectado u otro provider

4. Registrar middleware: MiddlewareConsumer

A diferencia de los controllers y providers, los middleware no se registran en el decorador @Module(). Se configuran en el método configure() del módulo, usando el :

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';

@Module({
    imports: [UsersModule, ProductsModule],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer): void {
        consumer
            .apply(LoggerMiddleware)
            .forRoutes('*'); // Todas las rutas
    }
}

Vamos a ver esto paso a paso:

  1. El módulo implementa la interfaz NestModule, que exige un método configure().
  2. configure() recibe un MiddlewareConsumer.
  3. .apply(LoggerMiddleware) — Qué middleware aplicar.
  4. .forRoutes('*') — A qué rutas. El '*' significa todas.

Si necesitas aplicar más de un middleware, pásalos separados por comas a apply(). Se ejecutan en el orden en que los pones:

configure(consumer: MiddlewareConsumer): void {
    consumer
        .apply(LoggerMiddleware, AuthHeaderMiddleware)
        .forRoutes('*');
}

Aquí primero se ejecuta LoggerMiddleware y después AuthHeaderMiddleware. Si el primero no llama a next(), el segundo middleware no se entera de que hay una request.

Y si necesitas que cada middleware se aplique a rutas distintas, encadena varias llamadas a apply().forRoutes():

configure(consumer: MiddlewareConsumer): void {
    consumer
        .apply(LoggerMiddleware)
        .forRoutes('*') // Logger en todas las rutas
        .apply(AuthHeaderMiddleware)
        .forRoutes('users', 'products'); // Auth solo en users y products
}

Cada bloque apply().forRoutes() es independiente. El LoggerMiddleware se ejecuta en todas las rutas, pero el AuthHeaderMiddleware solo pasa por /users y /products.


5. forRoutes() — Controlando dónde se aplica

El forRoutes() es bastante flexible y acepta varias formas:

5.1. String con la ruta

consumer
    .apply(LoggerMiddleware)
    .forRoutes('users'); // Solo rutas que empiecen por /users

5.2. Ruta + método HTTP concreto

import { RequestMethod } from '@nestjs/common';

consumer
    .apply(LoggerMiddleware)
    .forRoutes(
        { path: 'users', method: RequestMethod.GET },
        { path: 'users', method: RequestMethod.POST },
    );

Solo se aplica a GET /users y POST /users. Un DELETE /users/:id no pasaría por este middleware.

5.3. Referencia a un controller

import { UsersController } from './users/users.controller';

consumer
    .apply(LoggerMiddleware)
    .forRoutes(UsersController); // Todas las rutas de UsersController

Esta es la forma más robusta porque si cambias el prefijo del controller (@Controller('api/users')@Controller('v2/users')), el middleware sigue aplicándose sin tocar nada, no dependes de strings mágicos.

5.4. Wildcards con patrones

consumer
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'users/(.*)', method: RequestMethod.ALL });

El (.*) captura cualquier subruta: /users, /users/123, /users/123/orders, etc.


6. Excluir rutas

A veces quieres aplicar un middleware a casi todas las rutas de un controller excepto algunas. Para eso está exclude():

consumer
    .apply(LoggerMiddleware)
    .exclude(
        { path: 'users/health', method: RequestMethod.GET },
        { path: 'users/metrics', method: RequestMethod.GET },
    )
    .forRoutes(UsersController);

El LoggerMiddleware se aplica a todas las rutas de UsersController menos GET /users/health y GET /users/metrics. Perfecto para excluir endpoints de salud o métricas que generan mucho ruido en los logs.


7. Múltiples middleware

Puedes encadenar varios middleware y se ejecutan en el orden en que los pasas a apply():

consumer
    .apply(CorsMiddleware, LoggerMiddleware, AuthHeaderMiddleware)
    .forRoutes('*');

El orden aquí es: CorsMiddlewareLoggerMiddlewareAuthHeaderMiddleware → controller. Si cualquiera de ellos no llama a next(), la cadena se rompe ahí y la ejecución se para.

También puedes encadenar configuraciones diferentes para distintas rutas:

configure(consumer: MiddlewareConsumer): void {
    consumer
        .apply(LoggerMiddleware)
        .forRoutes('*') // Logger en todas las rutas
        .apply(AuthHeaderMiddleware)
        .forRoutes(UsersController, ProductsController); // Auth solo en users y products
}

8. Middleware global

Si quieres que un middleware se ejecute para absolutamente todas las peticiones, sin excepciones, puedes registrarlo globalmente en main.ts:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { corsLoggerMiddleware } from './common/middleware/cors-logger.middleware';

async function bootstrap(): Promise<void> {
    const app = await NestFactory.create(AppModule);

    app.use(corsLoggerMiddleware); // 👈 Middleware global

    await app.listen(3000);
    console.warn('Server running on http://localhost:3000');
}

bootstrap();

Importante: app.use() solo acepta middleware funcional, no de clase. ¿Por qué? Porque app.use() es el método nativo de Express. No pasa por el contenedor de NestJS, así que no puede resolver inyección de dependencias.

Úsalo cuando...
  • app.use() para middleware estándar de Express que ya tienes: helmet, morgan, cookie-parser, compression
  • MiddlewareConsumer cuando necesitas DI, control fino por ruta/controller, o exclude()
Evítalo cuando...
  • app.use() con middleware de clase NestJS (no puede inyectar dependencias)
  • Registrar el mismo middleware dos veces: una global con app.use() y otra con MiddlewareConsumer

9. Ejemplo práctico: middleware de correlación y timing

Vamos a crear un middleware que asigna un ID de correlación a cada request y mide el tiempo de respuesta, esto va genial para el debugging en prod:

// src/common/middleware/correlation.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { type Request, type Response, type NextFunction } from 'express';
import { randomUUID } from 'node:crypto';

@Injectable()
export class CorrelationMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction): void {
        const correlationId = (req.headers['x-correlation-id'] as string) ?? randomUUID();
        const start = Date.now();

        // Adjuntamos el ID a la request para que esté disponible en toda la cadena
        req.headers['x-correlation-id'] = correlationId;

        // Lo devolvemos en la respuesta para que el cliente lo tenga
        res.setHeader('x-correlation-id', correlationId);

        res.on('finish', () => {
            const duration = Date.now() - start;
            console.warn(
                `[${correlationId}] ${req.method} ${req.originalUrl} → ${res.statusCode} (${duration}ms)`,
            );
        });

        next();
    }
}

¿Qué hace esto?

  1. Si el cliente manda un header x-correlation-id, lo reutiliza. Si no, genera uno con randomUUID().
  2. Lo mete en req.headers para que cualquier service o controller de la cadena pueda acceder al ID.
  3. Lo añade como header de respuesta para que el cliente pueda correlacionar su petición con los logs.
  4. Mide el tiempo desde que entra la request hasta que se envía la respuesta.

Ahora lo registramos:

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CorrelationMiddleware } from './common/middleware/correlation.middleware';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';

@Module({
    imports: [UsersModule, ProductsModule],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer): void {
        consumer
            .apply(CorrelationMiddleware)
            .forRoutes('*');
    }
}
Middleware de correlación en acción 0 / 3
$
Pulsa para ejecutar el siguiente comando

10. Tipando la request extendida

Cuando tu middleware añade propiedades a req (como el correlation ID), TypeScript no sabe que existen. Para mantener el tipado estricto, extiende la interfaz de Express:

// src/common/types/express.d.ts
declare namespace Express {
    interface Request {
        correlationId?: string;
    }
}

Y actualiza tu tsconfig.json para incluir este archivo si no lo hace automáticamente. Ahora puedes acceder a req.correlationId sin que TypeScript se queje.

Versión mejorada del middleware con esta tipografía:

@Injectable()
export class CorrelationMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction): void {
        const correlationId = (req.headers['x-correlation-id'] as string) ?? randomUUID();

        req.correlationId = correlationId;
        res.setHeader('x-correlation-id', correlationId);

        next();
    }
}

Ahora en cualquier controller o service puedes acceder directamente:

@Get()
findAll(@Req() req: Request): User[] {
    console.warn(`Correlation ID: ${req.correlationId}`);
    return this.usersService.findAll();
}

Limpio, tipado y sin casteos guarros.


11. El ciclo de vida completo de una request

Vamos a ver el panorama completo. Cuando una petición HTTP llega a tu aplicación NestJS, atraviesa un pipeline con varias capas, el middleware es solo la primera.

Este es el orden de ejecución:

NestJS request life

Cada pieza tiene su responsabilidad, y es aquí donde igual es fácil de confundirse para que debemos usar cada cosa:

CapaResponsabilidadEjemplo
MiddlewareLógica transversal genérica. Acceso directo a req/res. Ejecuta antes que todo lo demásLogging, correlation IDs, helmet, CORS, body parsing
GuardsDecidir si la request puede o no pasar. Devuelven true/falseAutenticación JWT, autorización por roles, rate limiting
Interceptors (antes)Transformar la request antes del handler o añadir lógica envolventeLogging de entrada, cache check, timeout
PipesValidar y transformar los parámetros de entrada del handlerValidar DTOs, parsear IDs a números, sanitizar strings
HandlerTu lógica de negocio. El método del controller que procesa la requestfindAll(), create(), update(), remove()
Interceptors (después)Transformar la respuesta del handler antes de enviarlaSerializar, envolver en formato estándar, logging de salida
Exception FiltersCapturar excepciones y convertirlas en respuestas HTTP adecuadasConvertir errores a JSON, logging de errores, error pages

¿Por qué importa este orden?

Porque cada decisión de diseño depende de saber en qué punto del pipeline estás:

Cuando veamos Guards, Interceptors, Pipes y Exception Filters en los próximos posts, todo va a quedar bastante más claro, de momento grábate este diagrama a fuego.


12. Middleware vs Guards: la delgada línea.

Una duda que surge siempre: ¿cuándo uso middleware y cuándo un Guard?. La respuesta es más fácil de lo que parece:

🔧 Middleware

Acceso a req/res de Express. No conoce el contexto de NestJS (no sabe qué handler se va a ejecutar, no tiene acceso a metadata de decoradores). Ideal para lógica genérica y agnóstica del framework.

🛡️ Guard

Tiene acceso al ExecutionContext de NestJS. Puede leer metadata de decoradores (@Roles, @Public). Sabe exactamente qué controller y método se va a ejecutar. Ideal para autorización.

El middleware es agnóstico de NestJS porque es Express envuelto en una clase. No sabe qué controller va a manejar la request, no puede leer decoradores custom. Solo ve req, res y next.

El Guard sí conoce el contexto de NestJS y puede leer metadata puesta con @SetMetadata(), sabe qué handler se va a ejecutar, y puede tomar decisiones basadas en decoradores como @Roles('admin').

Por eso la autenticación básica como verificar que un JWT existe, puede ir en middleware, pero la autorización para verificar que el usuario tiene el rol correcto para esa ruta concreta, va en un Guard.


13. Errores comunes y debugging

Olvidar llamar a next()

// ❌ La request se queda colgada para siempre
use(req: Request, res: Response, next: NextFunction): void {
    console.warn('Log...');
    // next() — oops, olvidé llamarlo
}

Si la request se queda colgada por un timeout sin respuesta, lo primero que debes comprobar es que todos tus middleware llamen a next(), es el error más habitual.

Orden de los middleware importa

// El orden en apply() ES el orden de ejecución
consumer
    .apply(AuthMiddleware, LoggerMiddleware) // Auth ANTES que Logger
    .forRoutes('*');

consumer
    .apply(LoggerMiddleware, AuthMiddleware) // Logger ANTES que Auth
    .forRoutes('*');

Si pones el LoggerMiddleware después del AuthMiddleware y el auth falla (no llama a next()), el logger nunca se ejecuta. Piensa siempre en el orden.

Modificar res después de next()

// ❌ Esto puede causar "headers already sent"
use(req: Request, res: Response, next: NextFunction): void {
    next(); // El controller ya puede haber enviado la respuesta
    res.setHeader('x-custom', 'value'); // 💥 Error si la respuesta ya se envió
}

Si necesitas actuar sobre la respuesta, usa res.on('finish') como hicimos en el middleware, o usa un Interceptor que está diseñado exactamente para eso.


14. Recapitulando

🏗️ Middleware Clase

@Injectable() + implements NestMiddleware. Puede inyectar dependencias. Se configura con MiddlewareConsumer.

⚡ Middleware Funcional

Función (req, res, next). Sin DI. Más ligero para lógica simple. Obligatorio para app.use().

🎛️ MiddlewareConsumer

API fluida: apply().forRoutes().exclude(). Acepta strings, objetos {path, method} o controllers.

🛤️ forRoutes()

Controla dónde se aplica. Strings para rutas, RequestMethod para métodos, controllers para seguridad de tipos.

📋 Orden de ejecución

El orden en apply() es el de ejecución. Si un middleware no llama next(), la cadena se corta.

🔄 Request Pipeline

Middleware → Guards → Interceptors (antes) → Pipes → Handler → Interceptors (después) → Exception Filters.

En el próximo post nos metemos con DTOs, Pipes y Validación. Vamos a blindar las entradas de nuestra API con class-validator, class-transformer, y el ValidationPipe global. Todo lo que entra a tu API sin validar es un vector de ataque, así que vamos a securizarlo.

EA, nos vemos en los bares!! 🍺


Pon a prueba lo aprendido

1. ¿Qué interfaz debe implementar un middleware basado en clase en NestJS?

2. ¿Qué pasa si un middleware no llama a next()?

3. ¿Cuál es el orden correcto del pipeline de una request en NestJS?

4. ¿Qué tipo de middleware acepta app.use() en main.ts?

5. ¿Qué ventaja tiene un middleware de clase sobre uno funcional?