Cours NestJS · Module 2: Routing et cycle de requête
Filtres d'exceptions - cours et tribunaux de l'empire
Dans cette leçon6
Un provincial demande un tribut absent du registre, et le serveur répond par un statut 500 et le message « Internal server error ». Le client ne sait pas s'il s'est trompé ou si la base de données est tombée, et toi, tu ne sais pas ce que tu lui as réellement envoyé. Le préteur Augustus, juge suprême de Rome, répète que chaque affaire doit arriver devant le bon tribunal et se conclure par un verdict clair. Dans NestJS, ces tribunaux sont les exception filters - des classes qui interceptent les exceptions et les transforment en réponses HTTP.
Que sont les filtres d'exceptions ?
Dans l'Empire romain, chaque type d'affaire avait son tribunal :
- Tribunal du préteur - affaires civiles (404 Not Found)
- Cour du censeur - contrôle d'accès (403 Forbidden)
- Tribunal militaire - infractions graves (500 Internal Error)
NestJS possède une couche d'exceptions intégrée qui joue le rôle de tribunal par défaut : elle intercepte toute exception non gérée. Tu écris ton propre filtre quand tu veux changer la formulation du verdict.
Les exceptions intégrées de NestJS
Avant de bâtir ta propre cour, découvre les verdicts tout prêts. Toutes les classes ci-dessous héritent de HttpException et fixent elles-mêmes le bon code de statut :
1import {
2 BadRequestException, // 400
3 UnauthorizedException, // 401
4 ForbiddenException, // 403
5 NotFoundException, // 404
6 ConflictException, // 409
7 InternalServerErrorException, // 500
8 HttpException, // exception HTTP de base
9} from '@nestjs/common';Les commentaires à côté des imports indiquent le code que recevra le client. Levons maintenant ces exceptions dans le contrôleur des tributs :
1@Controller('tributa')
2export class TributeController {
3 private tributes = [
4 { id: 1, name: 'Aurum Galliae', amount: 50000 },
5 { id: 2, name: 'Argentum Hispaniae', amount: 30000 },
6 ];
7
8 @Get(':id')
9 findTribute(@Param('id', ParseIntPipe) id: number) {
10 const tribute = this.tributes.find(t => t.id === id);
11 if (!tribute) {
12 // Throw 404 exception
13 throw new NotFoundException(
14 `Tributum ${id} non inventum! (Tribut introuvable)`
15 );
16 }
17 return tribute;
18 }
19
20 @Post()
21 @UseGuards(RolesGuard)
22 createTribute(@Body() dto: any) {
23 if (!dto.name || !dto.amount) {
24 // Throw 400 exception
25 throw new BadRequestException(
26 'Le tribut doit contenir un nom et un montant !'
27 );
28 }
29
30 const exists = this.tributes.find(t => t.name === dto.name);
31 if (exists) {
32 // Throw 409 exception
33 throw new ConflictException(
34 `Tribute named "${dto.name}" already exists!`
35 );
36 }
37
38 const newTribute = { id: this.tributes.length + 1, ...dto };
39 this.tributes.push(newTribute);
40 return newTribute;
41 }
42}Le mot-clé throw interrompt la méthode, et la couche d'exceptions de NestJS envoie la réponse à ta place. Pour un tribut introuvable, le client reçoit ce JSON :
1{
2 "message": "Tributum 3 non inventum! (Tribut introuvable)",
3 "error": "Not Found",
4 "statusCode": 404
5}Ton texte atterrit dans le champ message, tandis que error et statusCode découlent de la classe de l'exception. Quand il te faut un code sans classe dédiée, utilise la classe de base HttpException avec une valeur de l'enum HttpStatus :
1throw new HttpException("Message d'erreur", HttpStatus.I_AM_A_TEAPOT); // 418Le code 418 existe vraiment, même s'il est né comme une plaisanterie. Méfie-toi d'un simple throw new Error() : une telle exception n'est pas une HttpException, donc le client ne voit que { "statusCode": 500, "message": "Internal server error" }. Les détails de l'erreur restent sur le serveur - et c'est tant mieux.
Créer des filtres d'exceptions personnalisés
Ton propre format de verdict vient d'une classe qui implémente l'interface ExceptionFilter. Le décorateur @Catch(HttpException) indique quelles affaires vont devant cette cour, et la méthode catch() reçoit l'exception et un ArgumentsHost - l'objet dont tu tires la requête et la réponse :
1// filters/roman-exception.filter.ts
2import {
3 ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
4} from '@nestjs/common';
5import { Request, Response } from 'express';
6
7@Catch(HttpException)
8export class RomanExceptionFilter implements ExceptionFilter {
9 catch(exception: HttpException, host: ArgumentsHost) {
10 const ctx = host.switchToHttp();
11 const response = ctx.getResponse<Response>();
12 const request = ctx.getRequest<Request>();
13 const status = exception.getStatus();
14
15 // Format personnalisé de la réponse d'erreur, dans le style de l'empire
16 response.status(status).json({
17 statusCode: status,
18 imperium: 'Roma Aeterna',
19 error: exception.message,
20 path: request.url,
21 method: request.method,
22 timestamp: new Date().toISOString(),
23 });
24 }
25}D'abord host.switchToHttp(), puis la réponse et la requête tirées de ce contexte, et enfin l'envoi de la réponse. Tu peux lire le statut de l'exception (exception.getStatus()) n'importe où avant l'envoi. Ce filtre ne change que la forme de la réponse - il recopie tel quel le code de statut de l'exception. Un piège : pour les erreurs de validation, exception.message vaut le générique « Bad Request Exception », et la liste des erreurs de champs se trouve dans exception.getResponse().
Attacher les filtres - les niveaux du tribunal
Tu attaches un filtre avec le décorateur @UseFilters() à une méthode ou à un contrôleur, ou globalement pour toute l'application :
1// 1. Method level - only this method
2@Controller('tributa')
3export class TributeController {
4 @Get(':id')
5 @UseFilters(new RomanExceptionFilter()) // Uniquement pour cette méthode
6 findTribute(@Param('id') id: string) {
7 throw new NotFoundException('Tribut introuvable !');
8 }
9}
10
11// 2. Controller level - entire controller
12@Controller('legiones')
13@UseFilters(RomanExceptionFilter) // Tout le contrôleur
14export class LegionController {
15 @Get(':id')
16 findLegion(@Param('id') id: string) {
17 throw new NotFoundException('Légion introuvable !');
18 }
19}
20
21// 3. Global level - entire application (in main.ts)
22// app.useGlobalFilters(new RomanExceptionFilter());Tu peux passer une instance (new RomanExceptionFilter()) ou la classe elle-même. La documentation de NestJS recommande la classe, car le framework peut alors réutiliser une seule instance dans tout le module et lui injecter ses dépendances. app.useGlobalFilters() a deux limites : le filtre est créé hors du système de modules, il ne reçoit donc aucune dépendance par le constructeur, et il ne couvre pas les gateways WebSocket. C'est pourquoi je préfère enregistrer un filtre global comme provider :
1// app.module.ts - global filter registered as a provider
2import { Module } from '@nestjs/common';
3import { APP_FILTER } from '@nestjs/core';
4import { RomanExceptionFilter } from './filters/roman-exception.filter';
5
6@Module({
7 providers: [{ provide: APP_FILTER, useClass: RomanExceptionFilter }],
8})
9export class AppModule {}Cela fonctionne comme useGlobalFilters, mais le filtre peut désormais injecter, par exemple, un logger. C'est ce que je te recommande.
Le filtre attrape-tout - le tribunal suprême
Un filtre avec un @Catch() vide attrape tout, y compris les erreurs qui ne sont pas des HttpException, comme une connexion à la base de données coupée :
1// filters/all-exceptions.filter.ts
2import {
3 ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
4} from '@nestjs/common';
5
6@Catch() // Sans argument = attrape TOUT
7export class AllExceptionsFilter implements ExceptionFilter {
8 catch(exception: unknown, host: ArgumentsHost) {
9 const ctx = host.switchToHttp();
10 const response = ctx.getResponse();
11 const request = ctx.getRequest();
12
13 const status = exception instanceof HttpException
14 ? exception.getStatus()
15 : HttpStatus.INTERNAL_SERVER_ERROR;
16
17 const message = exception instanceof HttpException
18 ? exception.message
19 : "Erreur interne du serveur de l'empire !";
20
21 response.status(status).json({
22 statusCode: status,
23 error: message,
24 path: request.url,
25 timestamp: new Date().toISOString(),
26 });
27 }
28}L'opérateur instanceof sépare deux chemins : les exceptions HTTP gardent leur statut et leur message, et tout le reste reçoit 500 et un texte générique. Ne renvoie jamais au client le exception.message d'une erreur inconnue - il pourrait révéler des détails de la base. Consigne plutôt une telle erreur dans les logs. Un même filtre peut aussi gérer plusieurs types à la fois : @Catch(HttpException, TypeError).
L'ordre d'exécution des filtres
Les filtres sont le seul élément du cycle de vie d'une requête qui ne commence pas par le niveau global :
- Filtre de la méthode (s'il existe) - vérifié en premier
- Filtre du contrôleur (s'il existe) - vérifié en deuxième
- Filtre global (s'il existe) - vérifié en dernier
Un seul filtre traite une exception. Si le filtre de la méthode la prend en charge, le filtre du contrôleur et le filtre global ne sont pas appelés. Mais quand le @Catch() du filtre de la méthode désigne un autre type d'exception, l'affaire remonte au contrôleur. À un même niveau, déclare le filtre attrape-tout avant le filtre plus spécifique, pour que ce dernier puisse encore traiter son propre type.
Dans la prochaine leçon, tu verras à quel endroit du cycle de vie d'une requête travaillent les filtres, et tout le module 6 sera consacré à la gestion des erreurs.
Souviens-toi : une exception est l'accusation, et un filtre est le tribunal qui la transforme en verdict clair pour le client.
Code de cette leçon : src/exception-filters.ts
1// Filtres d'exceptions - Cours et tribunaux de l'empire
2import {
3 ExceptionFilter, Catch, ArgumentsHost,
4 HttpException, HttpStatus, NotFoundException,
5 BadRequestException, ForbiddenException,
6} from '@nestjs/common';
7
8console.log("Filtres d'exceptions - le système judiciaire de l'empire !");
9
10// ===========================================
11// 1. Exceptions intégrées de NestJS
12// ===========================================
13
14function demonstrateExceptions() {
15 // 404 Not Found
16 // throw new NotFoundException('Tributum non inventum !');
17
18 // 400 Bad Request
19 // throw new BadRequestException('Données requises manquantes !');
20
21 // 403 Forbidden
22 // throw new ForbiddenException('Accès refusé !');
23
24 // N'importe quel code HTTP
25 // throw new HttpException('Message', HttpStatus.I_AM_A_TEAPOT);
26
27 console.log("Exceptions intégrées : NotFoundException, BadRequestException...");
28 console.log("Chacune génère automatiquement le code HTTP approprié");
29}
30
31demonstrateExceptions();
32
33// ===========================================
34// 2. Filtre d'exception personnalisé
35// ===========================================
36
37@Catch(HttpException)
38class RomanExceptionFilter implements ExceptionFilter {
39 catch(exception: HttpException, host: ArgumentsHost) {
40 const ctx = host.switchToHttp();
41 const response = ctx.getResponse();
42 const request = ctx.getRequest();
43 const status = exception.getStatus();
44
45 response.status(status).json({
46 statusCode: status,
47 imperium: 'Roma Aeterna',
48 error: exception.message,
49 path: request.url,
50 timestamp: new Date().toISOString(),
51 });
52 }
53}
54
55console.log("\n@Catch(HttpException) - attrape les exceptions HTTP");
56console.log("@Catch() - attrape TOUTES les exceptions");
57
58// ===========================================
59// 3. Attachement des filtres
60// ===========================================
61
62console.log("\nNiveaux d'attachement :");
63console.log("@UseFilters(filter) sur une méthode - uniquement cette méthode");
64console.log("@UseFilters(filter) sur un contrôleur - le contrôleur entier");
65console.log("app.useGlobalFilters(filter) - l'application entière");
66
67console.log("\n=== RÉSUMÉ SUR LES FILTRES D'EXCEPTIONS ===");
68console.log("HttpException - exception HTTP de base");
69console.log("@Catch(Type) - décorateur de filtre d'exception");
70console.log("ExceptionFilter - interface à implémenter");
71console.log("@UseFilters() - attachement à une méthode/contrôleur");
72console.log("Ordre : méthode > contrôleur > global");
73Tu 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. Quel code HTTP génère le fait de lever new NotFoundException() dans NestJS ?
2. Que signifie le décorateur @Catch(HttpException) sur une classe de filtre d'exception ?
3. Comment crée-t-on un filtre d'exception qui attrape TOUS les types d'exceptions (pas seulement HttpException) ?
4. Si un filtre est défini à la fois au niveau de la méthode et du contrôleur, lequel est exécuté en premier ?
Tâches pratiques dans le jeu
- Éditeur de code
Écris un RomanExceptionFilter avec @Catch(HttpException) qui retourne un JSON avec les champs : statusCode, imperium, error, path, timestamp
- Tri vertical
Ordonne les niveaux d'attachement d'un Exception Filter, de la portée la plus étroite à la plus large :
- Clique dans l'ordre
Range les éléments de la méthode catch() d'un filtre d'exceptions dans le bon ordre :
- Éditeur de code
Écris un AllExceptionsFilter avec @Catch() (sans arguments) qui gère à la fois HttpException et les autres erreurs
- Tri horizontal
Range les éléments pour lever une exception 404 dans NestJS :