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.

1. ¿Qué es un Middleware?
Un 💡 Middleware Función que se ejecuta ANTES de que la request llegue al route handler (controller). Tiene acceso al objeto Request, Response y a la función next() para pasar el control al siguiente middleware o al handler. Mismo concepto que en Express. Más info → 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:
req— El objeto Request, que es la petición del cliente.res— El objeto Response, la respuesta que se enviará al cliente.next()— Una función que, al llamarla, pasa el control al siguiente middleware o al route handler.
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 💡 NestMiddleware Interfaz de NestJS que define el contrato para un middleware basado en clase. Exige implementar un método use(req, res, next) que recibe los objetos Request, Response y la función next. Más info → :
// 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í:
@Injectable()— Un middleware de clase es un provider más. Esto significa que puede inyectar dependencias. ¿Necesitas unConfigServicepara leer un header configurable? Inyéctalo en el constructor.implements NestMiddleware— Te obliga a implementar el métodouse(), TypeScript te pega si no lo haces.res.on('finish')— Escuchamos el eventofinishdel response para loguear cuando la respuesta realmente se envía, incluyendo el status code y la duración.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).
- 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
- 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 💡 MiddlewareConsumer Clase helper de NestJS que proporciona métodos fluidos (apply, forRoutes, exclude) para configurar qué middleware se aplica a qué rutas. Se recibe como parámetro en el método configure() del módulo.
Más info →
:
// 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:
- El módulo implementa la interfaz
NestModule, que exige un métodoconfigure(). configure()recibe unMiddlewareConsumer..apply(LoggerMiddleware)— Qué middleware aplicar..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: CorsMiddleware → LoggerMiddleware → AuthHeaderMiddleware → 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.
- 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()
- 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?
- Si el cliente manda un header
x-correlation-id, lo reutiliza. Si no, genera uno conrandomUUID(). - Lo mete en
req.headerspara que cualquier service o controller de la cadena pueda acceder al ID. - Lo añade como header de respuesta para que el cliente pueda correlacionar su petición con los logs.
- 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('*');
}
}
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:

Cada pieza tiene su responsabilidad, y es aquí donde igual es fácil de confundirse para que debemos usar cada cosa:
| Capa | Responsabilidad | Ejemplo |
|---|---|---|
| Middleware | Lógica transversal genérica. Acceso directo a req/res. Ejecuta antes que todo lo demás | Logging, correlation IDs, helmet, CORS, body parsing |
| Guards | Decidir si la request puede o no pasar. Devuelven true/false | Autenticación JWT, autorización por roles, rate limiting |
| Interceptors (antes) | Transformar la request antes del handler o añadir lógica envolvente | Logging de entrada, cache check, timeout |
| Pipes | Validar y transformar los parámetros de entrada del handler | Validar DTOs, parsear IDs a números, sanitizar strings |
| Handler | Tu lógica de negocio. El método del controller que procesa la request | findAll(), create(), update(), remove() |
| Interceptors (después) | Transformar la respuesta del handler antes de enviarla | Serializar, envolver en formato estándar, logging de salida |
| Exception Filters | Capturar excepciones y convertirlas en respuestas HTTP adecuadas | Convertir 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:
- ¿Necesitas acceso a
reqyresde Express? → Middleware. Es la única capa con acceso directo al request/response de Express. - ¿Necesitas decidir si una ruta es accesible? → Guard. Es la capa diseñada para eso, no un middleware.
- ¿Necesitas validar un DTO? → Pipe. No un middleware. Los pipes se ejecutan justo antes del handler y conocen el parámetro concreto que deben validar.
- ¿Necesitas transformar la respuesta? → Interceptor. Tiene acceso al Observable que devuelve el handler.
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:
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.
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
@Injectable() + implements NestMiddleware. Puede inyectar dependencias. Se configura con MiddlewareConsumer.
Función (req, res, next). Sin DI. Más ligero para lógica simple. Obligatorio para app.use().
API fluida: apply().forRoutes().exclude(). Acepta strings, objetos {path, method} o controllers.
Controla dónde se aplica. Strings para rutas, RequestMethod para métodos, controllers para seguridad de tipos.
El orden en apply() es el de ejecución. Si un middleware no llama next(), la cadena se corta.
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?