Exercices — Module 07
Interceptors, Filters & Middleware · 5 exercices pratiques
Interceptor de réponse uniforme
Créez un interceptor global qui enveloppe toutes les réponses dans le format : { data, statusCode, message, timestamp, path }.
Voir la solution
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
map(data => ({
statusCode: response.statusCode,
message: 'OK',
timestamp: new Date().toISOString(),
path: request.url,
data,
})),
);
}
}
Exception Filter global
Créez un AllExceptionsFilter qui formate uniformément toutes les erreurs et les log avec le Logger NestJS. Le format : { statusCode, message, error, timestamp, path }.
Voir la solution
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse<Response>();
const req = ctx.getRequest<Request>();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionRes = exception instanceof HttpException
? exception.getResponse()
: null;
const message = exceptionRes
? (typeof exceptionRes === 'string' ? exceptionRes : (exceptionRes as any).message)
: 'Internal server error';
const body = {
statusCode: status,
message: Array.isArray(message) ? message : [message],
error: exception instanceof HttpException ? exception.name : 'InternalServerError',
timestamp: new Date().toISOString(),
path: req.url,
};
this.logger.error(`${req.method} ${req.url} — ${status}: ${JSON.stringify(message)}`);
if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.error(exception instanceof Error ? exception.stack : String(exception));
}
res.status(status).json(body);
}
}
Middleware de rate limiting
Créez un middleware de rate limiting simple (sans bibliothèque) qui limite les requêtes à 100/minute par IP. Retournez 429 Too Many Requests si la limite est dépassée.
Voir la solution
@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
private readonly store = new Map<string, { count: number; resetAt: number }>();
private readonly limit = 100;
private readonly windowMs = 60 * 1000;
use(req: Request, res: Response, next: NextFunction) {
const ip = req.ip || req.headers['x-forwarded-for'] as string || '0.0.0.0';
const now = Date.now();
const entry = this.store.get(ip);
if (!entry || entry.resetAt < now) {
this.store.set(ip, { count: 1, resetAt: now + this.windowMs });
res.setHeader('X-RateLimit-Limit', this.limit);
res.setHeader('X-RateLimit-Remaining', this.limit - 1);
return next();
}
if (entry.count >= this.limit) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
res.setHeader('Retry-After', retryAfter);
res.setHeader('X-RateLimit-Remaining', 0);
return res.status(429).json({
statusCode: 429,
message: `Rate limit exceeded. Try again in ${retryAfter}s`,
});
}
entry.count++;
res.setHeader('X-RateLimit-Remaining', this.limit - entry.count);
next();
}
}
Interceptor de déduplication
Créez un interceptor qui détecte les requêtes POST dupliquées (même idempotency key en header) et retourne la réponse mise en cache plutôt que de retraiter.
Voir la solution
@Injectable()
export class IdempotencyInterceptor implements NestInterceptor {
private readonly cache = new Map<string, any>();
private readonly TTL = 24 * 60 * 60 * 1000; // 24h
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
if (req.method !== 'POST') return next.handle();
const key = req.headers['idempotency-key'];
if (!key) return next.handle();
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return of(cached.response);
}
return next.handle().pipe(
tap(response => {
this.cache.set(key, {
response,
expiresAt: Date.now() + this.TTL,
});
}),
);
}
}
Audit log interceptor
Créez un interceptor d'audit qui enregistre en base de données toutes les mutations (POST, PUT, PATCH, DELETE) avec : userId, method, path, body, statusCode, duration, timestamp.
Voir la solution
@Injectable()
export class AuditInterceptor implements NestInterceptor {
constructor(private auditService: AuditService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
const MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (!MUTATING_METHODS.includes(req.method)) return next.handle();
const start = Date.now();
const userId = req.user?.id;
return next.handle().pipe(
tap(() => {
this.auditService.log({
userId,
method: req.method,
path: req.path,
body: this.sanitizeBody(req.body),
statusCode: res.statusCode,
duration: Date.now() - start,
ip: req.ip,
}).catch(err => console.error('Audit log failed:', err));
}),
);
}
private sanitizeBody(body: any): any {
if (!body) return null;
const { password, token, refreshToken, ...safe } = body;
return safe;
}
}