Cours NestJS · Module 2: Routing et cycle de requête

Middleware - le premier poste sur la route d'une requête

6 min de lecture
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 :

  1. Créer une classe de middleware avec @Injectable().
  2. Implémenter l'interface NestModule dans la classe du module.
  3. Appeler consumer.apply(Middleware) dans la méthode configure().
  4. 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 - non CanActivate, non NestInterceptor, non ExceptionFilter,
  • la méthode use(req: Request, res: Response, next: NextFunction) : req porte la requête, res permet de répondre, next transmet 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 use s'achève soit par next(), soit par une réponse envoyée via res,
  • enregistrement en quatre étapes : une classe avec @Injectable() → implements NestModule dans le module → consumer.apply(Middleware) dans configure() → .forRoutes(),
  • .forRoutes() accepte un chemin, une classe de contrôleur ou un objet avec path et method ; .exclude() écarte certaines routes,
  • app.use() dans main.ts branche 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");
87

Tu 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. 1. Quelle interface un middleware doit-il implémenter dans NestJS ?

  2. 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 :