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

Les providers personnalisés (useClass, useValue, useFactory, useExisting)

Cette leçon présente les providers personnalisés, qui permettent de tout injecter : jetons d'injection (classe, chaîne, Symbol, classe abstraite comme contrat) et @Inject, valeur toute prête avec useV...

0 chapitres 0 QCM Accès gratuit
Les providers personnalisés (useClass, useValue, useFactory, useExisting)

À propos de ce cours

Cette leçon présente les providers personnalisés, qui permettent de tout injecter : jetons d'injection (classe, chaîne, Symbol, classe abstraite comme contrat) et @Inject, valeur toute prête avec useValue, implémentation choisie selon l'environnement avec useClass, valeur calculée éventuellement asynchrone avec useFactory et inject, alias avec useExisting. Un module complet est exécuté en développement puis en production, et un test avec @nestjs/testing montre comment remplacer chaque provider par une version de test. Activités corrigées.

Objectifs de la leçon

  • Comprendre la forme longue d'un provider et la notion de jeton.
  • Injecter une valeur avec useValue et un jeton chaîne ou Symbol.
  • Choisir une implémentation selon l'environnement avec useClass.
  • Calculer un provider, même de façon asynchrone, avec useFactory.
  • Créer un alias avec useExisting.
  • Remplacer des providers dans un test.
La forme longue d'un provider

Quand vous écrivez providers: [StagiairesService], c'est un raccourci pour :

providers: [
  { provide: StagiairesService, useClass: StagiairesService },
  //  ↑ le JETON (ce qu'on demande)   ↑ la RECETTE (comment le fabriquer)
]

Le jeton est la clé sous laquelle le provider est enregistré ; la recette indique comment obtenir la valeur. Séparer les deux ouvre beaucoup de possibilités.

Les quatre recettes de providers NestJS

Un jeton, et quatre façons de produire la valeur injectée.

Les jetons
JetonExempleInjection
Classeprovide: StagiairesServicepar le type : private s: StagiairesService
Classe abstraite (contrat)provide: Notificateurpar le type : private n: Notificateur
Chaîneprovide: 'CONFIG_APP'@Inject('CONFIG_APP') private c: ConfigApp
Symbolprovide: HORLOGE (Symbol('HORLOGE'))@Inject(HORLOGE) private h: Horloge

On place les jetons chaîne ou Symbol dans une constante exportée pour éviter les fautes de frappe. Un Symbol est garanti unique : deux bibliothèques ne peuvent pas entrer en collision. La classe abstraite est une astuce très utilisée : elle sert à la fois de contrat (comme une interface) et de jeton (car elle existe à l'exécution).

📄 src/jetons.ts

export interface ConfigApp {
  etablissement: string;
  maxStagiairesParGroupe: number;
}

export const CONFIG_APP = 'CONFIG_APP';                 // jeton chaîne
export const HORLOGE = Symbol('HORLOGE');                 // jeton symbole (unique)
export const CONNEXION = 'CONNEXION';

export interface Horloge {
  maintenant(): Date;
}

📄 src/notificateurs.ts

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

export abstract class Notificateur {                      // classe abstraite = jeton + contrat
  abstract envoyer(a: string, message: string): string;
}

@Injectable()
export class NotificateurEmail extends Notificateur {
  envoyer(a: string, message: string) {
    return `e-mail à ${a} : ${message}`;
  }
}

@Injectable()
export class NotificateurConsole extends Notificateur {
  envoyer(a: string, message: string) {
    return `[développement] ${a} : ${message}`;
  }
}
Les quatre recettes dans un module

📄 src/inscriptions.module.ts

import { Module } from '@nestjs/common';
import { InscriptionsController } from './inscriptions.controller';
import { InscriptionsService } from './inscriptions.service';
import { CONFIG_APP, CONNEXION, HORLOGE } from './jetons';
import { Notificateur, NotificateurConsole, NotificateurEmail } from './notificateurs';

@Module({
  controllers: [InscriptionsController],
  providers: [
    InscriptionsService,                                     // raccourci de { provide: X, useClass: X }

    // 1. useValue : une valeur toute prête
    { provide: CONFIG_APP, useValue: { etablissement: 'Institut de formation de Tunis', maxStagiairesParGroupe: 24 } },
    { provide: HORLOGE, useValue: { maintenant: () => new Date('2026-09-15T08:00:00Z') } },

    // 2. useClass : la classe à instancier dépend du contexte
    {
      provide: Notificateur,
      useClass: process.env.NODE_ENV === 'production' ? NotificateurEmail : NotificateurConsole,
    },

    // 3. useFactory : une fonction (éventuellement async) qui reçoit d'autres providers
    {
      provide: CONNEXION,
      inject: [CONFIG_APP],
      useFactory: async (config: { etablissement: string }) => {
        await new Promise((r) => setTimeout(r, 30));            // simule l'ouverture d'une connexion
        console.log(`connexion ouverte pour ${config.etablissement}`);
        return { url: 'mysql://localhost/formation', ouverteA: new Date().toISOString() };
      },
    },

    // 4. useExisting : un second nom pour un provider existant
    { provide: 'NOTIFICATEUR', useExisting: Notificateur },
  ],
})
export class InscriptionsModule {}

Le service reçoit chaque provider, par son type ou par @Inject(jeton):

📄 src/inscriptions.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { CONFIG_APP, CONNEXION, ConfigApp, Horloge, HORLOGE } from './jetons';
import { Notificateur } from './notificateurs';

@Injectable()
export class InscriptionsService {
  constructor(
    @Inject(CONFIG_APP) private readonly config: ConfigApp,       // useValue
    @Inject(HORLOGE) private readonly horloge: Horloge,           // useValue (objet)
    private readonly notificateur: Notificateur,                  // useClass (choisie selon l'environnement)
    @Inject(CONNEXION) private readonly connexion: { url: string; ouverteA: string },   // useFactory async
    @Inject('NOTIFICATEUR') private readonly alias: Notificateur, // useExisting
  ) {}

  inscrire(nom: string) {
    return {
      etablissement: this.config.etablissement,
      date: this.horloge.maintenant().toISOString().slice(0, 10),
      message: this.notificateur.envoyer(nom, 'inscription confirmée'),
      connexion: this.connexion.url,
      aliasMemeInstance: this.alias === this.notificateur,
    };
  }
}

Exécution en développement :

connexion ouverte pour Institut de formation de Tunis
POST /api/inscriptions/Ben%20Ali → 201 {"etablissement":"Institut de formation de Tunis","date":"2026-09-15","message":"[développement] Ben Ali : inscription confirmée","connexion":"mysql://localhost/formation","aliasMemeInstance":true}

Même code, lancé avec NODE_ENV=production :

connexion ouverte pour Institut de formation de Tunis
POST /api/inscriptions/Ben%20Ali → 201 {"etablissement":"Institut de formation de Tunis","date":"2026-09-15","message":"e-mail à Ben Ali : inscription confirmée","connexion":"mysql://localhost/formation","aliasMemeInstance":true}
Ce qu'il faut observer
  • useValue: la configuration et l'horloge sont des valeurs toutes prêtes. L'horloge fixe (15 septembre 2026) rend les dates prévisibles : idéal pour les tests.
  • useClass: seul le message change entre les deux exécutions ; InscriptionsService n'a pas été modifié. C'est tout l'intérêt de dépendre d'un contrat.
  • useFactory: la fonction reçoit les providers listés dans inject (ici la configuration), peut être async, et NestJS attend sa promesse avant de démarrer (« connexion ouverte » s'affiche avant toute requête). C'est ainsi que les modules de base de données ouvrent leur connexion.
  • useExisting: 'NOTIFICATEUR' est un second nom pour le même provider (aliasMemeInstance: true), utile pour la compatibilité lors d'un renommage.

useClass ou useFactory ? useClass laisse NestJS créer la classe et injecter ses dépendances. useFactory vous donne la main : à vous de construire la valeur. Préférez useClass quand il s'agit simplement de choisir une classe ; useFactory quand la création demande un calcul, une attente ou une bibliothèque externe.

Remplacer les providers dans un test

Avec le paquet @nestjs/testing (installé dans tout projet généré par la CLI), on construit un module de test en fournissant ses propres versions des dépendances :

import { Test } from '@nestjs/testing';
import { InscriptionsService } from './inscriptions.service';
import { CONFIG_APP, CONNEXION, HORLOGE } from './jetons';
import { Notificateur } from './notificateurs';

async function testerInscription() {
  const envois: string[] = [];
  const module = await Test.createTestingModule({
    providers: [
      InscriptionsService,
      { provide: CONFIG_APP, useValue: { etablissement: 'Établissement de test', maxStagiairesParGroupe: 2 } },
      { provide: HORLOGE, useValue: { maintenant: () => new Date('2027-01-01T00:00:00Z') } },
      { provide: Notificateur, useValue: { envoyer: (a: string, m: string) => (envois.push(a), 'faux envoi') } },
      { provide: CONNEXION, useValue: { url: 'memoire://test' } },
      { provide: 'NOTIFICATEUR', useExisting: Notificateur },
    ],
  }).compile();

  const service = module.get(InscriptionsService);
  const resultat = service.inscrire('Testeur');
  console.log(resultat);
  console.log('envois enregistrés :', envois);
  console.log(resultat.date === '2027-01-01' ? '✓ test réussi' : '✗ test échoué');
}
testerInscription();

Exécution du test :

{
  etablissement: 'Établissement de test',
  date: '2027-01-01',
  message: 'faux envoi',
  connexion: 'memoire://test',
  aliasMemeInstance: true
}
envois enregistrés : [ 'Testeur' ]
✓ test réussi

Aucun e-mail, aucune connexion, une date maîtrisée : le service est testé seul. Le chapitre 17 généralise cette technique avec Jest (overrideProvider(), fonctions espions).

Activités

Activité 1 Configuration par useValue

Dans api-formation, créez un jeton REGLES_NOTES et injectez { min: 0, max: 20, seuilReussite: 10 } avec useValue. Utilisez-le dans NotesService à la place des nombres écrits en dur. Quel avantage pour un établissement qui note sur 100 ?

Voir le corrigé
export const REGLES_NOTES = Symbol('REGLES_NOTES');
export interface ReglesNotes { min: number; max: number; seuilReussite: number }

// module
{ provide: REGLES_NOTES, useValue: { min: 0, max: 20, seuilReussite: 10 } }

// service
constructor(@Inject(REGLES_NOTES) private readonly regles: ReglesNotes) {}

ajouter(…, valeur: number) {
  if (valeur < this.regles.min || valeur > this.regles.max) {
    throw new BadRequestException(`La note doit être comprise entre ${this.regles.min} et ${this.regles.max}`);
  }
}

Passer à une notation sur 100 ne demande de changer qu'une valeur dans le module, sans toucher au service ni aux tests (qui peuvent fournir leurs propres règles).

Activité 2 useClass selon l'environnement

Créez une classe abstraite StockageFichiers (enregistrer(nom, contenu)) et deux implémentations : StockageDisque (écrit dans un dossier) et StockageMemoire (garde dans une Map). Choisissez l'implémentation avec la variable d'environnement STOCKAGE. Vérifiez que le service qui l'utilise n'a pas besoin de changer.

Voir le corrigé
export abstract class StockageFichiers {
  abstract enregistrer(nom: string, contenu: string): Promise<string>;
}

@Injectable()
export class StockageMemoire extends StockageFichiers {
  private fichiers = new Map<string, string>();
  async enregistrer(nom: string, contenu: string) {
    this.fichiers.set(nom, contenu);
    return `memoire://${nom}`;
  }
}

@Injectable()
export class StockageDisque extends StockageFichiers {
  async enregistrer(nom: string, contenu: string) {
    const chemin = join(process.cwd(), 'stockage', nom);
    await mkdir(dirname(chemin), { recursive: true });
    await writeFile(chemin, contenu);
    return chemin;
  }
}

// module
{ provide: StockageFichiers, useClass: process.env.STOCKAGE === 'disque' ? StockageDisque : StockageMemoire }

Activité 3 useFactory asynchrone

Écrivez un provider JOURS_FERIES avec useFactory : une fonction async qui « charge » (après 100 ms) la liste des jours fériés de l'année. Injectez-le dans un service qui expose estFerie(date). Que se passe-t-il au démarrage si la fabrique lève une erreur ?

Voir le corrigé
{
  provide: JOURS_FERIES,
  useFactory: async () => {
    await new Promise((r) => setTimeout(r, 100));
    return ['2026-01-01', '2026-03-20', '2026-04-09', '2026-05-01', '2026-07-25', '2026-08-13', '2026-10-15'];
  },
}

@Injectable()
export class CalendrierService {
  constructor(@Inject(JOURS_FERIES) private readonly feries: string[]) {}
  estFerie(date: string) {
    return this.feries.includes(date);
  }
}

NestJS attend la fabrique avant de terminer le démarrage. Si elle lève une erreur, l'application ne démarre pas et affiche l'erreur, ce qui est voulu pour une ressource indispensable. Pour une ressource facultative, capturez l'erreur dans la fabrique et renvoyez une valeur de repli.

Activité 4 Tester sans dépendances réelles

En vous inspirant du test de la leçon, écrivez un test de NotesService qui fournit un faux StagiairesService (avec useValue) dont findOne lève NotFoundException pour l'identifiant 99, et vérifiez que ajouter(99, 'SQL', 12) lève bien cette exception.

Voir le corrigé
const module = await Test.createTestingModule({
  providers: [
    NotesService,
    {
      provide: StagiairesService,
      useValue: {
        findOne: (id: number) => {
          if (id === 99) throw new NotFoundException('Stagiaire 99 introuvable');
          return { id, nom: 'Test' };
        },
      },
    },
  ],
}).compile();

const service = module.get(NotesService);
try {
  service.ajouter(99, 'SQL', 12);
  console.log('✗ aucune exception');
} catch (e) {
  console.log(e instanceof NotFoundException ? '✓ NotFoundException levée' : '✗ mauvaise exception');
}

NotesService est testé sans aucun stagiaire réel. Le faux service ne contient que la méthode utilisée.

À retenir

  • Un provider = un jeton (provide) + une recette ; providers: [X] est un raccourci de useClass: X.
  • Jetons : classe, classe abstraite (contrat), chaîne ou Symbol avec @Inject(jeton).
  • useValue : valeur prête (configuration, faux service de test).
  • useClass : choisir une implémentation (selon l'environnement) sans modifier les consommateurs.
  • useFactory (+ inject) : valeur calculée, éventuellement async, attendue au démarrage ; useExisting : alias.
  • @nestjs/testing permet de remplacer chaque provider pour tester un service seul.