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

Gérer les dépendances circulaires (forwardRef)

Cette leçon traite des dépendances circulaires, quand deux providers ou deux modules dépendent l'un de l'autre. Elle montre les messages d'erreur réels de NestJS 11 (dont le trompeur « undefined at ru...

0 chapitres 0 QCM Accès gratuit
Gérer les dépendances circulaires (forwardRef)

À propos de ce cours

Cette leçon traite des dépendances circulaires, quand deux providers ou deux modules dépendent l'un de l'autre. Elle montre les messages d'erreur réels de NestJS 11 (dont le trompeur « undefined at runtime »), explique leur cause (import circulaire entre fichiers), présente la solution technique forwardRef pour les services et pour les modules, exécutée et vérifiée, puis la solution recommandée : supprimer le cycle en extrayant un service commun, en inversant une dépendance ou en utilisant des événements. Activités corrigées.

Objectifs de la leçon

  • Reconnaître une dépendance circulaire entre services ou entre modules.
  • Lire les messages d'erreur correspondants.
  • Utiliser forwardRef pour les services et pour les modules.
  • Supprimer un cycle par une meilleure conception.
Qu'est-ce qu'une dépendance circulaire ?

Il y a dépendance circulaire quand A a besoin de B et que B a besoin de A, directement ou par une chaîne (A → B → C → A). Pour créer A, il faut d'abord B ; pour créer B, il faut d'abord A… Le conteneur ne peut pas en sortir.

Dépendance circulaire et solutions

forwardRef contourne le cycle ; extraire un service le supprime.

Entre deux services

Le service des stagiaires veut connaître le groupe d'un stagiaire ; le service des groupes veut l'effectif d'un groupe, qu'il demande au service des stagiaires :

📄 src/stagiaires.service.ts

import { Injectable } from '@nestjs/common';
import { GroupesService } from './groupes.service';

@Injectable()
export class StagiairesService {
  constructor(private readonly groupesService: GroupesService) {}   // a besoin des groupes…

  stagiairesDu(code: string) {
    return ['Ben Ali', 'Trabelsi'].map((nom) => `${nom} (${code})`);
  }

  groupeDe(nom: string) {
    return this.groupesService.trouver('DEV101');
  }
}

📄 src/groupes.service.ts

import { Injectable } from '@nestjs/common';
import { StagiairesService } from './stagiaires.service';

@Injectable()
export class GroupesService {
  constructor(private readonly stagiairesService: StagiairesService) {}   // …et les groupes des stagiaires

  trouver(code: string) {
    return { code, effectif: this.stagiairesService.stagiairesDu(code).length };
  }
}

Erreur au démarrage :

Nest can't resolve dependencies of the StagiairesService (?). Please make sure that the argument at index [0] is available in the current module.

Potential solutions:
- The dependency at index [0] appears to be undefined at runtime
- This commonly occurs when using 'import type' instead of 'import' for injectable classes

Le message est trompeur: il ne parle pas de cycle mais d'un argument « undefined at runtime », et ne nomme même pas la dépendance. La cause est l'import circulaire entre les deux fichiers: quand Node.js charge stagiaires.service.ts, celui-ci importe groupes.service.ts, qui importe à son tour stagiaires.service.ts… encore en cours de chargement. À ce moment, la classe StagiairesService n'existe pas encore : elle vaut undefined dans les métadonnées. Retenez : « appears to be undefined at runtime » = cherchez un import circulaire (ou un import type sur une classe injectée).

Solution technique : forwardRef

forwardRef(() => Classe) remplace la référence directe par une fonction, appelée plus tard, quand toutes les classes existent. Il faut l'utiliser des deux côtés, avec @Inject:

📄 src/stagiaires.service.ts

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { GroupesService } from './groupes.service';

@Injectable()
export class StagiairesService {
  constructor(
    @Inject(forwardRef(() => GroupesService))
    private readonly groupesService: GroupesService,
  ) {}

  stagiairesDu(code: string) {
    return ['Ben Ali', 'Trabelsi'].map((nom) => `${nom} (${code})`);
  }

  groupeDe(nom: string) {
    return this.groupesService.trouver('DEV101');
  }
}

📄 src/groupes.service.ts

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { StagiairesService } from './stagiaires.service';

@Injectable()
export class GroupesService {
  constructor(
    @Inject(forwardRef(() => StagiairesService))
    private readonly stagiairesService: StagiairesService,
  ) {}

  trouver(code: string) {
    return { code, effectif: this.stagiairesService.stagiairesDu(code).length };
  }
}

Résultat réellement obtenu :

démarrage réussi
{ code: 'DEV101', effectif: 2 }
{ code: 'DEV101', effectif: 2 }

Attention: avec forwardRef, n'utilisez pas l'autre service dans le constructeur: l'ordre de création n'étant pas garanti, il peut ne pas être prêt. Utilisez-le dans les méthodes, ou dans onModuleInit. Et évitez forwardRef avec des providers de portée requête : la combinaison peut produire des dépendances undefined.

Entre deux modules

Le même problème survient quand deux modules s'importent mutuellement :

📄 src/stagiaires.module.ts

import { Module } from '@nestjs/common';
import { GroupesModule } from './groupes.module';

@Module({ imports: [GroupesModule] })
export class StagiairesModule {}

📄 src/groupes.module.ts

import { Module } from '@nestjs/common';
import { StagiairesModule } from './stagiaires.module';

@Module({ imports: [StagiairesModule] })
export class GroupesModule {}

Erreur au démarrage :

Nest cannot create the StagiairesModule instance.
The module at index [0] of the StagiairesModule "imports" array is undefined.

Potential causes:
- A circular dependency between modules. Use forwardRef() to avoid it. Read more: https://docs.nestjs.com/fundamentals/circular-dependency

Ici, NestJS identifie clairement la cause probable. La correction utilise forwardRef dans les imports, des deux côtés :

📄 src/stagiaires.module.ts

import { forwardRef, Module } from '@nestjs/common';
import { GroupesModule } from './groupes.module';

@Module({ imports: [forwardRef(() => GroupesModule)] })
export class StagiairesModule {}

📄 src/groupes.module.ts

import { forwardRef, Module } from '@nestjs/common';
import { StagiairesModule } from './stagiaires.module';

@Module({ imports: [forwardRef(() => StagiairesModule)] })
export class GroupesModule {}

Résultat réellement obtenu :

démarrage réussi
La meilleure solution : supprimer le cycle

forwardRef fait disparaître l'erreur, mais pas le problème de conception : deux classes qui dépendent l'une de l'autre sont fortement couplées et difficiles à tester séparément. Un cycle signale souvent qu'une responsabilité est mal placée. Trois façons de le supprimer :

  1. Extraire un service commun: ce dont les deux ont besoin (ici, la relation « quel stagiaire est dans quel groupe ») va dans un troisième service, dont les deux dépendent.
  2. Inverser une dépendance: se demander lequel des deux a vraiment besoin de l'autre ; souvent, une seule direction suffit, l'autre méthode étant déplacée.
  3. Communiquer par événements: au lieu d'appeler directement l'autre service, on émet un événement (« stagiaire inscrit ») que l'autre écoute (@nestjs/event-emitter, chapitre 15).

Version avec un service extrait, sans aucun forwardRef :

📄 src/inscriptions.service.ts

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

// Les données partagées sont extraites dans un troisième service, dont les deux autres dépendent
@Injectable()
export class InscriptionsService {
  private readonly inscriptions = [
    { stagiaire: 'Ben Ali', groupe: 'DEV101' },
    { stagiaire: 'Trabelsi', groupe: 'DEV101' },
    { stagiaire: 'Gharbi', groupe: 'DEV102' },
  ];
  stagiairesDu(code: string) {
    return this.inscriptions.filter((i) => i.groupe === code).map((i) => i.stagiaire);
  }
  groupeDe(nom: string) {
    return this.inscriptions.find((i) => i.stagiaire === nom)?.groupe;
  }
}

📄 src/stagiaires.service.ts

import { Injectable } from '@nestjs/common';
import { InscriptionsService } from './inscriptions.service';

@Injectable()
export class StagiairesService {
  constructor(private readonly inscriptions: InscriptionsService) {}
  groupeDe(nom: string) {
    return this.inscriptions.groupeDe(nom);
  }
}

📄 src/groupes.service.ts

import { Injectable } from '@nestjs/common';
import { InscriptionsService } from './inscriptions.service';

@Injectable()
export class GroupesService {
  constructor(private readonly inscriptions: InscriptionsService) {}
  trouver(code: string) {
    return { code, effectif: this.inscriptions.stagiairesDu(code).length };
  }
}

Résultat réellement obtenu :

démarrage réussi
{ code: 'DEV101', effectif: 2 }
DEV101

Les dépendances vont désormais toutes dans le même sens (vers InscriptionsService) : chaque service est testable seul, en fournissant un faux InscriptionsService.

Activités

Activité 1 Détecter les cycles

Pour chaque ensemble de dépendances, dites s'il y a un cycle : (a) Notes → Stagiaires → Groupes ; (b) Notes → Stagiaires, Stagiaires → Notes ; (c) Absences → Seances → Groupes → Absences ; (d) Formations → Groupes, Stagiaires → Groupes, Stagiaires → Formations.

Voir le corrigé

(a) Non. (b) Oui, cycle direct. (c) Oui, cycle de longueur 3, plus difficile à repérer. (d) Non : deux chemins mènent à Groupes, mais aucun ne revient au point de départ.

Activité 2 Lire l'erreur

Un étudiant obtient « Nest can't resolve dependencies of the NotesService (?)… The dependency at index [0] appears to be undefined at runtime ». Son constructeur est constructor(private readonly stagiaires: StagiairesService) et StagiairesService est bien dans les providers. Quelles sont les deux causes possibles ? Comment vérifier ?

Voir le corrigé

(1) Un import circulaire entre notes.service.ts et stagiaires.service.ts, direct ou indirect : vérifier si stagiaires.service.ts importe quelque chose qui importe notes.service.ts. (2) import type { StagiairesService } au lieu de import { StagiairesService }: le type est effacé et aucune classe n'est disponible à l'exécution.

Activité 3 Corriger avec forwardRef

Créez volontairement le cycle entre NotesService et StagiairesService: les notes vérifient que le stagiaire existe, et les stagiaires affichent leur moyenne en la demandant aux notes. Constatez l'erreur, puis corrigez-la avec forwardRef. Vérifiez que les deux routes fonctionnent.

Voir le corrigé

Appliquez @Inject(forwardRef(() => …)) dans les deux constructeurs, comme dans la leçon. Les deux routes fonctionnent car les services ne s'utilisent mutuellement que dans leurs méthodes, jamais dans leurs constructeurs.

Activité 4 Supprimer le cycle

Reprenez le cycle de l'activité 3 et supprimez-le sans forwardRef. Indice : qui a réellement besoin de qui ? La moyenne d'un stagiaire est-elle une information des stagiaires ou des notes ?

Voir le corrigé

La moyenne se calcule à partir des notes : c'est une méthode de NotesService (moyenneDe(stagiaireId)). StagiairesService n'a plus besoin des notes ; la route « stagiaire avec sa moyenne » est servie par un contrôleur qui appelle les deux services, ou par un service de plus haut niveau (BulletinsService) qui dépend des deux. Le cycle disparaît : Notes → Stagiaires, et Bulletins → Notes et Stagiaires.

À retenir

  • Un cycle (A → B → A) empêche le conteneur de créer les providers ou les modules.
  • « appears to be undefined at runtime » : import circulaire entre fichiers, ou import type sur une classe injectée.
  • forwardRef(() => X) avec @Inject, des deux côtés, pour les services ; dans imports, pour les modules.
  • Avec forwardRef, ne pas utiliser l'autre service dans le constructeur.
  • Meilleure solution : supprimer le cycle (service extrait, dépendance inversée, événements).