Cours NestJS · Module 2: Routing et cycle de requête
Interceptors - le courrier qui revient avec la réponse
Dans cette leçon6
La sentinelle du poste avancé compte les entrants. Le garde du trésor décide qui entre. Tous deux achèvent leur service au même instant - lorsque le visiteur franchit le seuil. Ni l'un ni l'autre ne voit avec quoi il en ressort.
Le courrier, lui, le voit. Il porte un ordre au gouverneur, attend sa réponse et la rapporte - et chemin faisant, il peut y ajouter quelque chose. Un interceptor est le premier mécanisme du cycle de requête à exécuter du code également après la fin du handler, et cette seule phrase explique presque tout ce qu'il fait.
L'interface et la signature
Un interceptor est une classe implémentant l'interface NestInterceptor. Non CanActivate - celle-ci appartient aux guards de la leçon précédente ; non PipeTransform - ce sont les pipes, qui viennent juste après cette leçon ; non ExceptionFilter - celle-là s'occupe d'attraper les erreurs. L'interface réclame une seule méthode :
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3
4@Injectable()
5export class AuditInterceptor implements NestInterceptor {
6 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
7 return next.handle();
8 }
9}La signature comporte trois parties écrites dans cet ordre : intercept( ouvre la méthode, context: ExecutionContext, est le premier argument, et next: CallHandler) le second. L'interceptor ci-dessus ne fait encore rien - il transmet la requête et rend le résultat intact.
Tu connais déjà ExecutionContext par les guards : c'est le même objet, avec les mêmes méthodes getHandler() et switchToHttp(). La nouveauté est CallHandler - une prise sur le reste de la chaîne. Tant que tu n'appelles pas handle() dessus, le handler du contrôleur ne s'exécute pas du tout.
Quatre phases
Le travail d'un interceptor se divise en quatre étapes, toujours dans cet ordre :
- Appel de la méthode
intercept(). - Logique avant le passage de la requête (before).
- Exécution du gestionnaire de route (
next.handle()). - Traitement de la réponse (after).
La division saute aux yeux dans le code, et une seule frontière suffit à retenir : tout ce que tu écris avant return next.handle() relève de la phase before ; tout ce qui entre dans .pipe(...) relève de la phase after. Une même méthode couvre les deux trajets du courrier - l'aller et le retour.
C'est précisément pourquoi un interceptor sait mesurer la durée du traitement d'une requête, ce dont ni un middleware ni un guard ne sont capables : il pose un repère dans la phase before et le relit dans la phase after, au sein d'une seule méthode et sur la même variable.
tap - agir sans changer le résultat
next.handle() ne renvoie pas des données achevées mais un Observable - un flux de la bibliothèque RxJS, dans lequel la réponse arrivera. Les opérations sur le flux s'accrochent par la méthode .pipe(), et l'opérateur le plus souvent utilisé pour exécuter de la logique après avoir reçu une réponse est tap :
1@Injectable()
2export class LoggingInterceptor implements NestInterceptor {
3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4 const start = Date.now();
5
6 return next.handle().pipe(
7 tap(() => console.log(`Time: ${Date.now() - start}ms`)),
8 );
9 }
10}const start = Date.now() s'exécute dans la phase before, la fonction placée dans tap dans la phase after, une fois la réponse revenue. L'écart entre les deux relevés est la durée du traitement.
Le nom tap vient de la mise sur écoute : l'opérateur observe la valeur qui passe et la laisse filer sans la modifier. C'est exactement ce qu'il faut pour la journalisation - la réponse du client ne doit pas dépendre du fait que nous écrivions ou non dans la console. Ses trois voisins de RxJS font autre chose : map remplace la valeur, filter peut la retenir, et reduce replie tout un flux en une seule valeur en attendant sa fin - or dans une requête HTTP la valeur est unique, il n'y a donc rien à replier.
map - changer la forme de la réponse
Quand la réponse doit vraiment prendre un autre aspect, tu recours à map. L'usage typique est une enveloppe commune à tous les endpoints :
1@Injectable()
2export class TransformInterceptor implements NestInterceptor {
3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4 return next.handle().pipe(
5 map((data) => ({ success: true, data })),
6 );
7 }
8}Le contrôleur renvoie une liste de tributs et le client reçoit { success: true, data: [...] } - et de même à chaque autre endpoint, sans ajouter une seule ligne dans les contrôleurs. La différence avec l'exemple précédent tient en une phrase : tap regarde, map remplace. Si la fonction placée dans tap renvoie quelque chose, cela est ignoré ; ce que renvoie la fonction placée dans map devient la réponse.
RxJS compte d'autres opérateurs de ce genre, et certains servent d'emblée dans un interceptor : catchError attrape une exception levée par le handler, timeout interrompt un traitement trop long. Tous se branchent dans le même .pipe().
Le branchement
Un interceptor se branche par le décorateur @UseInterceptors - sur une méthode ou sur tout un contrôleur :
1@Controller('tributes')
2@UseInterceptors(LoggingInterceptor)
3export class TributesController {
4 @Get()
5 findAll() {
6 return this.tributesService.findAll();
7 }
8}Tu passes la classe, non son instance - la création revient à l'injection de dépendances, ce qui permet à un interceptor de recevoir ses propres dépendances dans le constructeur. Plusieurs interceptors séparés par des virgules forment une chaîne : la phase before suit l'ordre d'écriture, la phase after l'ordre inverse, car le flux revient par où il est parti.
Quand l'enveloppe doit couvrir toute l'application, tu enregistres l'interceptor globalement : app.useGlobalInterceptors(new TransformInterceptor()) dans main.ts, ou comme provider avec le token APP_INTERCEPTOR. La seconde voie vaut mieux partout où l'interceptor a lui-même besoin de quelque chose - un provider passe par l'injection de dépendances, new non.
Récapitulatif
Le courrier part et revient :
- un interceptor implémente l'interface
NestInterceptor- nonCanActivate, nonPipeTransform, nonExceptionFilter, - la signature dans l'ordre :
intercept(→context: ExecutionContext,→next: CallHandler), - quatre phases : appel de
intercept()→ logique avant le passage de la requête (before) → exécution du gestionnaire de route (next.handle()) → traitement de la réponse (after), - la frontière est simple : avant
return next.handle(), c'est la phase before ; à l'intérieur de.pipe(...), la phase after, next.handle()renvoie unObservable; sans cet appel, le handler ne s'exécute jamais,tapest l'opérateur le plus souvent utilisé pour la logique après réception de la réponse - il observe la valeur et la laisse passer sans la changer, à la différence demap(remplace),filter(retient) etreduce(replie le flux),- mesure du temps :
const start = Date.now()dans la phase before,tap(() => ...)avec un secondDate.now()dans la phase after, map((data) => ({ success: true, data }))enveloppe la réponse dans une enveloppe commune,- branchement par
@UseInterceptors(LoggingInterceptor), globalement paruseGlobalInterceptorsou le tokenAPP_INTERCEPTOR.
Dans la prochaine leçon, tu découvriras les pipes - le quatrième et dernier mécanisme du cycle, qui prend en charge les arguments individuels d'une méthode avant même qu'elle ne s'exécute. En attendant, retiens ceci : le middleware et le guard travaillent à l'aller ; seul l'interceptor fait l'aller et le retour.
Code de cette leçon : src/interceptors.ts
1// Intercepteurs dans NestJS - Courriers et diplomates de l'empire
2import {
3 Injectable, NestInterceptor, ExecutionContext, CallHandler,
4} from '@nestjs/common';
5import { Observable } from 'rxjs';
6import { tap, map, catchError } from 'rxjs/operators';
7
8console.log("Interceptors - interception et modification de la communication !");
9
10// ===========================================
11// 1. LoggingInterceptor - Le chroniqueur
12// ===========================================
13
14@Injectable()
15export class LoggingInterceptor implements NestInterceptor {
16 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
17 const request = context.switchToHttp().getRequest();
18 const method = request.method;
19 const url = request.url;
20 const now = Date.now();
21
22 console.log('[Before] ' + method + ' ' + url);
23
24 return next.handle().pipe(
25 tap(() => {
26 const duration = Date.now() - now;
27 console.log('[After] ' + method + ' ' + url + ' - ' + duration + 'ms');
28 }),
29 );
30 }
31}
32
33// ===========================================
34// 2. TransformInterceptor - Formatage de la réponse
35// ===========================================
36
37@Injectable()
38export class TransformInterceptor implements NestInterceptor {
39 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
40 return next.handle().pipe(
41 map(data => ({
42 success: true,
43 data,
44 timestamp: new Date().toISOString(),
45 imperium: 'Roma Aeterna',
46 })),
47 );
48 }
49}
50
51// ===========================================
52// 3. CacheInterceptor - Mémoire cache
53// ===========================================
54
55@Injectable()
56export class CacheInterceptor implements NestInterceptor {
57 private cache = new Map<string, { data: any; expiry: number }>();
58
59 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
60 const request = context.switchToHttp().getRequest();
61 const cacheKey = request.url;
62 const cached = this.cache.get(cacheKey);
63
64 if (cached && cached.expiry > Date.now()) {
65 console.log('[Cache] Hit: ' + cacheKey);
66 return new Observable(subscriber => {
67 subscriber.next(cached.data);
68 subscriber.complete();
69 });
70 }
71
72 console.log('[Cache] Miss: ' + cacheKey);
73 return next.handle().pipe(
74 tap(data => {
75 this.cache.set(cacheKey, {
76 data,
77 expiry: Date.now() + 60000, // 60 secondes
78 });
79 }),
80 );
81 }
82}
83
84// ===========================================
85// 4. ErrorInterceptor - Gestion des erreurs
86// ===========================================
87
88@Injectable()
89export class ErrorInterceptor implements NestInterceptor {
90 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
91 return next.handle().pipe(
92 catchError(err => {
93 console.error('[Error] ' + err.message);
94 throw err;
95 }),
96 );
97 }
98}
99
100console.log("\n=== RÉSUMÉ SUR LES INTERCEPTEURS ===");
101console.log("NestInterceptor - interface des intercepteurs");
102console.log("next.handle() - invocation du handler (Observable)");
103console.log("tap() - effets de bord (journalisation)");
104console.log("map() - transformation de la réponse");
105console.log("catchError() - capture des erreurs");
106Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle interface un intercepteur implémente-t-il dans NestJS ?
2. Quel opérateur RxJS est le plus souvent utilisé dans un intercepteur pour exécuter de la logique après avoir reçu une réponse ?
Tâches pratiques dans le jeu
- Tri vertical
Ordonne les phases d'exécution d'un intercepteur dans NestJS :
- Éditeur de code
Écris un LoggingInterceptor qui journalise le temps d'exécution de la requête en utilisant Date.now() avant et après next.handle()
- Tri horizontal
Range les éléments de la signature de la méthode intercept dans le bon ordre :
- Éditeur de code
Écris un TransformInterceptor qui utilise map() de RxJS pour envelopper la réponse dans { success: true, data: response }