Cours NestJS · Module 1: Bases de NestJS

Modules et Dependency Injection

6 min de lecture
Dans cette leçon6

Tu possèdes déjà un contrôleur et un service. Mais comment NestJS sait-il que ce contrôleur existe ? Et où prend-il le service qu'il injecte dans son constructeur ? Des classes posées dans des fichiers ne signifient rien par elles-mêmes - quelqu'un doit les déclarer.

L'Empire ne gouvernait pas ses provinces depuis un seul bureau. Chacune avait sa propre administration : son registre de charges, ses spécialistes, et la liste de ce qu'elle mettait à disposition de ses voisines. L'empereur ne connaissait que la liste des provinces, non chaque scribe. Dans NestJS, cette province est un module.

Le décorateur @Module

Un module est une classe munie d'un décorateur qui énumère son contenu :

1import { Module } from '@nestjs/common';
2
3@Module({
4  imports: [DatabaseModule],
5  controllers: [LegionController],
6  providers: [LegionService],
7  exports: [LegionService],
8})
9export class LegionModule {}

L'import du décorateur a une forme fixe - import, { Module }, from, '@nestjs/common'.

Quatre champs décrivent toute la province, et chacun répond à une question différente :

controllers - qui reçoit les requêtes. providers - quels spécialistes travaillent ici ; c'est là que tu enregistres les services. imports - de quels autres modules nous nous servons. exports - ce que nous mettons à disposition au-dehors.

Ce sont tous les champs du décorateur @Module. Il n'y a pas de champ routes parmi eux - les routes ne se configurent pas dans un module ; elles découlent des décorateurs du contrôleur rencontrés plus tôt.

exports - ce qui se voit depuis l'autre côté de la frontière

Le champ exports est celui qui cause le plus d'ennuis, alors nommons-le franchement : un service inscrit dans exports sera disponible pour les modules qui importent ce module.

D'où l'inverse - un service enregistré dans providers mais absent d'exports ne fonctionne qu'à l'intérieur de son propre module. Un autre module qui tente de l'injecter obtiendra au démarrage une erreur de dépendance non résolue. C'est précisément l'erreur qui surgit dans la moitié des premiers projets NestJS.

Note ce qu'exports ne fait pas : il ne retire pas le service du module, n'en fait pas un contrôleur et n'a rien à voir avec un enregistrement en base. C'est une pure déclaration de visibilité.

Cette fermeture par défaut est délibérée. Un module doit dire explicitement ce qu'il partage - ce qui garde les frontières visibles et empêche des dépendances accidentelles de se former entre des parties éloignées de l'application.

Les quatre étapes de l'injection

Puisqu'un module enregistre les providers, nous pouvons assembler tout le chemin d'une dépendance. Il compte quatre étapes, toujours dans cet ordre :

  1. Définir un provider avec @Injectable() - une classe prête à être injectée.
  2. L'enregistrer dans le tableau providers du module - NestJS apprend qu'une telle classe existe.
  3. L'injecter par le constructeur là où elle est nécessaire.
  4. Utiliser le service dans les méthodes de la classe.

La deuxième étape est celle qu'on oublie facilement en ajoutant un nouveau service : le décorateur @Injectable() seul ne suffit pas. NestJS bâtit sa carte des dépendances à partir de ce qu'énumèrent les modules, non d'un balayage des fichiers.

Un module global

Certaines choses servent partout - la configuration, un logger, une connexion à la base. Importer leur module dans chaque autre module n'est alors que répétition :

1@Global()
2@Module({
3  providers: [ConfigService],
4  exports: [ConfigService],
5})
6export class ConfigModule {}

Le décorateur @Global() rend un module disponible sans l'importer dans les autres modules. Le module global lui-même s'enregistre une fois, généralement dans les imports du module racine. Attention au nom - il n'existe pas de décorateurs @Shared(), @Public() ni @Universal().

Une chose ne change pas malgré ce statut global : exports s'applique toujours. @Global() te dispense d'importer le module, mais c'est exports qui décide de ce que ce module partage.

Utilise-le avec parcimonie. Un module global disparaît des listes d'imports, on cesse donc de voir qui dépend de quoi - et c'est l'information qui te sauve sur un projet d'ampleur. La configuration et un logger, oui ; tout le reste, probablement pas.

Un provider sous un nom

Jusqu'ici, nous reconnaissions les providers par le type de leur classe. Mais on peut aussi injecter une simple valeur - un objet, un tableau, un nombre - et celles-là n'ont pas de type :

1@Module({
2  providers: [
3    {
4      provide: 'CONFIG_TOKEN',
5      useValue: { apiUrl: 'https://api.imperium.rome', timeout: 5000 },
6    },
7  ],
8})
9export class AppModule {}

useValue fournit une valeur toute prête au lieu d'une classe à instancier, et provide lui donne un nom - un token sous lequel elle sera reconnue.

Puisqu'il n'y a ici aucun type permettant à NestJS de choisir un provider, nous désignons le token explicitement au moment de l'injection :

1@Injectable()
2export class ApiService {
3  constructor(@Inject('CONFIG_TOKEN') private config: { apiUrl: string }) {}
4}

@Inject('CONFIG_TOKEN') dit : donne-moi ce qui a été enregistré sous ce nom. Avec les services ordinaires, le décorateur est inutile puisque le type suffit - ici il est indispensable.

Récapitulatif

Les provinces ont leurs administrations, l'empereur n'en connaît que la liste :

  • un module regroupe des éléments liés et les déclare à NestJS - des classes dans des fichiers ne signifient rien seules,
  • l'import du décorateur : import, { Module }, from, '@nestjs/common',
  • les quatre champs de @Module : controllers, providers, imports, exports - il n'y a pas de champ routes, les routes découlent des décorateurs du contrôleur,
  • un service dans exports est disponible pour les modules importateurs ; sans cela, il ne fonctionne qu'en interne,
  • exports ne retire ni ne change rien - c'est une pure déclaration de visibilité,
  • les quatre étapes de l'injection : @Injectable() → enregistrement dans providers → injection par le constructeur → utilisation dans les méthodes,
  • @Injectable() seul ne suffit pas - NestJS bâtit sa carte des dépendances à partir des modules, non d'un balayage de fichiers,
  • @Global() rend un module disponible sans import ; @Shared(), @Public() et @Universal() n'existent pas,
  • être global ne dispense pas d'exports - et emploie-le avec parcimonie, car il masque les dépendances,
  • useValue enregistre une valeur toute prête, provide lui donne un token, et @Inject('TOKEN') désigne ce token à l'injection.

Dans la prochaine leçon, tu découvriras les contrôleurs de l'intérieur - tu verras comment une requête atteint la bonne méthode. En attendant, retiens ceci : un module est l'administration d'une province - il enregistre qui y travaille et dit explicitement ce qu'il prête à ses voisines.

Code de cette leçon : src/legion.module.ts
1// Modules et injection de dépendances - Organisation de la légion
2import { Module } from '@nestjs/common';
3import { LegionController } from './legion.controller';
4import { LegionService } from './legion.service';
5import { ArmorService } from './armor.service';
6
7console.log("Organisation de la légion par modules et DI !");
8
9// ===========================================
10// 1. Module de base - structure de la légion
11// ===========================================
12
13@Module({
14  controllers: [LegionController],
15  providers: [LegionService, ArmorService],
16  exports: [LegionService], // Nous exposons pour les autres modules
17})
18export class LegionModule {}
19
20// ===========================================
21// 2. Module de province important LegionModule
22// ===========================================
23
24import { ProvinciaController } from './provincia.controller';
25import { ProvinciaService } from './provincia.service';
26
27@Module({
28  imports: [LegionModule], // L'import donne accès aux exports
29  controllers: [ProvinciaController],
30  providers: [ProvinciaService],
31})
32export class ProvinciaModule {}
33
34// ===========================================
35// 3. Module principal de l'Imperium
36// ===========================================
37
38@Module({
39  imports: [LegionModule, ProvinciaModule],
40})
41export class AppModule {}
42
43// ===========================================
44// 4. Dependency Injection en pratique
45// ===========================================
46
47import { Injectable } from '@nestjs/common';
48
49@Injectable()
50export class ArmorService {
51  getArmorStrength(type: string): number {
52    const armorMap: Record<string, number> = {
53      lorica_segmentata: 90,
54      lorica_hamata: 70,
55      lorica_squamata: 60,
56      scutum: 50,
57    };
58    return armorMap[type] || 30;
59  }
60}
61
62@Injectable()
63export class LegionService {
64  // DI - NestJS injecte automatiquement ArmorService
65  constructor(private readonly armorService: ArmorService) {
66    console.log("LegionService créé avec ArmorService !");
67  }
68
69  calculateDefense(armorType: string, soldiers: number): number {
70    const armorStrength = this.armorService.getArmorStrength(armorType);
71    return armorStrength * soldiers;
72  }
73}
74
75console.log("\n=== DEPENDENCY INJECTION ===");
76console.log("1. @Module déclare controllers, providers, exports");
77console.log("2. imports permet d'utiliser les exports d'un autre module");
78console.log("3. NestJS injecte automatiquement les dépendances via le constructeur");
79console.log("4. @Injectable() marque une classe comme provider");
80

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. Lequel des champs suivants ne fait PAS partie du décorateur @Module() dans NestJS ?

  2. 2. Que signifie placer un service dans le tableau 'exports' du module ?

  3. 3. Quel décorateur rend un module disponible globalement sans avoir besoin de l'importer ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Définis un module LegionModule avec le décorateur @Module() qui enregistre LegionController dans controllers, LegionService dans providers et rend LegionService disponible pour les autres modules via exports.

  • Tri vertical

    Remets dans l'ordre, du début à la fin, les étapes du processus d'injection de dépendances dans NestJS :

  • Clique dans l'ordre

    Remets dans le bon ordre les éléments de l'import du décorateur Module :

  • Éditeur de code

    Définis une constante configProvider avec provide: 'CONFIG_TOKEN' et useValue contenant un objet de configuration (par exemple { legion: 'Legio X Equestris' }) et ajoute-la aux providers du module AppModule. Écris un service LegionConfigService qui injecte cette valeur via le constructeur avec le décorateur @Inject('CONFIG_TOKEN') et la renvoie dans une méthode getConfig().