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

Les modules NestJS (@Module : imports, controllers, providers, exports)

Cette leçon détaille le décorateur @Module et ses quatre propriétés : controllers, providers, imports et exports. À travers une application complète en deux modules (groupes et stagiaires), exécutée e...

0 chapitres 0 QCM Accès gratuit
Les modules NestJS (@Module : imports, controllers, providers, exports)

À propos de ce cours

Cette leçon détaille le décorateur @Module et ses quatre propriétés : controllers, providers, imports et exports. À travers une application complète en deux modules (groupes et stagiaires), exécutée et testée, elle explique l'encapsulation des providers, la façon de partager un service entre modules, la réexportation, l'injection dans la classe d'un module et la lecture du message d'erreur « Nest can't resolve dependencies ». Les activités font ajouter un module, corriger des erreurs de dépendances et analyser des cas d'imports.

Objectifs de la leçon

  • Connaître le rôle des quatre propriétés de @Module.
  • Partager un service entre deux modules (exports + imports).
  • Comprendre l'encapsulation des providers.
  • Lire et corriger l'erreur « Nest can't resolve dependencies ».
Les quatre propriétés de @Module
PropriétéContientSignification
controllersdes classes @Controllerles contrôleurs créés par ce module ; leurs routes sont enregistrées
providersdes classes @Injectable (ou des providers personnalisés, chapitre 07)les services créés par ce module, injectables à l'intérieur du module
exportsune partie des providers (ou des modules importés)ce que le module rend disponible aux modules qui l'importent
importsd'autres modulesles modules dont on veut utiliser les providers exportés

Retenez la règle d'or : un provider est privé à son module. Pour l'utiliser ailleurs, il faut à la fois qu'il soit exporté par son module et que ce module soit importé là où on l'utilise.

Encapsulation des providers dans les modules NestJS

StagiairesModule importe GroupesModule : il voit GroupesService (exporté) mais pas CodesService (privé).

Un exemple complet : groupes et stagiaires

Un stagiaire appartient à un groupe. La route GET /api/stagiaires/:id doit renvoyer le stagiaire avec le détail de son groupe, fourni par GroupesService. Générez la structure :

nest g mo groupes
nest g co groupes --no-spec
nest g s groupes --no-spec
nest g s groupes/codes --flat --no-spec
nest g mo stagiaires
nest g co stagiaires --no-spec
nest g s stagiaires --no-spec

Le module des groupes: un service interne (CodesService), un service exporté (GroupesService) et un contrôleur.

📄 src/groupes/codes.service.ts

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

@Injectable()
export class CodesService {          // service interne : NON exporté
  normaliser(code: string): string {
    return code.trim().toUpperCase();
  }
}

📄 src/groupes/groupes.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { CodesService } from './codes.service';

export interface Groupe {
  code: string;
  filiere: string;
}

@Injectable()
export class GroupesService {
  private groupes: Groupe[] = [
    { code: 'DEV101', filiere: 'Développement digital' },
    { code: 'DEV102', filiere: 'Développement digital' },
  ];

  constructor(private readonly codes: CodesService) {}

  findAll(): Groupe[] {
    return this.groupes;
  }

  findOne(code: string): Groupe {
    const g = this.groupes.find((x) => x.code === this.codes.normaliser(code));
    if (!g) throw new NotFoundException(`Groupe ${code} introuvable`);
    return g;
  }
}

📄 src/groupes/groupes.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { GroupesService } from './groupes.service';

@Controller('groupes')
export class GroupesController {
  constructor(private readonly groupesService: GroupesService) {}

  @Get()
  findAll() {
    return this.groupesService.findAll();
  }

  @Get(':code')
  findOne(@Param('code') code: string) {
    return this.groupesService.findOne(code);
  }
}

📄 src/groupes/groupes.module.ts

import { Module } from '@nestjs/common';
import { CodesService } from './codes.service';
import { GroupesController } from './groupes.controller';
import { GroupesService } from './groupes.service';

@Module({
  controllers: [GroupesController],
  providers: [GroupesService, CodesService],
  exports: [GroupesService],          // seul GroupesService est visible à l'extérieur
})
export class GroupesModule {}

Le module des stagiaires importe GroupesModule pour pouvoir injecter GroupesService:

📄 src/stagiaires/stagiaires.service.ts

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

@Injectable()
export class StagiairesService {
  private stagiaires = [
    { id: 1, nom: 'Ben Ali', groupe: 'DEV101' },
    { id: 2, nom: 'Mansouri', groupe: 'DEV102' },
  ];

  constructor(private readonly groupesService: GroupesService) {}   // vient de GroupesModule

  findOne(id: number) {
    const s = this.stagiaires.find((x) => x.id === id);
    if (!s) throw new NotFoundException(`Stagiaire ${id} introuvable`);
    return { ...s, groupe: this.groupesService.findOne(s.groupe) };   // groupe détaillé
  }
}

📄 src/stagiaires/stagiaires.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { StagiairesService } from './stagiaires.service';

@Controller('stagiaires')
export class StagiairesController {
  constructor(private readonly stagiairesService: StagiairesService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.stagiairesService.findOne(Number(id));
  }
}

📄 src/stagiaires/stagiaires.module.ts

import { Module } from '@nestjs/common';
import { GroupesModule } from '../groupes/groupes.module';
import { StagiairesController } from './stagiaires.controller';
import { StagiairesService } from './stagiaires.service';

@Module({
  imports: [GroupesModule],           // pour pouvoir injecter GroupesService
  controllers: [StagiairesController],
  providers: [StagiairesService],
})
export class StagiairesModule {}

📄 src/app.module.ts

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

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

Avec le préfixe global api (leçon 3.3), voici les réponses réellement obtenues :

Réponses de l'API :

GET /api/groupes → 200 [{"code":"DEV101","filiere":"Développement digital"},{"code":"DEV102","filiere":"Développement digital"}]
GET /api/groupes/dev102 → 200 {"code":"DEV102","filiere":"Développement digital"}
GET /api/stagiaires/1 → 200 {"id":1,"nom":"Ben Ali","groupe":{"code":"DEV101","filiere":"Développement digital"}}
GET /api/stagiaires/7 → 404 {"message":"Stagiaire 7 introuvable","error":"Not Found","statusCode":404}

Observez : dev102 en minuscules fonctionne grâce à CodesService, utilisé à l'intérieur de GroupesModule ; le stagiaire 1 est renvoyé avec l'objet groupe complet ; l'exception NotFoundException levée dans le service devient automatiquement une réponse 404.

Lire l'erreur de dépendance

Supprimez imports: [GroupesModule] de StagiairesModule et relancez. L'application refuse de démarrer :

Erreur au démarrage :

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

Potential solutions:
- Is StagiairesModule a valid NestJS module?
- If GroupesService is a provider, is it part of the current StagiairesModule?
- If GroupesService is exported from a separate @Module, is that module imported within StagiairesModule?
  @Module({
    imports: [ /* the Module containing GroupesService */ ]
  })

Ce message, que vous rencontrerez souvent, se lit ainsi :

  • StagiairesService (?): la classe qui n'a pas pu être créée ; le ? marque le paramètre introuvable (s'il y en a plusieurs : (JournalService, ?)).
  • GroupesService at index [0]: la dépendance manquante et sa position dans le constructeur.
  • in the StagiairesModule module: le module dans lequel NestJS l'a cherchée.

Trois causes possibles, à vérifier dans l'ordre : le provider n'est déclaré nulle part ; il est déclaré dans un autre module qui ne l'exporte pas ; le module qui l'exporte n'est pas importé.

Erreur fréquente: ajouter GroupesService dans les providers de StagiairesModule pour « faire disparaître » l'erreur. L'application démarre… mais il existe alors deux instances de GroupesService avec chacune ses propres données, et CodesService manque à son tour. La bonne correction est toujours exports + imports.

Réexporter un module

Un module peut exporter un module qu'il importe. Ceux qui l'importent reçoivent alors aussi les exports du module réexporté :

@Module({
  imports: [GroupesModule, FormationsModule],
  exports: [GroupesModule, FormationsModule],   // réexportation
})
export class ReferentielModule {}

// Ailleurs : un seul import donne accès à GroupesService ET FormationsService
@Module({ imports: [ReferentielModule], providers: [InscriptionsService] })
export class InscriptionsModule {}
Injecter dans la classe d'un module

La classe d'un module peut elle aussi recevoir des providers dans son constructeur, par exemple pour une configuration au démarrage :

@Module({ providers: [GroupesService, CodesService], exports: [GroupesService] })
export class GroupesModule {
  constructor(private readonly groupesService: GroupesService) {
    console.log(`GroupesModule : ${groupesService.findAll().length} groupes disponibles`);
  }
}

Les classes de modules elles-mêmes ne peuvent pas être injectées comme providers (cela provoquerait des dépendances circulaires). Pour lancer du code au démarrage, on préfère les événements de cycle de vie (leçon 5.6).

Activités

Activité 1 Reproduire l'exemple

Dans api-formation, créez les modules groupes et stagiaires comme dans la leçon (fichiers identiques), créez requetes/stagiaires.http avec les quatre requêtes et vérifiez les réponses. Commit : feat: modules groupes et stagiaires.

Voir le corrigé

Les quatre réponses doivent être identiques à celles de la leçon. Si l'application ne démarre pas, lisez le message d'erreur en suivant la méthode de la leçon.

Activité 2 Provoquer et lire trois erreurs

Faites successivement les trois modifications suivantes (en annulant chaque fois la précédente), relevez le message et expliquez-le : (a) retirez exports: [GroupesService]; (b) retirez CodesService des providers de GroupesModule ; (c) dans StagiairesService, injectez CodesService en second paramètre.

Voir le corrigé

(a) Nest can't resolve dependencies of the StagiairesService (?)… argument GroupesService at index [0] is available in the StagiairesModule module: le module est importé mais n'exporte plus le service. (b) …dependencies of the GroupesService (?)… argument CodesService at index [0]… in the GroupesModule module: le service interne n'est plus déclaré. (c) …dependencies of the StagiairesService (GroupesService, ?)… argument CodesService at index [1]… in the StagiairesModule module: GroupesService est trouvé (il n'y a plus de ? en première position), mais CodesService est privé à GroupesModule. Pour (c), la bonne solution n'est pas d'exporter CodesService mais de passer par une méthode de GroupesService.

Activité 3 Ajouter un module formations

Créez un FormationsModule dont le service contient deux formations ({ id, intitule }). Chaque groupe reçoit un formationId, et GET /api/groupes/:code doit renvoyer l'intitulé de la formation. Quels imports et exports faut-il ajouter ?

Voir le corrigé

FormationsModule exporte FormationsService; GroupesModule importe FormationsModule et injecte FormationsService dans GroupesService. StagiairesModule n'a rien à changer : il ne dépend que de GroupesModule. La correction complète figure dans le TP 2.

Activité 4 Analyser une architecture

Pour chaque situation, dites si l'injection fonctionne :

  1. A importe B ; B exporte BService; un service de A injecte BService.
  2. A importe B ; B importe C ; C exporte CService; un service de A injecte CService.
  3. Même cas, mais B déclare aussi exports: [C].
  4. B déclare BService dans ses providers sans l'exporter ; le contrôleur de B l'injecte.
Voir le corrigé

1 : oui. 2 : non, les imports ne sont pas transitifs : A ne voit que ce que B exporte. 3 : oui, B réexporte C. 4 : oui, un provider est toujours visible dans son propre module.

Activité 5 Deux instances ?

Ajoutez dans GroupesService un compteur de consultations incrémenté par findOne et une route GET /api/groupes/stats/consultations. Appelez plusieurs fois /api/groupes/dev101 puis /api/stagiaires/1: le compteur est-il partagé ? Ajoutez ensuite (à tort) GroupesService et CodesService aux providers de StagiairesModule et recommencez.

Voir le corrigé

Dans la bonne configuration, le compteur est partagé : une seule instance de GroupesService existe, utilisée par les deux modules. Avec la mauvaise, StagiairesModule crée sa propre instance : les consultations faites via /api/stagiaires ne sont plus comptées par la route des groupes. Avec une vraie base de données, l'effet serait moins visible mais les problèmes de cache ou de configuration seraient les mêmes.

À retenir

  • controllers et providers déclarent ce que le module crée.
  • Un provider est privé à son module ; exports le rend visible aux modules qui importent.
  • imports donne accès aux exports d'autres modules ; les imports ne sont pas transitifs.
  • « Nest can't resolve dependencies » : chercher la déclaration, l'export, puis l'import.
  • Ne jamais redéclarer un provider d'un autre module : on créerait une seconde instance.