Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
NestJS

Modules partagés, modules globaux et modules dynamiques

Cette leçon présente trois façons de réutiliser un module dans NestJS. Le module partagé, importé par plusieurs modules, fournit une instance unique de ses services (démonstration à l'appui). Le modul...

0 chapitres 0 QCM Accès gratuit
Modules partagés, modules globaux et modules dynamiques

À propos de ce cours

Cette leçon présente trois façons de réutiliser un module dans NestJS. Le module partagé, importé par plusieurs modules, fournit une instance unique de ses services (démonstration à l'appui). Le module global, déclaré avec @Global, rend ses exports disponibles partout sans import, avec ses avantages et ses risques. Le module dynamique, configuré au moment de l'import par une méthode statique forRoot qui renvoie un DynamicModule, s'appuie sur un jeton d'injection et @Inject. La leçon présente aussi les conventions forRoot, register et forFeature et les modules dynamiques de l'écosystème (ConfigModule, TypeOrmModule, JwtModule).

Objectifs de la leçon

  • Créer un module partagé et vérifier qu'il fournit une instance unique.
  • Déclarer un module global et savoir quand l'utiliser.
  • Écrire un module dynamique configurable avec forRoot().
  • Injecter une valeur de configuration avec un jeton et @Inject().
  • Reconnaître forRoot, register et forFeature dans l'écosystème NestJS.

Modules partagés, globaux et dynamiques

Trois façons de réutiliser un module, du plus courant au plus avancé.

1. Le module partagé

Dans NestJS, tout module est partagé par nature : dès qu'il exporte un provider, plusieurs modules peuvent l'importer. Point essentiel : ils reçoivent la même instance. Démonstration avec un compteur placé dans un CommonModule importé par deux modules :

📄 src/common/compteur.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class CompteurService {
  private readonly creeLe = new Date().toISOString();
  private total = 0;
  incrementer(source: string): string {
    this.total++;
    return `${source} → total ${this.total} (instance créée à ${this.creeLe.slice(11, 23)})`;
  }
}

📄 src/common/common.module.ts

import { Module } from '@nestjs/common';
import { CompteurService } from './compteur.service';

@Module({
  providers: [CompteurService],
  exports: [CompteurService],
})
export class CommonModule {}

📄 src/groupes/groupes.module.ts

import { Controller, Get, Module } from '@nestjs/common';
import { CommonModule } from '../common/common.module';
import { CompteurService } from '../common/compteur.service';

@Controller('groupes')
export class GroupesController {
  constructor(private readonly compteur: CompteurService) {}

  @Get()
  findAll() {
    return { visite: this.compteur.incrementer('groupes') };
  }
}

@Module({ imports: [CommonModule], controllers: [GroupesController] })
export class GroupesModule {}

Pour raccourcir l'exemple, contrôleur et module sont écrits dans le même fichier. Dans un vrai projet, gardez un fichier par classe.

Le module des stagiaires (ci-dessous, partie 3) importe lui aussi CommonModule. Résultat réel de trois requêtes :

Réponses :

GET /stagiaires → {"entete":"Institut de formation de Tunis — 2026-2027","visite":"stagiaires → total 1 (instance créée à 21:16:31.459)"}
GET /groupes → {"visite":"groupes → total 2 (instance créée à 21:16:31.459)"}
GET /stagiaires → {"entete":"Institut de formation de Tunis — 2026-2027","visite":"stagiaires → total 3 (instance créée à 21:16:31.459)"}

Le total continue de 1 à 3 d'un module à l'autre et l'heure de création est identique : une seule instance de CompteurService sert les deux modules. C'est ce qui permet de partager une connexion à la base de données, un cache ou une configuration.

2. Le module global

Certains services sont utilisés presque partout (configuration, journalisation). Les importer dans chaque module devient fastidieux. Le décorateur @Global() rend les exports d'un module disponibles dans toute l'application, à condition que le module soit importé une fois (en général dans AppModule).

@Global()
@Module({
  providers: [JournalService],
  exports: [JournalService],
})
export class JournalModule {}

// app.module.ts : importé une seule fois
@Module({ imports: [JournalModule, StagiairesModule, GroupesModule] })
export class AppModule {}

// stagiaires.module.ts : aucun import nécessaire pour injecter JournalService
@Module({ controllers: [StagiairesController], providers: [StagiairesService] })
export class StagiairesModule {}

Avec modération: un module global masque les dépendances. En lisant StagiairesModule, on ne voit plus qu'il dépend de la journalisation. La documentation de NestJS recommande de réserver @Global() à quelques modules d'infrastructure (configuration, base de données) et d'utiliser des imports explicites pour le reste.

3. Le module dynamique

Un module « statique » est toujours identique. Un module dynamique reçoit des options au moment de l'import: nom de l'établissement, adresse de la base de données, clé secrète… Il expose une méthode statique, par convention forRoot(), qui renvoie un objet DynamicModule:

📄 src/parametres/parametres.constants.ts

export interface ParametresOptions {
  etablissement: string;
  anneeScolaire: string;
}

export const PARAMETRES_OPTIONS = 'PARAMETRES_OPTIONS';   // jeton d'injection

📄 src/parametres/parametres.module.ts

import { DynamicModule, Global, Module } from '@nestjs/common';
import { PARAMETRES_OPTIONS, ParametresOptions } from './parametres.constants';
import { ParametresService } from './parametres.service';

@Global()
@Module({})
export class ParametresModule {
  static forRoot(options: ParametresOptions): DynamicModule {
    return {
      module: ParametresModule,
      providers: [
        { provide: PARAMETRES_OPTIONS, useValue: options },   // les options deviennent injectables
        ParametresService,
      ],
      exports: [ParametresService],
    };
  }
}
  • module: la classe du module (obligatoire) ;
  • providers: ici, un provider de valeur { provide: JETON, useValue: options } qui rend les options injectables (les providers personnalisés sont détaillés au chapitre 07) ;
  • exports: comme pour un module statique.

Comme les options ne sont pas une classe, on les injecte grâce à leur jeton avec @Inject():

📄 src/parametres/parametres.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { PARAMETRES_OPTIONS, ParametresOptions } from './parametres.constants';

@Injectable()
export class ParametresService {
  constructor(@Inject(PARAMETRES_OPTIONS) private readonly options: ParametresOptions) {}

  entete(): string {
    return `${this.options.etablissement} — ${this.options.anneeScolaire}`;
  }
}

Le module est configuré au moment de l'import, dans AppModule:

📄 src/app.module.ts

import { Module } from '@nestjs/common';
import { GroupesModule } from './groupes/groupes.module';
import { ParametresModule } from './parametres/parametres.module';
import { StagiairesModule } from './stagiaires/stagiaires.module';

@Module({
  imports: [
    ParametresModule.forRoot({ etablissement: 'Institut de formation de Tunis', anneeScolaire: '2026-2027' }),
    StagiairesModule,
    GroupesModule,
  ],
})
export class AppModule {}

Et grâce à @Global(), le module des stagiaires injecte ParametresService sans l'importer, alors qu'il importe explicitement CommonModule:

📄 src/stagiaires/stagiaires.module.ts

import { Controller, Get, Module } from '@nestjs/common';
import { CommonModule } from '../common/common.module';
import { CompteurService } from '../common/compteur.service';
import { ParametresService } from '../parametres/parametres.service';

@Controller('stagiaires')
export class StagiairesController {
  constructor(
    private readonly compteur: CompteurService,        // module partagé importé
    private readonly parametres: ParametresService,    // module global : aucun import
  ) {}

  @Get()
  findAll() {
    return { entete: this.parametres.entete(), visite: this.compteur.incrementer('stagiaires') };
  }
}

@Module({ imports: [CommonModule], controllers: [StagiairesController] })
export class StagiairesModule {}

Le champ entete des réponses ci-dessus (« Institut de formation de Tunis — 2026-2027 ») provient bien des options passées à forRoot().

Le jeton et l'interface sont placés dans un fichier parametres.constants.ts séparé : si le service importait le jeton depuis le fichier du module, et le module importait le service, on créerait une dépendance circulaire entre fichiers.

4. Les conventions de nommage
MéthodeUsageExemple de l'écosystème
forRoot(options)configurer une fois, pour toute l'applicationConfigModule.forRoot(), TypeOrmModule.forRoot({ … })
forFeature(…)compléter la configuration racine pour un module de fonctionnalitéTypeOrmModule.forFeature([Stagiaire])
register(options)configuration propre à chaque module qui l'importeJwtModule.register({ secret }), HttpModule.register({ timeout })
forRootAsync({ useFactory, inject })options calculées à partir d'autres providers (souvent la configuration)TypeOrmModule.forRootAsync({ … })
// Aperçu des chapitres 10 et 11 : configuration lue dans .env, puis base de données
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'mysql',
        host: config.get('DB_HOST'),
        database: config.get('DB_NAME'),
        autoLoadEntities: true,
      }),
    }),
    StagiairesModule,
  ],
})
export class AppModule {}

Vous savez maintenant ce que fait ce code : deux modules dynamiques, le premier global, le second configuré à partir d'un service injecté.

Activités

Activité 1 Vérifier l'instance unique

Reproduisez CommonModule et CompteurService, importez-le dans deux modules de votre projet et vérifiez que le total est partagé. Ajoutez ensuite CompteurService directement aux providers d'un des deux modules (sans retirer l'import) : que constatez-vous ?

Voir le corrigé

Le module qui déclare lui-même CompteurService en crée une seconde instance (heure de création différente, compteur séparé). Un provider déclaré localement est prioritaire sur celui reçu par import. C'est presque toujours une erreur.

Activité 2 Votre module dynamique

Écrivez un JournalModule dynamique avec forRoot({ prefixe: string; horodatage?: boolean }). Son JournalService.ecrire(message) affiche [PREFIXE] message (précédé de l'heure si horodatage vaut true, false par défaut). Rendez-le global sans le décorateur @Global() (indice : une propriété de DynamicModule) et testez-le depuis un module qui ne l'importe pas.

Voir le corrigé

📄 src/journal/journal.module.ts

import { DynamicModule, Inject, Injectable, Module } from '@nestjs/common';

export interface JournalOptions {
  prefixe: string;
  horodatage?: boolean;
}
export const JOURNAL_OPTIONS = 'JOURNAL_OPTIONS';

@Injectable()
export class JournalService {
  constructor(@Inject(JOURNAL_OPTIONS) private readonly options: JournalOptions) {}
  ecrire(message: string): string {
    const heure = this.options.horodatage ? `${new Date().toISOString().slice(11, 19)} ` : '';
    const ligne = `${heure}[${this.options.prefixe}] ${message}`;
    console.log(ligne);
    return ligne;
  }
}

@Module({})
export class JournalModule {
  static forRoot(options: JournalOptions): DynamicModule {
    return {
      module: JournalModule,
      global: true,                       // autre façon de rendre un module global
      providers: [{ provide: JOURNAL_OPTIONS, useValue: { horodatage: false, ...options } }, JournalService],
      exports: [JournalService],
    };
  }
}

Résultat :

[API-FORMATION] route /test appelée
Réponse : [API-FORMATION] route /test appelée

La propriété global: true d'un DynamicModule a le même effet que @Global(). La décomposition { horodatage: false, ...options } applique la valeur par défaut.

Activité 3 forRoot ou register ?

Pour chaque besoin, choisissez forRoot, register ou forFeature: (a) connexion unique à la base de données ; (b) déclarer, dans StagiairesModule, les tables utilisées par ce module ; (c) un client HTTP avec un délai maximum de 5 s pour un module et de 30 s pour un autre ; (d) lire le fichier .env une fois au démarrage.

Voir le corrigé

(a) forRoot; (b) forFeature; (c) register, une configuration par module importateur ; (d) forRoot.

Activité 4 Global ou pas ?

Pour chacun de ces modules, dites s'il est raisonnable de le rendre global et justifiez : configuration de l'application, service d'envoi d'e-mails, module des groupes, journalisation, connexion à la base de données.

Voir le corrigé

Configuration, journalisation et connexion à la base : oui, c'est de l'infrastructure utilisée presque partout. E-mails : plutôt non, peu de modules en ont besoin et l'import explicite documente la dépendance. Groupes : non, c'est un module métier ; le rendre global cacherait les relations entre fonctionnalités.

Activité 5 Lire un module de l'écosystème

Expliquez ligne par ligne l'aperçu de code de la partie 4 : quels modules sont dynamiques, lequel est global, pourquoi forRootAsync est nécessaire pour TypeORM, et d'où vient le ConfigService injecté ?

Voir le corrigé

ConfigModule.forRoot({ isGlobal: true }) est dynamique et global : son ConfigService est injectable partout. TypeOrmModule.forRootAsync est dynamique ; la version Async est nécessaire car les options dépendent d'un autre provider (ConfigService), que l'on demande avec inject et que la fabrique useFactory reçoit en paramètre. StagiairesModule est un module statique ordinaire.

À retenir

  • Un module importé par plusieurs modules fournit une seule instance de ses providers.
  • @Global() (ou global: true) rend les exports visibles partout : à réserver à l'infrastructure.
  • Un module dynamique expose forRoot()/register() qui renvoie un DynamicModule configuré.
  • Les options deviennent injectables avec { provide: JETON, useValue } et @Inject(JETON).
  • forRoot (une fois), forFeature (par module), register (configuration par importateur), …Async (fabrique).