À propos de ce cours
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.
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éthode | Usage | Exemple de l'écosystème |
|---|---|---|
forRoot(options) | configurer une fois, pour toute l'application | ConfigModule.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'importe | JwtModule.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).