Accueil / Articles / Diagnostic de l’erreur « NestJS Cannot Resolve Dependencies », cause par cause

Diagnostic de l’erreur « NestJS Cannot Resolve Dependencies », cause par cause

Apprenez à interpréter les erreurs de résolution des dépendances dans NestJS et à corriger leurs cinq causes les plus fréquentes : absence de fournisseurs, modules non exportés, cycles, tokens incorrects et modules de test sans enveloppe.

1046 mots

Tôt ou tard, chaque projet NestJS s’arrête lors du démarrage avec un message indiquant que le conteneur ne peut pas résoudre un argument du constructeur. La formulation semble obscure mais est précise, et elle renvoie presque toujours à l’un de cinq erreurs de configuration. Ce guide montre comment interpréter ce message et explique chaque cause dans l’ordre dans lequel vous devez les vérifier.

Interprétation du message d’erreur

Une erreur typique a l’aspect suivant :

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

La classe mentionnée dans le message (UsersService) est celle que Nest tentait d’instancier. À l’intérieur des parenthèses, chaque paramètre du constructeur est listé, et le ? indique celui qui a posé problème ; index [0] précise cette position. La dernière partie indique le module dont l’espace de noms a été consulté. Chaque solution présentée ci-dessous permet de rendre cet argument visible.

Cause 1 : la classe n’a jamais été enregistrée comme fournisseur

Le cas le plus simple : le fichier de service existe, mais aucun module ne le liste. Nest ne gère que les classes présentes dans l’array providers d’un module.

@Module({
  controllers: [UsersController],
  providers: [UsersService], // <-- missing? that's your error
})
export class UsersModule {}

La commande CLI nest generate service met à jour le module pour vous. C’est ici que l’étape est souvent oubliée pour les services écrits manuellement.

Cause 2 : le fournisseur se trouve dans un autre module qui n’est ni importé ni exporté

C’est la variante la plus fréquente entre modules. AuthService est enregistré dans AuthModule, et UsersService l’injecte, mais UsersModule ne contient aucune importation vers AuthModule:

@Module({
  imports: [AuthModule], // <-- without this, AuthService is invisible here
  providers: [UsersService],
})
export class UsersModule {}

exports:

@Module({
  providers: [AuthService],
  exports: [AuthService], // <-- other modules can only use what you export
})
export class AuthModule {}

Un modèle mental utile : un fournisseur est privé à son module sauf s’il est exporté, et un fournisseur exporté reste invisible pour les modules qui n’importent pas son propriétaire. Les deux conditions doivent être remplies.

Cause 3 : deux services dépendent l’un de l’autre

Si UsersService a besoin de OrdersService et que OrdersService a besoin de UsersService, aucun des deux ne peut être construit en premier. Nest propose forwardRef() pour différer la résolution. Il est appliqué au niveau du module lors de l’import :

// users.module.ts
@Module({
  imports: [forwardRef(() => OrdersModule)],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

et à nouveau au point d’injection dans le constructeur du service :

// users.service.ts
constructor(
  @Inject(forwardRef(() => OrdersService))
  private ordersService: OrdersService,
) {}

L’image miroir est nécessaire de l’autre côté (OrdersModule qui importe forwardRef(() => UsersModule)). Considérez cela comme un correctif temporaire plutôt que comme une solution définitive. Un cycle signifie généralement que le comportement partagé se trouve au mauvais endroit ; en le déplaçant dans un troisième module que les deux peuvent importer, on élimine à la fois le cycle et la nécessité de forwardRef.

Un piège similaire : les imports circulaires de fichiers, souvent via des fichiers index.ts intermédiaires, peuvent rendre une référence à une classe undefined au moment de la décoration. Nest signale alors une dépendance non résolue, même si les modules semblent corrects.

Cause 4 : le jeton d’injection ne correspond pas

Les fournisseurs personnalisés sont enregistrés sous un jeton, et l’injection doit utiliser exactement ce même jeton. Prenons par exemple un fournisseur de valeurs identifié par une chaîne de caractères :

{
  provide: 'CONFIG_OPTIONS',
  useValue: configOptions,
}

Déclarer un paramètre de constructeur uniquement avec son type ne permettra pas de le trouver, car ce type n’est pas un token. Utilisez @Inject() avec la même chaîne de caractères :

constructor(@Inject('CONFIG_OPTIONS') private config: ConfigOptions) {}

Les types en TypeScript disparaissent également à l’exécution, c’est pourquoi une interface ne peut jamais servir de token par elle-même.

Les repositories de TypeORM échouent de la même manière. Définir le paramètre comme une classe de repository ne correspond pas au token enregistré par Nest :

// Wrong
constructor(private repo: UserRepository) {}

La forme fonctionnelle utilise @InjectRepository() avec l’entité :

// Right
constructor(
  @InjectRepository(User)
  private repo: Repository<User>,
) {}

Pour que ce token existe d’abord, le module doit également importer TypeOrmModule.forFeature([User]).

Cause 5 : le module de test manque de fournisseurs ou de mocks

Parfois, l’application démarre correctement tandis que les tests unitaires génèrent la même erreur. Test.createTestingModule crée un conteneur entièrement nouveau ne contenant que ce que vous déclarez, de sorte que toutes les dépendances de la classe testée doivent être fournies, généralement sous forme d’objet simulé lié au bon token :

const module = await Test.createTestingModule({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository, // <-- every dependency needs one of these
    },
  ],
}).compile();

getRepositoryToken(User) produit le même token que @InjectRepository(User) cherche. Si l’erreur n’apparaît qu’en test, le câblage en environnement de production est correct et la configuration des tests est incomplète.

Une liste de vérification pour le débogage

Vérifiez ces points dans l’ordre suivant :

  1. La classe est-elle listée dans les providers du module concerné ?
  2. Le module propriétaire est-il importé là où le provider est utilisé, et exporte-t-il ce provider ?
  • Y a-t-il une dépendance circulaire, que ce soit entre des services ou entre des fichiers ? Corrigez cela avec forwardRef, puis refaites le refactoring.
  • Pour les fournisseurs et répertoires personnalisés, le token d’injection correspond-il exactement à l’enregistrement ?
  • L’échec se produit-il uniquement dans les tests ? Ajoutez les fournisseurs ou mocks manquants.
  • Points clés

    • Le ? et l’index dans le message indiquent précisément quel argument de constructeur manque, et dans quel scope de module.
    • La visibilité dans Nest est explicite : enregistrer, exporter, importer.
    • forwardRef cache les cycles ; en extrayant un module partagé, on les corrige.
    • C’est le token, et non les types TypeScript, qui gère l’injection en temps de exécution.
    • Les modules de test sont des conteneurs séparés et nécessitent leur propre configuration complète.

    Pour en savoir plus sur la manière de préserver des limites de modules saines à mesure que le codebase grandit, consultez six règles DDD pour structurer les domaines dans les applications NestJS.