À propos de ce cours
Objectifs de la leçon
- Connaître les trois portées de providers et leur effet.
- Créer un provider de portée requête et lire la requête courante.
- Créer un provider transitoire.
- Comprendre la propagation de la portée et son coût.
- Choisir la bonne portée selon le besoin.
Singleton : une instance ; requête : une par requête ; transitoire : une par consommateur.
Les trois portées
| Portée | Déclaration | Instances |
|---|---|---|
| DEFAULT (singleton) | @Injectable() | une seule, créée au démarrage, partagée par tous |
| REQUEST | @Injectable({ scope: Scope.REQUEST }) | une nouvelle par requête HTTP, détruite après la réponse |
| TRANSIENT | @Injectable({ scope: Scope.TRANSIENT }) | une nouvelle pour chaque classe qui l'injecte |
La démonstration
Chaque provider reçoit un numéro d'instance au moment de sa création. Le contrôleur injecte un provider de chaque portée :
📄 src/portees.ts
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import type { Request } from 'express';
let numero = 0;
const suivant = () => ++numero;
// 1. Portée par défaut : une seule instance pour toute l'application
@Injectable()
export class CompteurGlobal {
readonly instance = suivant();
private total = 0;
incrementer() {
return ++this.total;
}
}
// 2. Portée « requête » : une nouvelle instance pour chaque requête HTTP
@Injectable({ scope: Scope.REQUEST })
export class ContexteRequete {
readonly instance = suivant();
readonly utilisateur: string;
constructor(@Inject(REQUEST) requete: Request) {
this.utilisateur = String(requete.headers['x-utilisateur'] ?? 'anonyme');
}
}
// 3. Portée « transitoire » : une nouvelle instance pour chaque classe qui l'injecte
@Injectable({ scope: Scope.TRANSIENT })
export class Journal {
readonly instance = suivant();
private contexte = '?';
definirContexte(c: string) {
this.contexte = c;
}
ecrire(m: string) {
return `[${this.contexte}#${this.instance}] ${m}`;
}
}
@Injectable()
export class GroupesService {
constructor(private readonly journal: Journal) {
this.journal.definirContexte('Groupes');
}
info() {
return this.journal.ecrire('liste demandée');
}
}
@Injectable()
export class NotesService {
constructor(private readonly journal: Journal) {
this.journal.definirContexte('Notes');
}
info() {
return this.journal.ecrire('bulletin demandé');
}
}
📄 src/demo.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CompteurGlobal, ContexteRequete, GroupesService, NotesService } from './portees';
let controleursCrees = 0;
@Controller('demo')
export class DemoController {
constructor(
private readonly compteur: CompteurGlobal,
private readonly contexte: ContexteRequete, // portée requête → le contrôleur le devient aussi
private readonly groupes: GroupesService,
private readonly notes: NotesService,
) {
controleursCrees++;
}
@Get()
demo() {
return {
compteurGlobal: `instance ${this.compteur.instance}, appel n° ${this.compteur.incrementer()}`,
contexteRequete: `instance ${this.contexte.instance}, utilisateur ${this.contexte.utilisateur}`,
journaux: [this.groupes.info(), this.notes.info()],
controleursCrees,
};
}
}
Trois requêtes (utilisateurs sami, ines, puis sans en-tête) :
GET /api/demo → 200 {"compteurGlobal":"instance 1, appel n° 1","contexteRequete":"instance 4, utilisateur sami","journaux":["[Groupes#2] liste demandée","[Notes#3] bulletin demandé"],"controleursCrees":1}
GET /api/demo → 200 {"compteurGlobal":"instance 1, appel n° 2","contexteRequete":"instance 5, utilisateur ines","journaux":["[Groupes#2] liste demandée","[Notes#3] bulletin demandé"],"controleursCrees":2}
GET /api/demo → 200 {"compteurGlobal":"instance 1, appel n° 3","contexteRequete":"instance 6, utilisateur anonyme","journaux":["[Groupes#2] liste demandée","[Notes#3] bulletin demandé"],"controleursCrees":3}
Lecture du résultat
- CompteurGlobal (par défaut) : toujours l'instance 1, et le compteur continue d'une requête à l'autre (1, 2, 3). Toute l'application partage le même objet.
- ContexteRequete (requête) : une nouvelle instance à chaque requête (4, 5, 6), qui lit l'en-tête
X-Utilisateurde sa requête grâce au jetonREQUEST. Les données d'un utilisateur ne peuvent pas « déborder » sur la requête d'un autre. - Journal (transitoire) : deux instances différentes, une pour
GroupesService(#2) et une pourNotesService(#3), chacune avec son propre contexte. Comme ces deux services sont des singletons, leurs journaux sont créés une seule fois, au démarrage. - controleursCrees passe à 1, 2, 3 : le contrôleur est recréé à chaque requête!
La propagation de la portée requête
Le dernier point est essentiel : un provider de portée requête contamine tout ce qui l'injecte. Le contrôleur dépend de ContexteRequete: pour lui fournir le bon contexte à chaque requête, NestJS doit recréer le contrôleur à chaque requête. Si un service de portée par défaut injectait ContexteRequete, il deviendrait lui aussi « par requête », ainsi que tous ceux qui l'injectent, et ainsi de suite.
Coût: créer des objets à chaque requête prend du temps et de la mémoire. Sur une API très sollicitée, une chaîne de dépendances en portée requête peut dégrader nettement les performances. De plus, les providers de portée requête ne reçoivent pas les événements du cycle de vie (onModuleInit…). Gardez la portée par défaut, et isolez la portée requête au plus près de l'endroit où elle est nécessaire.
Quand utiliser quelle portée ?
| Besoin | Portée |
|---|---|
| Services métier, accès aux données, configuration, cache | par défaut (singleton) |
| Données propres à la requête : utilisateur connecté, identifiant de corrélation, langue, établissement (application multi-établissements) | requête, avec modération |
| Outil avec un état propre à chaque consommateur : journal avec contexte, constructeur de requêtes | transitoire |
Pour l'utilisateur connecté, NestJS propose aussi une alternative sans portée requête : le lire dans la requête avec un décorateur de paramètre personnalisé (leçon 5.4) et le passer en paramètre au service. C'est l'approche du chapitre 13.
Activités
Activité 1 Prédire les instances
Un CacheService (portée par défaut) est injecté dans trois services. Un Horodatage transitoire est injecté dans ces trois mêmes services. Un LangueCourante de portée requête est injecté dans un seul contrôleur. Au bout de 10 requêtes vers ce contrôleur, combien d'instances de chaque provider ont été créées ?
Voir le corrigé
CacheService: 1. Horodatage: 3, une par service consommateur, créées au démarrage. LangueCourante: 10, une par requête. Le contrôleur aussi a été créé 10 fois.
Activité 2 Un identifiant de corrélation
Créez un provider de portée requête Correlation qui lit l'en-tête X-Correlation-Id ou en génère un (randomUUID()), et une route qui le renvoie. Envoyez deux requêtes simultanées (avec deux onglets ou deux terminaux) : les identifiants se mélangent-ils ?
Voir le corrigé
@Injectable({ scope: Scope.REQUEST })
export class Correlation {
readonly id: string;
constructor(@Inject(REQUEST) req: Request) {
const recu = req.headers['x-correlation-id'];
this.id = typeof recu === 'string' && recu ? recu : randomUUID();
}
}
Chaque requête a son instance : les identifiants ne se mélangent jamais. C'est l'outil de base pour suivre une requête à travers plusieurs services dans les journaux. Comme dit dans la leçon, il rend « par requête » toutes les classes qui l'injectent.
Activité 3 Un journal transitoire
Reprenez Journal (transitoire). Remplacez sa portée par la portée par défaut et relancez la démonstration. Que deviennent les messages [Groupes#…] et [Notes#…]? Expliquez.
Voir le corrigé
Les deux services reçoivent alors la même instance. Le second appel à definirContexte (celui de NotesService, créé en second) écrase le premier : les deux messages affichent [Notes#2], même celui de GroupesService. La portée transitoire existe précisément pour les outils qui gardent un état propre à leur consommateur.
Activité 4 Repérer la contamination
Un StagiairesService (par défaut) injecte un UtilisateurCourant de portée requête pour enregistrer l'auteur des modifications. StagiairesService est injecté dans NotesService, GroupesService et trois contrôleurs. Quelles classes deviennent « par requête » ? Proposez une conception qui évite cette contamination.
Voir le corrigé
StagiairesService, puis NotesService, GroupesService, les trois contrôleurs et tout ce qui dépend d'eux : une grande partie de l'application est recréée à chaque requête. Meilleure conception : lire l'utilisateur dans le contrôleur (décorateur de paramètre @UtilisateurCourant(), leçon 5.4) et le passer en paramètre, par exemple stagiairesService.modifier(id, dto, utilisateur). Tous les services restent alors des singletons.
À retenir
- Portée par défaut : une instance partagée, créée au démarrage (à utiliser presque toujours).
- Scope.REQUEST : une instance par requête ; @Inject(REQUEST) donne la requête courante.
- Scope.TRANSIENT : une instance par classe consommatrice.
- La portée requête se propage à tout ce qui injecte le provider (contrôleurs compris) : coût en performances.
- Pour l'utilisateur courant, préférer un décorateur de paramètre et un passage en paramètre.