En esta página

Interceptores y middleware

12 min lectura TextoCap. 4 — Seguridad y comunicación

El pipeline completo de NestJS

Antes de entrar en interceptores y middleware, es importante entender el orden en que se ejecutan todos los elementos del pipeline de NestJS para una petición HTTP entrante:

Petición HTTP entrante
    ↓
1. Middleware (Express)
    ↓
2. Guards (CanActivate)
    ↓
3. Interceptores (pre-handler)
    ↓
4. Pipes (transformación y validación)
    ↓
5. Controlador (handler)
    ↓
6. Interceptores (post-handler)
    ↓
7. Exception Filters (si hay error)
    ↓
Respuesta HTTP saliente

Los interceptores son únicos porque envuelven tanto la ejecución pre-handler como la post-handler, lo que los hace perfectos para lógica que necesita "antes y después" del controlador.

¿Qué es un interceptor?

Un interceptor implementa la interfaz NestInterceptor con el método intercept. Este método recibe el ExecutionContext y un CallHandler, y debe retornar un Observable:

import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';

export class MiInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    console.log('Antes del handler');

    return next.handle().pipe(
      // aquí puedes transformar, filtrar o hacer side effects
      // con los operadores de RxJS
    );
  }
}

next.handle() retorna el Observable que representa la ejecución del método del controlador. Puedes manipularlo con operadores RxJS.

Interceptor de caché de respuestas

import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler
} from '@nestjs/common';
import { Observable, of, tap } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  private readonly cache = new Map<string, { data: unknown; expira: number }>();
  private readonly TTL_MS = 30_000; // 30 segundos

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const req = context.switchToHttp().getRequest<{ url: string; method: string }>();

    if (req.method !== 'GET') {
      return next.handle();
    }

    const cacheKey = req.url;
    const entrada = this.cache.get(cacheKey);

    if (entrada && entrada.expira > Date.now()) {
      return of(entrada.data); // retorna desde caché
    }

    return next.handle().pipe(
      tap((datos: unknown) => {
        this.cache.set(cacheKey, {
          data: datos,
          expira: Date.now() + this.TTL_MS,
        });
      }),
    );
  }
}

Interceptor de manejo de excepciones personalizado

import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
  HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import { Observable, catchError, throwError } from 'rxjs';

@Injectable()
export class ErrorInterceptor implements NestInterceptor {
  private readonly logger = new Logger(ErrorInterceptor.name);

  intercept(_ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(
      catchError((error: unknown) => {
        if (error instanceof HttpException) {
          // Re-lanza excepciones HTTP tal como están
          return throwError(() => error);
        }

        // Loguea errores inesperados y los convierte en 500
        this.logger.error(
          'Error inesperado',
          error instanceof Error ? error.stack : String(error)
        );

        return throwError(() =>
          new HttpException(
            'Error interno del servidor',
            HttpStatus.INTERNAL_SERVER_ERROR,
          )
        );
      }),
    );
  }
}

Aplicando interceptores

// Nivel de método
@Get(':id')
@UseInterceptors(CacheInterceptor)
findOne(@Param('id') id: string) {}

// Nivel de controlador
@Controller('productos')
@UseInterceptors(LoggingInterceptor)
export class ProductosController {}

// Nivel global — en main.ts
app.useGlobalInterceptors(
  new LoggingInterceptor(),
  new TransformResponseInterceptor(),
  new TimeoutInterceptor(10_000),
);

// Nivel global con DI — en app.module.ts (recomendado para inyectar deps)
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
    { provide: APP_INTERCEPTOR, useClass: TransformResponseInterceptor },
  ],
})
export class AppModule {}

Middleware de NestJS

Los middlewares en NestJS son equivalentes a los middlewares de Express: funciones que se ejecutan en la cadena de petición-respuesta antes de llegar al sistema de NestJS.

Middleware funcional (más liviano)

// common/middleware/correlation-id.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

export function correlationIdMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
): void {
  const correlationId = req.headers['x-correlation-id'] as string ?? uuidv4();
  req.headers['x-correlation-id'] = correlationId;
  res.set('X-Correlation-Id', correlationId);
  next();
}

Middleware de rate limiting manual

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

@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  private readonly intentos = new Map<string, { count: number; resetEn: number }>();
  private readonly MAX_INTENTOS = 100;
  private readonly VENTANA_MS = 60_000; // 1 minuto

  use(req: Request, _res: Response, next: NextFunction): void {
    const ip = req.ip ?? 'unknown';
    const ahora = Date.now();
    const entrada = this.intentos.get(ip);

    if (!entrada || ahora > entrada.resetEn) {
      this.intentos.set(ip, { count: 1, resetEn: ahora + this.VENTANA_MS });
      return next();
    }

    if (entrada.count >= this.MAX_INTENTOS) {
      throw new TooManyRequestsException(
        `Límite de ${this.MAX_INTENTOS} peticiones por minuto excedido`
      );
    }

    entrada.count++;
    next();
  }
}

Aplicando middleware con NestModule

@Module({
  imports: [UsuariosModule, ProductosModule, AuthModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer): void {
    consumer
      .apply(correlationIdMiddleware)
      .forRoutes('*');

    consumer
      .apply(RateLimitMiddleware)
      .forRoutes({ path: 'auth/*', method: RequestMethod.POST });
  }
}

Con interceptores y middleware tienes control completo sobre el ciclo de vida de cada petición en tu aplicación NestJS. En la próxima lección exploraremos una funcionalidad completamente diferente: los WebSockets para comunicación en tiempo real.

Interceptores vs Middleware — ¿cuándo usar cada uno?
Los middleware se ejecutan antes del pipeline de NestJS y no tienen acceso al contexto de ejecución (no saben qué controlador o método se llamará). Los interceptores se ejecutan dentro del pipeline y tienen acceso completo al contexto. Usa middleware para lógica HTTP genérica (CORS, rate limiting, body parsing) e interceptores para lógica específica de NestJS (transformar respuestas, caché, logging con contexto de método).
Cache con interceptor y Redis
Puedes implementar caché de respuestas como interceptor. El interceptor verifica si la respuesta existe en Redis, la sirve directamente si existe, o llama al handler y guarda el resultado en Redis si no existe. Combina esto con un decorador `@CacheTTL(60)` para configurar el tiempo de caché por ruta.