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;
}
}