Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
NestJS

Le cycle de vie d'une requête dans NestJS

Cette leçon suit le trajet complet d'une requête dans une application NestJS : middleware, guards, intercepteurs avant le contrôleur, pipes, méthode du contrôleur et service, intercepteurs après le co...

0 chapitres 0 QCM Accès gratuit
Le cycle de vie d'une requête dans NestJS

À propos de ce cours

Cette leçon suit le trajet complet d'une requête dans une application NestJS : middleware, guards, intercepteurs avant le contrôleur, pipes, méthode du contrôleur et service, intercepteurs après le contrôleur, puis filtres d'exception en cas d'erreur. Une application de démonstration, dont chaque étape s'affiche dans le terminal, montre l'ordre réel d'exécution aux niveaux global, contrôleur, route et paramètre, pour une requête réussie et pour une requête en erreur. La leçon explique aussi comment choisir le bon enrichisseur selon le besoin.

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

Cycle de vie d'une requête NestJS

Middleware, guards, intercepteurs, pipes, contrôleur, puis intercepteurs au retour ; en cas d'erreur, les filtres d'exception.

  1. Middleware: premières fonctions exécutées, sur la requête brute, avant même que NestJS ait choisi la route.
  2. Guards: décident si la requête peut continuer (true) ou non.
  3. Intercepteurs (avant): code exécuté juste avant le contrôleur.
  4. Pipes: transforment et valident chaque paramètre de la méthode.
  5. Contrôleur (qui appelle le service) : produit le résultat.
  6. Intercepteurs (après): peuvent transformer le résultat, dans l'ordre inverse.
  7. 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.
ÉtapeOrdre des niveaux
Middlewareglobal (app.use) puis modules (configure)
Guards, intercepteurs (avant), pipesglobal → contrôleur → route (→ paramètre)
Intercepteurs (après)route → contrôleur → global
Filtres d'exceptionroute → 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
BesoinOutilPourquoi
Journaliser toutes les requêtes, même vers une route inconnueMiddlewareexécuté avant le routage
Vérifier l'identité ou le rôleGuardconnaît la route visée (et ses métadonnées)
Convertir / valider les paramètresPipeagit paramètre par paramètre
Mesurer la durée, formater ou mettre en cache la réponseIntercepteurentoure le contrôleur (avant et après)
Uniformiser les réponses d'erreurFiltre d'exceptionreç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).