Cours NestJS · Module 2: Routing et cycle de requête
Middleware - le premier poste sur la route d'une requête
Dans cette leçon6
Les routes de la leçon précédente disent où va une requête. Cette leçon porte sur ce qui se passe en chemin - car entre l'entrée d'une requête dans l'application et l'exécution d'une méthode de contrôleur, NestJS dresse quatre postes : les middlewares, les guards, les interceptors et les pipes. Tu les rencontreras l'un après l'autre ; nous commençons par le premier.
Un middleware est le poste de péage sur la route. Il se tient le plus loin de la cité, laisse passer tout le monde et inscrit chacun au registre - marchand, courrier, légionnaire. Il ne demande pas où l'on se rend, car au péage on l'ignore encore.
L'interface NestMiddleware
Un middleware doit implémenter l'interface NestMiddleware. Non CanActivate - c'est l'interface des guards de la prochaine leçon ; non NestInterceptor - ce sont les interceptors ; non ExceptionFilter - celle-là s'occupe des erreurs. L'interface réclame une seule méthode, use, à trois arguments :
1import { Injectable, NestMiddleware } from '@nestjs/common';
2import { Request, Response, NextFunction } from 'express';
3
4@Injectable()
5export class LoggerMiddleware implements NestMiddleware {
6 use(req: Request, res: Response, next: NextFunction) {
7 console.log(req.method, req.originalUrl);
8
9 next();
10 }
11}Trois arguments, trois rôles. req (Request) porte tout ce qui vient du client : la méthode HTTP, l'adresse, les en-têtes, le corps de la requête. res (Response) permet de répondre - fixer un statut, un en-tête, envoyer un contenu. next (NextFunction) est le laissez-passer : appeler next() transmet la requête plus loin.
Le middleware ci-dessus écrit la méthode et l'adresse dans la console, puis laisse filer la requête. Cela suffit à tenir un registre de tout ce qui entre dans l'application.
next() - le laissez-passer qu'on oublie facilement
L'appel à next() n'est pas une formalité. Si un middleware n'appelle pas next(), la requête restera bloquée - elle n'avancera pas plus loin dans la chaîne. Tu ne verras ni erreur ni exception : le serveur ne redémarre pas, le middleware ne s'exécute pas deux fois, et la requête ne passe pas d'elle-même au contrôleur. Le client attend simplement l'expiration de son délai.
C'est l'erreur la plus fréquente quand on écrit un middleware, et la seule qui ne laisse aucune trace dans les journaux.
Il arrive pourtant que nous voulions arrêter une requête - et une règle s'applique alors : puisque tu ne la transmets pas, tu dois clore la réponse toi-même :
1@Injectable()
2export class TollMiddleware implements NestMiddleware {
3 use(req: Request, res: Response, next: NextFunction) {
4 if (!req.headers['x-toll-paid']) {
5 res.status(402).json({ message: 'Peage non acquitte' });
6
7 return;
8 }
9
10 next();
11 }
12}Tout chemin traversant la méthode use doit s'achever soit par un appel à next(), soit par une réponse envoyée via res. Il n'existe pas de troisième possibilité - ou plutôt si, et elle porte le nom de requête bloquée.
L'enregistrement - quatre étapes
La classe seule ne fait rien tant que tu n'as pas dit où elle doit agir. Le processus compte quatre étapes, dans cet ordre :
- Créer une classe de middleware avec
@Injectable(). - Implémenter l'interface
NestModuledans la classe du module. - Appeler
consumer.apply(Middleware)dans la méthodeconfigure(). - Spécifier les routes avec
.forRoutes().
La première étape est déjà derrière toi - c'est la classe de l'exemple précédent. Les trois autres se déroulent dans un module :
1import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2
3@Module({
4 controllers: [TributesController],
5 providers: [TributesService],
6})
7export class TributesModule implements NestModule {
8 configure(consumer: MiddlewareConsumer) {
9 consumer.apply(LoggerMiddleware).forRoutes('tributes');
10 }
11}implements NestModule est la promesse que la classe fournira une méthode configure - NestJS l'appelle en construisant le module. À l'intérieur, le consumer de type MiddlewareConsumer reçoit deux informations : .apply(LoggerMiddleware) dit quoi brancher, .forRoutes('tributes') dit où.
Deux choses passent facilement inaperçues. implements NestModule seul ne lance rien - c'est un contrat pour TypeScript qui vérifie le nom de la méthode et le type de l'argument ; NestJS appelle configure() dans chaque module qui la possède. En revanche, consumer.apply(LoggerMiddleware) sans .forRoutes() n'attache le middleware à aucune route - là encore sans la moindre erreur.
La portée - d'une route à toute l'application
.forRoutes() prend plusieurs formes. La chaîne 'tributes' couvre toutes les routes commençant par /tributes. Une classe de contrôleur, par exemple TributesController, couvre toutes ses routes. L'objet { path: 'tributes', method: RequestMethod.POST } restreint la chose à une seule méthode HTTP. On peut aussi exclure des routes par .exclude() - utile pour /health, qu'il n'y a aucun sens à journaliser.
Quand un middleware doit couvrir toute l'application, tu n'as pas besoin de module :
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.use(helmet());
5
6 await app.listen(3000);
7}app.use() dans main.ts branche un middleware globalement et accepte aussi de simples fonctions Express - c'est par cette voie qu'on active les bibliothèques toutes faites comme helmet ou cors. Une différence subsiste : un middleware enregistré par app.use() ne passe pas par l'injection de dépendances, et n'a donc pas accès aux services de l'application.
Là où le middleware s'arrête
Le registre du péage, les en-têtes CORS, l'analyse des cookies - tout cela est indépendant de la destination d'une requête. Et c'est heureux, car le middleware agit au niveau d'Express, avant que NestJS n'ait déterminé quel contrôleur et quelle méthode traiteront la requête. Il reçoit req, res et next - trois objets HTTP, et rien de plus.
Les décisions du genre « cet endpoint exige le rang de sénateur » réclament un savoir dont le péage ne dispose pas. C'est à quoi sert le deuxième poste, et ce sera le sujet de la prochaine leçon.
Récapitulatif
Le péage inscrit chacun et laisse passer :
- quatre mécanismes se dressent sur la route d'une requête : middleware → guards → interceptors → pipes ; le middleware est le premier,
- un middleware implémente l'interface
NestMiddleware- nonCanActivate, nonNestInterceptor, nonExceptionFilter, - la méthode
use(req: Request, res: Response, next: NextFunction):reqporte la requête,respermet de répondre,nexttransmet plus loin, - sans appel à
next(), la requête reste bloquée et n'avance pas dans la chaîne - le serveur ne redémarre pas, le middleware ne s'exécute pas deux fois, la requête n'atteint pas le contrôleur d'elle-même, - tout chemin traversant
uses'achève soit parnext(), soit par une réponse envoyée viares, - enregistrement en quatre étapes : une classe avec
@Injectable()→implements NestModuledans le module →consumer.apply(Middleware)dansconfigure()→.forRoutes(), .forRoutes()accepte un chemin, une classe de contrôleur ou un objet avecpathetmethod;.exclude()écarte certaines routes,app.use()dansmain.tsbranche un middleware globalement, mais sans injection de dépendances,- le middleware agit avant que la route cible ne soit déterminée : il convient à la journalisation et aux en-têtes, non aux décisions liées à l'endpoint.
Dans la prochaine leçon, tu découvriras les guards - le deuxième poste, le premier à savoir où va une requête et donc capable de dire « non ». En attendant, retiens ceci : un middleware qui n'appelle pas next() ne signale aucune erreur. Il se tait, simplement.
Code de cette leçon : src/middleware.ts
1// Middleware dans NestJS - Les gardiens des portes du forum
2import { Injectable, NestMiddleware } from '@nestjs/common';
3import { Request, Response, NextFunction } from 'express';
4
5console.log("Middleware - gardiens qui contrôlent le trafic dans l'empire !");
6
7// ===========================================
8// 1. Classe de middleware - Logger
9// ===========================================
10
11@Injectable()
12export class RomanLoggerMiddleware implements NestMiddleware {
13 use(req: Request, res: Response, next: NextFunction) {
14 const startTime = Date.now();
15
16 console.log('[Roman Gate] ' + req.method + ' ' + req.originalUrl);
17 console.log('[Roman Gate] IP: ' + req.ip);
18
19 // Une fois la réponse terminée - journaliser le temps
20 res.on('finish', () => {
21 const duration = Date.now() - startTime;
22 console.log('[Roman Gate] Status: ' + res.statusCode + ' (' + duration + 'ms)');
23 });
24
25 next(); // Passer le contrôle plus loin
26 }
27}
28
29// ===========================================
30// 2. Middleware fonctionnel
31// ===========================================
32
33export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
34 res.header('Access-Control-Allow-Origin', '*');
35 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
36 res.header('X-Powered-By', 'Imperium Romanum');
37 console.log('[CORS] En-têtes définis pour : ' + req.originalUrl);
38 next();
39}
40
41// ===========================================
42// 3. Enregistrement du middleware dans le module
43// ===========================================
44
45import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
46
47@Module({
48 imports: [],
49 controllers: [],
50 providers: [],
51})
52export class AppModule implements NestModule {
53 configure(consumer: MiddlewareConsumer) {
54 // Middleware pour toutes les routes
55 consumer
56 .apply(RomanLoggerMiddleware)
57 .forRoutes('*');
58
59 // Middleware uniquement pour certaines routes
60 consumer
61 .apply(corsMiddleware)
62 .forRoutes(
63 { path: 'legiones', method: RequestMethod.ALL },
64 { path: 'tributes', method: RequestMethod.GET },
65 );
66 }
67}
68
69// ===========================================
70// 4. Middleware avec exclusion de routes
71// ===========================================
72
73// consumer
74// .apply(AuthMiddleware)
75// .exclude(
76// { path: 'auth/login', method: RequestMethod.POST },
77// { path: 'auth/register', method: RequestMethod.POST },
78// )
79// .forRoutes('*');
80
81console.log("\n=== RÉSUMÉ SUR LE MIDDLEWARE ===");
82console.log("Le middleware agit entre la requête et la réponse");
83console.log("@Injectable() + NestMiddleware - classe de middleware");
84console.log("Fonction (req, res, next) - middleware fonctionnel");
85console.log("consumer.apply().forRoutes() - enregistrement dans le module");
86console.log("next() - passe le contrôle plus loin");
87Tu 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 middleware doit-il implémenter dans NestJS ?
2. Que se passe-t-il si un middleware n'appelle PAS la fonction next() ?
Tâches pratiques dans le jeu
- Éditeur de code
Écris un LoggerMiddleware implémentant NestMiddleware avec une méthode use(req, res, next) qui journalise la méthode et l'URL de la requête, puis appelle next()
- Tri vertical
Ordonne les étapes d'enregistrement d'un middleware dans NestJS, de la première à la dernière :