À propos de ce cours
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.
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 :
- 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.
- 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.
- 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).