À propos de ce cours
Objectifs de la leçon
- Citer dans l'ordre les étapes traversées par une requête.
- Connaître l'ordre entre niveaux global, contrôleur et route.
- Comprendre le chemin d'une requête qui échoue.
- Choisir le bon enrichisseur pour un besoin donné.
Le trajet d'une requête
Middleware, guards, intercepteurs, pipes, contrôleur, puis intercepteurs au retour ; en cas d'erreur, les filtres d'exception.
- Middleware: premières fonctions exécutées, sur la requête brute, avant même que NestJS ait choisi la route.
- Guards: décident si la requête peut continuer (
true) ou non. - Intercepteurs (avant): code exécuté juste avant le contrôleur.
- Pipes: transforment et valident chaque paramètre de la méthode.
- Contrôleur (qui appelle le service) : produit le résultat.
- Intercepteurs (après): peuvent transformer le résultat, dans l'ordre inverse.
- En cas d'exception à n'importe quelle étape après le middleware : filtres d'exception, qui construisent la réponse d'erreur.
La démonstration
Pour observer cet ordre, construisons une application où chaque étape écrit une ligne dans le terminal. Ne cherchez pas encore à maîtriser l'écriture de ces classes (chapitres 08 et 09) : concentrez-vous sur le résultat.
📄 src/etapes.ts
import {
ArgumentMetadata, ArgumentsHost, CallHandler, CanActivate, Catch, ExceptionFilter,
ExecutionContext, HttpException, Injectable, NestInterceptor, NestMiddleware, PipeTransform,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { Observable, tap } from 'rxjs';
// ① Middleware : reçoit la requête brute (Express)
@Injectable()
export class JournalMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`① middleware ${req.method} ${req.originalUrl}`);
next();
}
}
// ② Guard : autorise ou refuse l'accès
@Injectable()
export class TraceGuard implements CanActivate {
constructor(private readonly niveau: string) {}
canActivate(context: ExecutionContext): boolean {
console.log(`② guard ${this.niveau}`);
return true;
}
}
// ③ et ⑥ Intercepteur : avant ET après le contrôleur
@Injectable()
export class TraceInterceptor implements NestInterceptor {
constructor(private readonly niveau: string) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
console.log(`③ intercepteur ${this.niveau} (avant)`);
return next.handle().pipe(tap(() => console.log(`⑥ intercepteur ${this.niveau} (après)`)));
}
}
// ④ Pipe : transforme / valide chaque paramètre
@Injectable()
export class TracePipe implements PipeTransform {
constructor(private readonly niveau: string) {}
transform(value: unknown, metadata: ArgumentMetadata) {
console.log(`④ pipe ${this.niveau} sur « ${metadata.data ?? metadata.type} »`);
return value;
}
}
// Filtre d'exception : formate les erreurs
@Catch(HttpException)
export class TraceFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
console.log(`✗ filtre d'exception : ${exception.message}`);
host.switchToHttp().getResponse<Response>().status(exception.getStatus())
.json({ statusCode: exception.getStatus(), message: exception.message });
}
}
Le contrôleur applique un guard, un intercepteur et un pipe au niveau du contrôleur, d'autres au niveau de la route, et un pipe sur le paramètre:
📄 src/stagiaires.controller.ts
import {
Controller, Get, NotFoundException, Param, UseGuards, UseInterceptors, UsePipes,
} from '@nestjs/common';
import { TraceGuard, TraceInterceptor, TracePipe } from './etapes';
@Controller('stagiaires')
@UseGuards(new TraceGuard('contrôleur'))
@UseInterceptors(new TraceInterceptor('contrôleur'))
@UsePipes(new TracePipe('contrôleur'))
export class StagiairesController {
@Get(':id')
@UseGuards(new TraceGuard('route'))
@UseInterceptors(new TraceInterceptor('route'))
findOne(@Param('id', new TracePipe('paramètre')) id: string) {
console.log(`⑤ contrôleur : findOne(${id})`);
if (id === '99') throw new NotFoundException(`Stagiaire ${id} introuvable`);
return { id, nom: 'Ben Ali' };
}
}
Le middleware est branché dans le module ; le niveau global est configuré dans main.ts:
📄 src/app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { JournalMiddleware } from './etapes';
import { StagiairesController } from './stagiaires.controller';
@Module({ controllers: [StagiairesController] })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(JournalMiddleware).forRoutes('*');
}
}
// extrait de main.ts
app.useGlobalGuards(new TraceGuard('global'));
app.useGlobalInterceptors(new TraceInterceptor('global'));
app.useGlobalPipes(new TracePipe('global'));
app.useGlobalFilters(new TraceFilter());
Voici ce qu'affiche réellement le terminal pour deux requêtes :
Terminal :
===== GET /stagiaires/3 =====
① middleware GET /stagiaires/3
② guard global
② guard contrôleur
② guard route
③ intercepteur global (avant)
③ intercepteur contrôleur (avant)
③ intercepteur route (avant)
④ pipe global sur « id »
④ pipe contrôleur sur « id »
④ pipe paramètre sur « id »
⑤ contrôleur : findOne(3)
⑥ intercepteur route (après)
⑥ intercepteur contrôleur (après)
⑥ intercepteur global (après)
→ réponse 200 {"id":"3","nom":"Ben Ali"}
===== GET /stagiaires/99 =====
① middleware GET /stagiaires/99
② guard global
② guard contrôleur
② guard route
③ intercepteur global (avant)
③ intercepteur contrôleur (avant)
③ intercepteur route (avant)
④ pipe global sur « id »
④ pipe contrôleur sur « id »
④ pipe paramètre sur « id »
⑤ contrôleur : findOne(99)
✗ filtre d'exception : Stagiaire 99 introuvable
→ réponse 404 {"statusCode":404,"message":"Stagiaire 99 introuvable"}
Ce qu'il faut retenir de cette trace
- L'ordre des types est fixe : middleware → guards → intercepteurs → pipes → contrôleur.
- Pour un même type, l'ordre des niveaux va du plus large au plus précis : global → contrôleur → route (→ paramètre pour les pipes).
- Au retour, les intercepteurs s'exécutent dans l'ordre inverse (route → contrôleur → global), comme des poupées russes.
- Quand le contrôleur lève une exception, les intercepteurs « après » ne s'exécutent pas pour ce résultat : la main passe au filtre d'exception. Sans filtre personnalisé, le filtre intégré de NestJS produit la réponse
{ message, error, statusCode }que vous avez déjà vue. - Les filtres, eux, sont cherchés du plus précis au plus large : route → contrôleur → global.
| Étape | Ordre des niveaux |
|---|---|
| Middleware | global (app.use) puis modules (configure) |
| Guards, intercepteurs (avant), pipes | global → contrôleur → route (→ paramètre) |
| Intercepteurs (après) | route → contrôleur → global |
| Filtres d'exception | route → contrôleur → global (le premier qui correspond traite l'erreur) |
Quand une étape refuse la requête
Si un guard renvoie false, rien de ce qui suit n'est exécuté : ni intercepteur, ni pipe, ni contrôleur. NestJS répond 403. De même, un pipe qui rejette une donnée (par exemple ParseIntPipe sur « abc ») arrête la requête avec un 400 avant le contrôleur. Ce découpage garantit que votre contrôleur ne reçoit que des requêtes autorisées et des données valides.
Choisir le bon outil
| Besoin | Outil | Pourquoi |
|---|---|---|
| Journaliser toutes les requêtes, même vers une route inconnue | Middleware | exécuté avant le routage |
| Vérifier l'identité ou le rôle | Guard | connaît la route visée (et ses métadonnées) |
| Convertir / valider les paramètres | Pipe | agit paramètre par paramètre |
| Mesurer la durée, formater ou mettre en cache la réponse | Intercepteur | entoure le contrôleur (avant et après) |
| Uniformiser les réponses d'erreur | Filtre d'exception | reçoit toutes les exceptions |
Activités
Activité 1 Reproduire la trace
Créez un projet de test (nest new cycle-requete --skip-git), recopiez les fichiers de la leçon et vérifiez la trace. Ajoutez ensuite un second paramètre @Query('detail') detail: string à findOne: combien de lignes « ④ pipe » obtenez-vous pour une requête ?
Voir le corrigé
Cinq lignes, réellement obtenues dans cet ordre :
④ pipe global sur « detail »
④ pipe global sur « id »
④ pipe contrôleur sur « detail »
④ pipe contrôleur sur « id »
④ pipe paramètre sur « id »
Les pipes global et contrôleur s'appliquent à chaque paramètre de la méthode ; le pipe déclaré dans @Param('id', …) seulement à id. L'ordre des niveaux (global → contrôleur → paramètre) est respecté. L'essentiel à retenir : les pipes travaillent paramètre par paramètre.
Activité 2 Un guard qui refuse
Que se passe-t-il exactement quand un guard renvoie false? Écrivez un guard FermeGuard qui renvoie toujours false, appliquez-le à une route et relevez le statut, le corps de la réponse et ce qui s'affiche dans le terminal.
Voir le corrigé
import { CanActivate, Controller, Get, Injectable, Module, UseGuards } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
@Injectable()
class FermeGuard implements CanActivate {
canActivate() {
console.log('guard : refus');
return false;
}
}
@Controller('test')
class TestController {
@Get()
@UseGuards(FermeGuard)
test() {
console.log('contrôleur : jamais exécuté');
return 'ok';
}
}
Résultat :
guard : refus
403 {"message":"Forbidden resource","error":"Forbidden","statusCode":403}
Le contrôleur n'est jamais exécuté. Pour un message plus explicite, le guard peut lever lui-même une exception (throw new ForbiddenException('…')), comme le RolesGuard de la leçon 5.4.
Activité 3 Prédire l'ordre
Un intercepteur A est global, un intercepteur B est appliqué au contrôleur, un intercepteur C à la route. Chacun affiche « avant X » et « après X ». Le contrôleur affiche « action ». Donnez l'affichage complet. Que change le fait de déplacer B au niveau global (enregistré après A) ?
Voir le corrigé
avant A, avant B, avant C, action, après C, après B, après A. Si B devient global et est enregistré après A (app.useGlobalInterceptors(A, B)), l'ordre reste le même : A puis B, car les globaux sont exécutés dans l'ordre d'enregistrement.
Activité 4 Le bon outil
Pour chaque besoin, choisissez l'outil et le niveau (global, contrôleur, route) : (a) refuser les requêtes sans clé d'API sur toute l'application ; (b) transformer :id en nombre sur GET /stagiaires/:id; (c) ajouter un en-tête X-Temps-Reponse à toutes les réponses ; (d) réserver toutes les routes de AdminController aux administrateurs ; (e) ajouter un identifiant unique à chaque requête dès son arrivée, pour le retrouver dans les journaux.
Voir le corrigé
(a) guard global ; (b) pipe sur le paramètre (ParseIntPipe) ; (c) intercepteur global ; (d) guard au niveau du contrôleur ; (e) middleware (le plus tôt possible, avant le routage).
Activité 5 Erreur dans un pipe
Dans la démonstration, modifiez TracePipe('paramètre') pour qu'il lève new BadRequestException('Identifiant invalide') si la valeur n'est pas un nombre. Envoyez GET /stagiaires/abc. Quelles lignes s'affichent ? Le contrôleur est-il appelé ? Quel statut reçoit le client ?
Voir le corrigé
Middleware, trois guards, trois intercepteurs (avant), pipes global et contrôleur, puis le pipe de paramètre lève l'exception : le contrôleur n'est pas appelé, aucun intercepteur « après » ne s'affiche, et le filtre affiche « ✗ filtre d'exception : Identifiant invalide ». Le client reçoit 400. C'est exactement le rôle de ParseIntPipe (chapitre 08).
À retenir
- Ordre : middleware → guards → intercepteurs (avant) → pipes → contrôleur → intercepteurs (après).
- Pour un même type : global → contrôleur → route ; les intercepteurs « après » en sens inverse.
- Une exception court-circuite la suite et passe par les filtres d'exception.
- Un guard qui renvoie false produit 403 ; un pipe qui rejette produit 400 : le contrôleur n'est pas appelé.
- Chaque outil a sa place : journal (middleware), accès (guard), données (pipe), réponse (intercepteur), erreurs (filtre).