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

La portée des providers (singleton, request, transient)

Cette leçon explique la portée (scope) des providers NestJS, c'est-à-dire le nombre d'instances créées : portée par défaut (une instance partagée par toute l'application), portée requête (une instance...

0 chapitres 0 QCM Accès gratuit
La portée des providers (singleton, request, transient)

À propos de ce cours

Cette leçon explique la portée (scope) des providers NestJS, c'est-à-dire le nombre d'instances créées : portée par défaut (une instance partagée par toute l'application), portée requête (une instance par requête HTTP, avec accès à l'objet REQUEST) et portée transitoire (une instance par classe qui l'injecte). Une application de démonstration compte les instances réellement créées et montre la propagation de la portée requête au contrôleur. La leçon précise l'impact sur les performances, les limites (événements du cycle de vie) et les cas où chaque portée est justifiée.

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.

Portées des providers NestJS

Singleton : une instance ; requête : une par requête ; transitoire : une par consommateur.

Les trois portées
PortéeDéclarationInstances
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-Utilisateur de sa requête grâce au jeton REQUEST. 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 pour NotesService (#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 ?
BesoinPortée
Services métier, accès aux données, configuration, cachepar 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êtestransitoire

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.