Interceptors, Filters & Middleware

Contrôler le cycle de vie complet d'une requête

NestJS offre plusieurs mécanismes pour intercepter et modifier les requêtes et réponses à différents points du pipeline : Middleware → Guards → Interceptors → Pipes → Handler → Exception Filters.

Pipeline d'exécution NestJS

Requête HTTP
     ↓
1. Middleware       (Express/Fastify level — avant NestJS)
     ↓
2. Guards          (Authentification, autorisation)
     ↓
3. Interceptors    (Avant le handler — logging, cache...)
     ↓
4. Pipes           (Transformation, validation des paramètres)
     ↓
5. Handler         (Méthode du controller)
     ↓
6. Interceptors    (Après le handler — transformation réponse)
     ↓
7. Exception Filter (Capture les exceptions non gérées)
     ↓
Réponse HTTP

Interceptors

Un interceptor peut transformer la requête/réponse, mesurer les performances, ou ajouter un comportement avant/après le handler.

Interceptor de transformation de réponse

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

export interface ApiResponse<T> {
  data: T;
  statusCode: number;
  message: string;
  timestamp: string;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
    const ctx = context.switchToHttp();
    const response = ctx.getResponse();

    return next.handle().pipe(
      map(data => ({
        data,
        statusCode: response.statusCode,
        message: 'Success',
        timestamp: new Date().toISOString(),
      }))
    );
  }
}

// Enregistrement global
app.useGlobalInterceptors(new TransformInterceptor());

Interceptor de logging des performances

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

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        const ms = Date.now() - start;
        this.logger.log(`${method} ${url} — ${ms}ms`);
      }),
      catchError(err => {
        const ms = Date.now() - start;
        this.logger.error(`${method} ${url} — ${ms}ms — ERROR: ${err.message}`);
        throw err;
      }),
    );
  }
}

Interceptor de timeout

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),  // Timeout de 5 secondes
      catchError(err => {
        if (err instanceof TimeoutError) {
          throw new RequestTimeoutException('La requête a expiré');
        }
        throw err;
      }),
    );
  }
}

Opérateurs RxJS dans les interceptors

import { map, tap, catchError, timeout, retry, switchMap } from 'rxjs/operators';
import { of, throwError } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private cacheService: CacheService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = request.url;

    return from(this.cacheService.get(cacheKey)).pipe(
      switchMap(cached => {
        if (cached) return of(cached);  // Retourne le cache immédiatement
        return next.handle().pipe(
          tap(data => this.cacheService.set(cacheKey, data, 300)),
        );
      }),
    );
  }
}

Exception Filters

Les exception filters interceptent toutes les exceptions non gérées et retournent une réponse JSON uniforme.

Filter global HTTP

import {
  ExceptionFilter, Catch, ArgumentsHost,
  HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    const body = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: typeof exceptionResponse === 'object'
        ? (exceptionResponse as any).message
        : exceptionResponse,
    };

    this.logger.error(`${request.method} ${request.url} — ${status}`);
    response.status(status).json(body);
  }
}

// Catch-all filter
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      message: 'Internal server error',
    });
  }
}

// Enregistrement global
app.useGlobalFilters(new HttpExceptionFilter());

Middleware

Le middleware s'exécute avant le cycle de vie NestJS. Il a accès aux objets req, res et next (comme Express).

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

    res.on('finish', () => {
      const ms = Date.now() - start;
      console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} ${res.statusCode} — ${ms}ms — ${ip}`);
    });

    next();
  }
}

// app.module.ts — application du middleware
@Module({ ... })
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');  // Toutes les routes

    consumer
      .apply(AuthMiddleware)
      .forRoutes(
        { path: 'users', method: RequestMethod.GET },
        UsersController,  // Ou un controller entier
      );

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

Logger NestJS

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

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

  async create(dto: CreateUserDto): Promise<User> {
    this.logger.log(`Creating user: ${dto.email}`);
    try {
      const user = await this.userRepo.save(this.userRepo.create(dto));
      this.logger.log(`User created: ${user.id}`);
      return user;
    } catch (err) {
      this.logger.error(`Failed to create user: ${err.message}`, err.stack);
      throw err;
    }
  }
}

// Niveaux : log, error, warn, debug, verbose
// Configuration dans main.ts
const app = await NestFactory.create(AppModule, {
  logger: process.env.NODE_ENV === 'production'
    ? ['error', 'warn', 'log']
    : ['error', 'warn', 'log', 'debug', 'verbose'],
});

Cache avec @nestjs/cache-manager

npm install @nestjs/cache-manager cache-manager
# Pour Redis : npm install cache-manager-redis-store @types/cache-manager
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      ttl: 5000,    // millisecondes
      max: 100,     // Nombre max d'items en cache
      isGlobal: true,
    }),
  ],
})
export class AppModule {}

// Utilisation dans un controller
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';

@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
  @Get()
  @CacheKey('all-products')
  @CacheTTL(60)  // 60 secondes
  findAll() {
    return this.productsService.findAll();
  }
}

// Invalidation manuelle du cache
@Injectable()
export class ProductsService {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async update(id: number, dto: UpdateProductDto) {
    const product = await this.productRepo.save({ id, ...dto });
    await this.cacheManager.del('all-products');  // Invalide le cache
    return product;
  }
}
✏️ Exercices du module ▶ Mini-projet Module 08 →