7. Le cycle de vie d'une requête
Entre l'arrivée d'une requête et l'exécution de l'action, [NestJS] fait intervenir plusieurs types d'objets, chacun avec un rôle précis. Nous en avons déjà rencontré : les middlewares ([res.locals], [cookie-parser]), les pipes ([ParseIntPipe], [ValidationPipe]). L'étude de cas utilise les cinq :

Objet | Rôle | Dans l’étude de cas |
middleware | préparer la requête ; ne connaît pas encore l’action | lecture des cookies, langue, message flash |
garde | autoriser ou refuser l’accès à l’action | authentification (JWT), rôles |
intercepteur | du code avant et après l’action | l’utilisateur connecté, exposé aux vues |
pipe | convertir et valider un paramètre | [ParseIntPipe] |
filtre d'exceptions | transformer une exception en réponse | pages d’erreur HTML, redirection vers /login |
7.1. Exemple 24 : les cinq étapes
L'exemple 24 a une action, qui calcule le carré d'un nombre, et un objet de chaque type. Chacun écrit une ligne dans la console : on voit ainsi dans quel ordre ils interviennent.
Le fichier [src/journal.middleware.ts] :
| import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
// 1. le MIDDLEWARE : le premier à voir la requête. Il ne sait pas encore
// quelle action va la traiter
@Injectable()
export class JournalMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
console.log(`\n--- ${req.method} ${req.originalUrl}`);
console.log('1. middleware');
next(); // on passe la main à la suite de la chaîne
}
}
|
- ligne 7 : un middleware [NestJS] est une classe qui implémente [NestMiddleware]. Une simple fonction, comme dans les exemples précédents, convient aussi ;
- ligne 11 : le middleware doit appeler [next()] pour passer la main à la suite (ou envoyer lui-même une réponse). Sinon, la requête reste bloquée.
Le fichier [src/app.module.ts] :
| import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { CalculController } from './calcul.controller';
import { JournalMiddleware } from './journal.middleware';
@Module({
controllers: [CalculController],
})
export class AppModule implements NestModule {
// un middleware de classe s'enregistre dans le module
configure(consumer: MiddlewareConsumer): void {
consumer.apply(JournalMiddleware).forRoutes('*');
}
}
|
lignes 10-12 : un middleware de classe s'enregistre dans un module qui implémente [NestModule]. [forRoutes('*')] l'applique à toutes les routes.
Le fichier [src/bloquer.guard.ts] :
| import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';
// 2. la GARDE : décide si la requête a le droit d'atteindre l'action.
// false -> NestJS lève une ForbiddenException (403)
@Injectable()
export class BloquerGuard implements CanActivate {
canActivate(contexte: ExecutionContext): boolean {
const req = contexte.switchToHttp().getRequest<Request>();
// la garde connaît l'action visée (ce que le middleware ignorait)
console.log(`2. garde (action visée : ${contexte.getHandler().name})`);
return req.query.bloquer !== '1';
}
}
|
- ligne 7 : une garde implémente [CanActivate] ;
- ligne 8 : sa méthode [canActivate] reçoit le contexte d'exécution, et rend [true] (la requête continue) ou [false] ([NestJS] lève une [ForbiddenException], code 403) ;
- ligne 11 : contrairement au middleware, la garde connaît l'action visée ([getHandler()]) et son contrôleur ([getClass()]). C'est ce qui permet à une garde de lire les décorateurs posés sur l'action (cf. [@Roles], chapitre suivant).
Le fichier [src/chrono.interceptor.ts] :
| import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
// 3. l'INTERCEPTEUR : du code AVANT l'action, et APRÈS elle
@Injectable()
export class ChronoInterceptor implements NestInterceptor {
intercept(contexte: ExecutionContext, suite: CallHandler): Observable<unknown> {
console.log('3. intercepteur (avant)');
const debut = Date.now();
// suite.handle() exécute la suite : pipes puis action. Le résultat
// de l'action arrive dans le flux (Observable) renvoyé
return suite.handle().pipe(tap(() => console.log(`6. intercepteur (après, ${Date.now() - debut} ms)`)));
}
}
|
- ligne 6 : un intercepteur implémente [NestInterceptor] ;
- ligne 8 : le code placé avant [suite.handle()] s'exécute avant l'action ;
- ligne 12 : [suite.handle()] exécute la suite (pipes puis action) et rend un flux [RxJS] ([Observable]) qui produira le résultat de l'action. L'opérateur [tap] exécute du code après l'action, sans modifier son résultat (l'opérateur [map] permettrait de le modifier).
Le fichier [src/entier.pipe.ts] :
| import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
// 4. le PIPE : convertit et valide un paramètre de l'action
@Injectable()
export class EntierPipe implements PipeTransform<string, number> {
transform(valeur: string): number {
console.log(`4. pipe (valeur reçue : "${valeur}")`);
const n = Number(valeur);
if (!Number.isInteger(n)) {
throw new BadRequestException(`"${valeur}" n'est pas un nombre entier`);
}
return n;
}
}
|
Un pipe semblable à [ParseIntPipe] (cf. exemple 12).
Le fichier [src/page-erreur.filter.ts] :
| import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
// x. le FILTRE D'EXCEPTIONS : reçoit toute exception non traitée, d'où
// qu'elle vienne (garde, pipe, action...), et fabrique la réponse.
// @Catch() sans argument : toutes les exceptions
@Catch()
export class PageErreurFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const res = host.switchToHttp().getResponse<Response>();
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException ? exception.message : 'Erreur interne';
console.log(`x. filtre d'exceptions (${status} : ${message})`);
// une page HTML plutôt que la réponse JSON par défaut de NestJS
res.status(status).render('erreur', { status, message });
}
}
|
- ligne 7 : [@Catch()] sans argument : le filtre reçoit toutes les exceptions. [@Catch(HttpException)] le limiterait aux exceptions HTTP ;
- ligne 9 : la méthode [catch] reçoit l'exception et un objet [host] qui donne accès à la requête et à la réponse ;
- ligne 11 : une [HttpException] porte son code de statut. Toute autre exception (une erreur de programmation, une base de données injoignable) est une erreur 500 ;
- ligne 15 : au lieu du JSON par défaut, le filtre affiche une page HTML, avec le bon code de statut.
Le fichier [src/main.ts] :
| import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { dirname, join } from 'path';
import { AppModule } from './app.module';
import { ChronoInterceptor } from './chrono.interceptor';
import { PageErreurFilter } from './page-erreur.filter';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { logger: ['error', 'warn'] });
app.useStaticAssets(join(dirname(require.resolve('bootstrap/package.json')), 'dist'), { prefix: '/bootstrap/' });
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
// un intercepteur et un filtre GLOBAUX : ils s'appliquent à toutes les routes
app.useGlobalInterceptors(new ChronoInterceptor());
app.useGlobalFilters(new PageErreurFilter());
await app.listen(3000);
console.log('Serveur démarré sur http://localhost:3000');
}
bootstrap();
|
- ligne 9 : on réduit les messages de [NestJS] aux erreurs et avertissements, pour mieux voir les nôtres ;
- lignes 14-15 : l'intercepteur et le filtre sont globaux : ils s'appliquent à toutes les actions.
Le fichier [src/calcul.controller.ts] :
| import { Controller, Get, Param, Render, UseGuards } from '@nestjs/common';
import { BloquerGuard } from './bloquer.guard';
import { EntierPipe } from './entier.pipe';
@Controller('carre')
@UseGuards(BloquerGuard) // la garde s'applique à toutes les actions du contrôleur
export class CalculController {
@Get(':n')
@Render('carre')
carre(@Param('n', EntierPipe) n: number): object {
console.log(`5. action carre(${n})`);
if (n === 13) {
throw new Error('13 porte malheur'); // une erreur "imprévue"
}
return { n, carre: n * n };
}
}
|
- ligne 6 : [@UseGuards] applique une garde à toutes les actions du contrôleur (on peut aussi le placer sur une seule action) ;
- ligne 10 : le pipe convertit le paramètre ;
- lignes 12-14 : une erreur « imprévue », pour voir le filtre à l'œuvre.
Quatre requêtes : un calcul normal, un paramètre invalide, une requête bloquée par la garde, et l'erreur imprévue. La console du serveur :
| --- GET /carre/7
1. middleware
2. garde (action visée : carre)
3. intercepteur (avant)
4. pipe (valeur reçue : "7")
5. action carre(7)
6. intercepteur (après, 2 ms)
--- GET /carre/abc
1. middleware
2. garde (action visée : carre)
3. intercepteur (avant)
4. pipe (valeur reçue : "abc")
x. filtre d'exceptions (400 : "abc" n'est pas un nombre entier)
--- GET /carre/7?bloquer=1
1. middleware
2. garde (action visée : carre)
x. filtre d'exceptions (403 : Forbidden resource)
--- GET /carre/13
1. middleware
2. garde (action visée : carre)
3. intercepteur (avant)
4. pipe (valeur reçue : "13")
5. action carre(13)
x. filtre d'exceptions (500 : Erreur interne)
|
On lit dans ce journal :
- l'ordre des étapes : middleware, garde, intercepteur (avant), pipe, action, intercepteur (après) ;
- une exception interrompt la chaîne, où qu'elle se produise, et c'est le filtre qui fabrique la réponse : la garde qui refuse (403), le pipe qui refuse (400), l'action qui échoue (500).
Les pages obtenues :




Le filtre d'exceptions de l'étude de cas ([web/html-exception.filter.ts]) suit ce modèle, avec un cas particulier : une erreur 401 (utilisateur non connecté) n'affiche pas de page d'erreur mais redirige vers la page de connexion.