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

Injecter un service dans un contrôleur

Cette leçon détaille les façons de recevoir un provider dans un contrôleur NestJS : l'injection par le constructeur avec private readonly (la forme recommandée), l'injection par propriété avec @Inject...

0 chapitres 0 QCM Accès gratuit
Injecter un service dans un contrôleur

À propos de ce cours

Cette leçon détaille les façons de recevoir un provider dans un contrôleur NestJS : l'injection par le constructeur avec private readonly (la forme recommandée), l'injection par propriété avec @Inject, les dépendances facultatives avec @Optional et l'accès programmatique au conteneur avec ModuleRef. Un contrôleur complet est exécuté pour montrer chaque forme. La leçon donne aussi les bonnes pratiques (dépendances explicites, nombre de dépendances raisonnable, pas de logique dans le constructeur), les erreurs courantes et des activités corrigées.

Objectifs de la leçon

  • Injecter un ou plusieurs services par le constructeur.
  • Connaître l'injection par propriété et ses limites.
  • Déclarer une dépendance facultative avec @Optional.
  • Obtenir un provider à la demande avec ModuleRef.
  • Éviter les erreurs courantes d'injection.

Formes d'injection dans NestJS

Le constructeur avant tout ; les autres formes pour des cas particuliers.

1. L'injection par le constructeur

C'est la forme normale, utilisée dans 99 % des cas. Vous la connaissez depuis le chapitre 03 :

@Controller('stagiaires')
export class StagiairesController {
  constructor(
    private readonly stagiairesService: StagiairesService,
    private readonly notesService: NotesService,
  ) {}
}
  • private: la propriété n'est utilisée qu'à l'intérieur du contrôleur.
  • readonly: elle ne sera jamais réaffectée (le compilateur l'interdit).
  • L'ordre des paramètres n'a pas d'importance pour NestJS, qui se fie aux types.
  • Le type doit être une classe (ou un jeton avec @Inject, leçon 7.4) : une interface disparaît à la compilation.

Ne mettez pas de logique dans le constructeur (appel à un service, chargement de données) : il est exécuté pendant la construction du graphe d'objets, avant que l'application soit prête. Utilisez onModuleInit (leçon 5.6).

Un contrôleur qui utilise les autres formes

📄 src/services.ts

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

@Injectable()
export class GroupesService {
  findAll() {
    return ['DEV101', 'DEV102'];
  }
}

@Injectable()
export class JournalService {
  private lignes: string[] = [];
  ecrire(message: string) {
    this.lignes.push(message);
  }
  get nombre() {
    return this.lignes.length;
  }
}

@Injectable()
export class StatistiquesService {
  calculer() {
    return { groupes: 2 };
  }
}

📄 src/groupes.controller.ts

import { Controller, Get, Inject, Optional } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { GroupesService, JournalService, StatistiquesService } from './services';

class CacheService {}   // n'est déclaré dans aucun module

@Controller('groupes')
export class GroupesController {
  // 2. Injection par propriété (rare : réservée aux cas d'héritage)
  @Inject(JournalService)
  private readonly journal: JournalService;

  // 1. Injection par le constructeur (la forme normale)
  constructor(
    private readonly groupesService: GroupesService,
    @Optional() private readonly cache?: CacheService,      // 3. dépendance facultative
    private readonly moduleRef?: ModuleRef,                  // 4. accès programmatique au conteneur
  ) {}

  @Get()
  findAll() {
    this.journal.ecrire('liste des groupes');
    return {
      groupes: this.groupesService.findAll(),
      cacheDisponible: this.cache !== undefined,
      lignesJournal: this.journal.nombre,
    };
  }

  @Get('statistiques')
  statistiques() {
    const stats = this.moduleRef!.get(StatistiquesService);  // obtenu à la demande
    return stats.calculer();
  }
}

Réponses obtenues :

GET /api/groupes → 200 {"groupes":["DEV101","DEV102"],"cacheDisponible":false,"lignesJournal":1}
GET /api/groupes → 200 {"groupes":["DEV101","DEV102"],"cacheDisponible":false,"lignesJournal":2}
GET /api/groupes/statistiques → 200 {"groupes":2}
2. L'injection par propriété

@Inject(JournalService) sur une propriété demande à NestJS de la remplir après la création de l'objet. Le compteur lignesJournal qui passe de 1 à 2 montre que le journal (un singleton) a bien été injecté. Cette forme est déconseillée en général : la dépendance n'apparaît pas dans le constructeur, et la propriété n'est pas disponible pendant celui-ci. Elle est utile dans un cas précis : une classe de base héritée par plusieurs classes, pour éviter que chaque classe fille répète le super(…) avec toutes les dépendances.

3. Les dépendances facultatives

CacheService n'est déclaré dans aucun module. Sans @Optional(), l'application refuserait de démarrer (« Nest can't resolve dependencies… »). Avec, NestJS injecte undefined: la réponse indique cacheDisponible: false. Le code doit alors tester la présence (this.cache?.lire(…)).

4. ModuleRef : obtenir un provider à la demande

ModuleRef (de @nestjs/core) est le conteneur lui-même, injectable comme n'importe quel provider. moduleRef.get(Classe) renvoie l'instance d'un provider du module. C'est utile quand le provider à utiliser n'est connu qu'à l'exécution (choisir un service selon un paramètre), mais à éviter quand l'injection classique suffit : la dépendance devient invisible.

// Exemple d'usage légitime : choisir une stratégie à l'exécution
@Injectable()
export class ExportService {
  constructor(private readonly moduleRef: ModuleRef) {}

  exporter(format: 'pdf' | 'csv', donnees: unknown[]) {
    const generateur = format === 'pdf' ? this.moduleRef.get(ExportPdf) : this.moduleRef.get(ExportCsv);
    return generateur.generer(donnees);
  }
}
Erreurs courantes
SymptômeCause
« Nest can't resolve dependencies of the XController (?) »service non déclaré dans providers, ou non exporté/importé (leçon 5.2)
Le message parle d'un argument « undefined at runtime »import type utilisé pour une classe injectée, ou import circulaire entre fichiers (leçon 7.6)
Erreur « Cannot read properties of undefined » dans une méthode@Injectable() oublié sur la classe qui reçoit (leçon 7.2)
Type de paramètre écrit comme une interfacel'interface n'existe pas à l'exécution : utiliser une classe (abstraite) ou un jeton + @Inject
this.service undefined alors que tout est déclaréméthode passée comme fonction de rappel (liste.map(this.service.transformer)) : this est perdu ; écrire (x) => this.service.transformer(x)

Combien de dépendances ? Si un constructeur reçoit plus de 4 ou 5 services, c'est souvent le signe que la classe fait trop de choses. Découpez-la, ou regroupez une partie de la logique dans un service intermédiaire.

Activités

Activité 1 Injecter deux services

Créez un TableauDeBordController (GET /api/tableau-de-bord) qui injecte StagiairesService et GroupesService et renvoie { stagiaires: n, groupes: m }. Où déclarer le contrôleur pour que les deux services soient disponibles ?

Voir le corrigé

Dans un module qui a accès aux deux services : soit le même module que ces services, soit un nouveau TableauDeBordModule qui importe StagiairesModule et GroupesModule, à condition qu'ils exportent leurs services (leçon 5.2).

Activité 2 Dépendance facultative

Ajoutez à NotesService une dépendance facultative NotificationsService qui envoie un message au stagiaire après chaque note. L'application doit fonctionner avec ou sans ce service déclaré. Testez les deux cas.

Voir le corrigé
@Injectable()
export class NotesService {
  constructor(
    private readonly stagiairesService: StagiairesService,
    @Optional() private readonly notifications?: NotificationsService,
  ) {}

  ajouter(stagiaireId: number, matiere: string, valeur: number) {
    // … enregistrement …
    this.notifications?.envoyer(stagiaireId, `Nouvelle note en ${matiere} : ${valeur}`);
    return note;
  }
}

Sans NotificationsService dans les providers, la note est enregistrée sans message ; avec, le message est envoyé. Le ? du paramètre et le ?. de l'appel sont indispensables.

Activité 3 Corriger l'erreur

Ce contrôleur démarre mais renvoie 500 sur GET /api/stagiaires/noms. Trouvez la cause.

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

  @Get('noms')
  noms() {
    return this.stagiairesService.findAll().map(this.formater);
  }

  formater(s: Stagiaire) {
    return this.stagiairesService.nomComplet(s);
  }
}
Voir le corrigé

.map(this.formater) passe la méthode sans son objet : à l'intérieur, this vaut undefined, donc this.stagiairesService provoque l'erreur. Correction : .map((s) => this.formater(s)). Une fonction fléchée conserve le this du contexte (leçon 4.1).

Activité 4 Choisir à l'exécution avec ModuleRef

Écrivez deux services ExportCsv et ExportJson ayant une méthode generer(liste), et une route GET /api/stagiaires/export?format=csv|json qui utilise ModuleRef pour choisir le service. Proposez ensuite une version sans ModuleRef, en injectant les deux services. Laquelle préférez-vous ?

Voir le corrigé

Avec deux implémentations seulement, injecter les deux et choisir avec un if est plus lisible et plus facile à tester : les dépendances sont visibles dans le constructeur. ModuleRef devient intéressant quand les implémentations sont nombreuses ou chargées dynamiquement.

À retenir

  • Injection par le constructeur avec private readonly : la forme à utiliser par défaut.
  • NestJS se fie aux types des paramètres, pas à leur ordre ; le type doit exister à l'exécution.
  • @Inject sur une propriété : rare, utile pour l'héritage.
  • @Optional() : dépendance facultative, injectée à undefined si absente.
  • ModuleRef.get() : obtenir un provider à la demande, pour des cas avancés.