À propos de ce cours
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.
Un jeton, et quatre façons de produire la valeur injectée.
Les jetons
| Jeton | Exemple | Injection |
|---|---|---|
| Classe | provide: StagiairesService | par le type : private s: StagiairesService |
| Classe abstraite (contrat) | provide: Notificateur | par le type : private n: Notificateur |
| Chaîne | provide: 'CONFIG_APP' | @Inject('CONFIG_APP') private c: ConfigApp |
| Symbol | provide: 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 ;
InscriptionsServicen'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 êtreasync, 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.