J3 · Injection de dépendances et modules
Le conteneur de Nest ressemble à celui de Spring : des singletons créés au démarrage et injectés par constructeur. Deux différences changent pourtant la façon d'écrire le code : les types TypeScript n'existent plus à l'exécution, et rien n'est découvert automatiquement. Ce chapitre montre comment déclarer des providers, injecter une interface par token, choisir un scope et découper l'application en modules. TaskFlow y gagne une couche service et une abstraction de stockage, prête à recevoir TypeORM en J4.
Objectifs
- Comprendre comment Nest résout une dépendance de constructeur, et quand il lui faut un token.
- Déclarer des providers avec
useClass,useValue,useFactoryetuseExisting. - Utiliser des tokens personnalisés avec
@Inject, et@Optionalpour une dépendance facultative. - Choisir un scope (singleton, request, transient) en connaissant son coût.
- Organiser l'application en modules :
imports,exports,@Global, modules dynamiques (forRoot,forRootAsync). - Isoler la logique dans
ProjectsService, derrière une abstractionProjectsStore, et créerCommonModule.
Spring → NestJS
| Spring | NestJS | À retenir |
|---|---|---|
@Service, @Component | @Injectable() + déclaration dans providers | Le décorateur seul ne suffit pas : pas de component scan |
| Injection par constructeur | Injection par constructeur, type lu dans design:paramtypes | Le type du paramètre doit exister à l'exécution (une classe) |
@Qualifier("store"), injection d'interface | @Inject(PROJECTS_STORE) | Les interfaces disparaissent à la compilation : il faut un token |
@Configuration + @Bean | useFactory (+ inject) | La fabrique reçoit ses dépendances par inject |
@Profile, @ConditionalOnProperty | Fabrique qui choisit selon la configuration, ConditionalModule | Décidé une fois, au démarrage |
| Alias de bean | useExisting | La même instance sous deux tokens |
@Autowired(required = false), Optional<T> | @Optional() | undefined si aucun provider ne correspond |
| Singleton (défaut) | Scope.DEFAULT (défaut) | Partagé par toutes les requêtes |
@RequestScope (via un proxy) | Scope.REQUEST | Pas de proxy : toute la chaîne d'injection est recréée à chaque requête |
@Scope("prototype") | Scope.TRANSIENT | Une instance par classe consommatrice |
Contexte plat, @Import | imports / exports entre modules | Un provider est privé à son module tant qu'il n'est pas exporté |
Auto-configuration + application.yml | Modules dynamiques forRoot() / forRootAsync() | Configurer un module, c'est appeler une méthode statique |
@Lazy sur une dépendance circulaire | forwardRef() | À éviter : revoir le découpage |
Comprendre
Comment Nest résout une dépendance
Spring lit le type des paramètres de constructeur dans le bytecode, toujours disponible par réflexion. TypeScript, lui, efface les types à la compilation. Pour que Nest les retrouve, le compilateur doit les écrire à part : avec emitDecoratorMetadata (activé dans le tsconfig.json de TaskFlow), toute classe décorée reçoit une métadonnée design:paramtypes, la liste des valeurs qui représentent les types de ses paramètres. Pour constructor(private readonly projects: ProjectsService), cette liste vaut [ProjectsService], c'est-à-dire la classe elle-même.
Au démarrage, Nest lit ces listes, utilise chaque classe comme token (la clé de recherche dans le conteneur), cherche le provider correspondant dans le module courant puis dans les modules importés, instancie les dépendances dans l'ordre et met les instances en cache. Un @Inject(token) sur un paramètre remplace la métadonnée pour ce paramètre.
Ce qui ne survit pas à la compilation devient Object dans design:paramtypes, et Nest ne peut plus rien résoudre :
| Type du paramètre | Métadonnée émise | Résolution |
|---|---|---|
| Une classe importée normalement | la classe | automatique |
| Une interface ou un alias de type | Object | @Inject(TOKEN) obligatoire |
Une classe importée avec import type | Object | importer la classe normalement, ou @Inject |
Une union de types (une classe ou null, par exemple) | Object | @Inject |
C'est aussi pourquoi Nest utilise les décorateurs « expérimentaux » de TypeScript (experimentalDecorators) : ce sont les seuls compatibles avec emitDecoratorMetadata. La même cause produit d'autres pièges, comme la conversion de variables d'environnement en J7 (types et métadonnées).
Providers : useClass, useValue, useFactory, useExisting
Un provider associe un token à une recette. providers: [ProjectsService] est un raccourci pour { provide: ProjectsService, useClass: ProjectsService }. Les quatre recettes :
| Forme | Ce que Nest fait | Dans TaskFlow | Équivalent Spring |
|---|---|---|---|
useClass | Instancie la classe et injecte ses dépendances | { provide: APP_GUARD, useClass: JwtAuthGuard } ; dans les tests, un store en mémoire à la place du vrai | Une classe @Component |
useValue | Fournit une valeur déjà construite, sans injection | { provide: CLOCK, useValue: () => new Date() }, le ValidationPipe global | Un @Bean qui renvoie un objet existant |
useFactory | Appelle une fonction ; ses arguments sont listés dans inject ; elle peut être async | Le choix du PROJECTS_STORE selon la configuration ; toutes les options forRootAsync | Une méthode @Bean d'une classe @Configuration |
useExisting | Donne un second nom à un provider existant : même instance | Pas utilisé (exemple ci-dessous) | Un alias de bean |
Une fabrique async est attendue avant que quoi que ce soit qui en dépend soit créé : c'est ainsi que les modules de base de données ouvrent leur connexion avant le démarrage. Une dépendance de fabrique peut être facultative : inject: [{ token: SomeService, optional: true }].
useExisting crée un alias, alors que useClass crée toujours une nouvelle instance :
// Exemple hors TaskFlow
providers: [
InMemoryProjectsStore,
// PROJECTS_STORE et InMemoryProjectsStore désignent la même instance.
{ provide: PROJECTS_STORE, useExisting: InMemoryProjectsStore },
// Avec useClass, PROJECTS_STORE serait une seconde instance, avec sa propre Map.
],Les tests utilisent le même mécanisme pour remplacer une dépendance, comme un @MockBean :
describe('ProjectsService', () => {
let service: ProjectsService;
beforeEach(async () => {
// Like a @SpringBootTest slice: only the providers this test needs, with
// the real in-memory store standing in for the database.
const moduleRef = await Test.createTestingModule({
providers: [ProjectsService, { provide: PROJECTS_STORE, useClass: InMemoryProjectsStore }],
}).compile();
service = moduleRef.get(ProjectsService);
});
it('makes the creator a member who can read the project', async () => {
const project = await service.create({ name: 'Website' }, alice);
await expect(service.findOne(project.id, alice)).resolves.toMatchObject({ name: 'Website' });
});
it('answers 404, not 403, to a user outside the project', async () => {
const project = await service.create({ name: 'Website' }, alice);
await expect(service.findOne(project.id, bob)).rejects.toBeInstanceOf(NotFoundException);
});Les mocks et Test.createTestingModule sont détaillés en J10.
Tokens personnalisés, @Inject et @Optional
Un token est une classe (le cas par défaut), une chaîne ou un Symbol. Pour tout ce qui n'est pas une classe (une interface, une fonction, un objet de configuration), déclarez une constante et injectez-la avec @Inject :
ProjectsServicereçoit son stockage par@Inject(PROJECTS_STORE) private readonly store: ProjectsStore(cas TaskFlow). L'interfaceProjectsStorene sert qu'au compilateur ; c'est le token qui sert au conteneur. C'est le rôle de@Qualifieren Spring, avec une différence : en Spring, le type suffit tant qu'il n'y a qu'une implémentation ; ici, le token est obligatoire dès que le type est une interface.TasksServicereçoit l'horloge par@Inject(CLOCK) private readonly clock: Clock(CommonModule).- Les décorateurs des bibliothèques sont des
@Injectdéguisés :@InjectRepository(Task)injecte le tokengetRepositoryToken(Task), qui vaut la chaîne"TaskRepository"(J4).
Préférez un Symbol à une chaîne : Symbol('PROJECTS_STORE') est unique, alors que deux bibliothèques peuvent choisir la même chaîne. Exportez la constante à côté de l'interface, comme projects.store.ts.
@Optional() rend une dépendance facultative : si aucun provider ne correspond, le paramètre vaut undefined au lieu de faire échouer le démarrage. TaskFlow l'utilise dans NotificationsListener : @Optional() @InjectQueue(NOTIFICATIONS_QUEUE) private readonly queue?: Queue<NotificationJob>. La file BullMQ n'existe que si REDIS_URL est défini ; sans elle, le listener simule l'envoi (J9). C'est l'équivalent de @Autowired(required = false).
Scopes : singleton, request, transient
| Scope | Instances | Usage typique | Coût |
|---|---|---|---|
Scope.DEFAULT | Une seule, créée au démarrage, partagée par toute l'application | Presque tout : services, repositories, guards | Aucun |
Scope.REQUEST | Une par requête HTTP, abandonnée après la réponse | Un contexte propre à la requête (tenant, utilisateur) | L'instance et toute sa chaîne de consommateurs sont recréées à chaque requête |
Scope.TRANSIENT | Une par classe qui l'injecte | Un outil dont l'état dépend de son utilisateur (logger nommé d'après la classe) | Une instance par consommateur |
// Exemple hors TaskFlow : un provider request-scoped
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import type { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
constructor(@Inject(REQUEST) private readonly request: Request) {}
get correlationId(): string | undefined {
return this.request.headers['x-correlation-id'] as string | undefined;
}
}Là où l'analogie avec Spring casse : @RequestScope injecte un proxy dans les singletons, qui délègue à l'instance de la requête en cours. Nest n'a pas de proxy. Le scope remonte la chaîne : le service qui injecte RequestContext devient lui aussi request-scoped, puis le contrôleur qui injecte ce service, et tout est recréé à chaque requête. Le coût n'est pas seulement l'objet lui-même. Le scope transient, lui, ne remonte pas : un singleton qui injecte un provider transient reste un singleton.
TaskFlow n'a que des singletons. Les données propres à une requête circulent en paramètres : @CurrentUser() user dans le contrôleur, puis service.findOne(id, user). C'est plus explicite, testable sans contexte HTTP, et sans coût. Un singleton ne doit jamais stocker de données de requête dans ses attributs : deux requêtes concurrentes les partageraient (état dans un singleton).
Modules : imports, exports, @Global
Le contexte Spring est plat : tout bean est visible de tous les autres. Un module Nest est une unité d'encapsulation, plus proche d'un module Java (JPMS) : ses providers sont privés, sauf ceux qu'il liste dans exports, et un autre module n'y accède qu'en l'important.
// No component scan: a provider is only visible to modules that import this
// one, and only because it is listed in `exports`.
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}| Clé | Rôle |
|---|---|
providers | Ce que le module instancie (services, stores, fabriques) |
controllers | Les contrôleurs dont il enregistre les routes |
imports | Les modules dont il utilise les exports |
exports | Ses providers (ou modules réexportés) visibles par les modules qui l'importent |
Les dépendances entre modules de TaskFlow vont dans un seul sens :
AuthModuleimporteUsersModule, qui exporteUsersService.TasksModuleimporteProjectsModule, qui exporteProjectsService(pourassertAccess), mais pas le store : les autres modules passent par les règles du service (module).NotificationsQueueModuleréexporteBullModule, pour que le listener puisse injecter la file.
Côté TasksModule, importer ProjectsModule suffit pour que TasksService reçoive ProjectsService dans son constructeur :
// ProjectsModule exports ProjectsService: importing the module is what makes
// it injectable into TasksService.
@Module({
imports: [TypeOrmModule.forFeature([Task]), ProjectsModule],
controllers: [TasksController],
providers: [TasksService, TasksScheduler],
})
export class TasksModule {}Un module @Global() rend ses exports disponibles partout sans import, une fois qu'il est importé à la racine. TaskFlow en a deux : CommonModule (horloge) et ConfigModule.forRoot({ isGlobal: true }) (J7). Réservez-le aux briques vraiment transverses : un module global masque ses dépendances, et la lecture des imports ne suffit plus à comprendre le graphe.
Modules dynamiques : forRoot, forRootAsync
Un module statique a des métadonnées figées. Un module dynamique est une méthode statique qui construit ces métadonnées à partir d'arguments et renvoie un objet DynamicModule ({ module, providers, exports, imports, global }). C'est le rôle de l'auto-configuration Spring Boot alimentée par application.yml, mais par un appel explicite.
Les noms suivent une convention :
| Méthode | Sens | Exemples TaskFlow |
|---|---|---|
forRoot(options) | Configuration unique, à la racine | ConfigModule.forRoot(), EventEmitterModule.forRoot(), ScheduleModule.forRoot() |
forRootAsync({ inject, useFactory }) | Idem, avec des options calculées à partir d'autres providers | TypeOrmModule.forRootAsync(), LoggerModule.forRootAsync(), BullModule.forRootAsync() |
forFeature(...) | Complément par module, appuyé sur la configuration racine | TypeOrmModule.forFeature([Project, ProjectMember]) |
register(options) / registerAsync(...) | Configuration propre à chaque import | JwtModule.registerAsync(), BullModule.registerQueue() |
forRootAsync reprend la forme d'un provider useFactory : la fabrique reçoit ses dépendances par inject, et le module attend son résultat. Le module de base de données de TaskFlow en est l'exemple type :
@Module({
imports: [
TypeOrmModule.forRootAsync({
inject: [databaseConfig.KEY],
useFactory: (db: DatabaseConfig) => ({
...postgresOptions(db),
// Picks up every entity registered with TypeOrmModule.forFeature().
autoLoadEntities: true,
}),
}),
],
})
export class DatabaseModule {}databaseConfig.KEY est le token de la configuration typée de J7. Il est visible ici parce que ConfigModule est global ; sinon, il faudrait ajouter imports: [ConfigModule.forFeature(databaseConfig)] dans les options.
Écrire son propre module dynamique tient en quelques lignes :
// Exemple hors TaskFlow : un module configurable écrit à la main
import { type DynamicModule, Module } from '@nestjs/common';
export const MAILER_OPTIONS = Symbol('MAILER_OPTIONS');
export interface MailerOptions {
from: string;
}
@Module({})
export class MailerModule {
static forRoot(options: MailerOptions): DynamicModule {
return {
module: MailerModule,
// MailerService reçoit les options par @Inject(MAILER_OPTIONS).
providers: [{ provide: MAILER_OPTIONS, useValue: options }, MailerService],
exports: [MailerService],
};
}
}Pour générer aussi forRootAsync sans tout réécrire, @nestjs/common fournit ConfigurableModuleBuilder. Pour charger un module selon une variable d'environnement, TaskFlow utilise ConditionalModule.registerWhen() de @nestjs/config, avec un piège sur la forme chaîne (ConditionalModule).
Dépendances circulaires et forwardRef
Si ProjectsModule importe TasksModule et réciproquement, l'un des deux est encore undefined quand l'autre est évalué. forwardRef() retarde la résolution :
// Exemple hors TaskFlow : à éviter
@Module({ imports: [forwardRef(() => TasksModule)] })
export class ProjectsModule {}
// Et côté provider :
constructor(@Inject(forwardRef(() => TasksService)) private readonly tasks: TasksService) {}Le plan le dit : mieux vaut revoir le découpage. TaskFlow n'a pas de cycle : TasksModule dépend de ProjectsModule, et quand l'archivage d'un projet doit modifier les tâches, TypeOrmProjectsStore utilise directement l'entité Task dans sa transaction, sans importer TasksModule (J5). Voir aussi dépendance circulaire.
Cas TaskFlow
ProjectsService : le contrôleur ne fait que router
Le contrôleur de J2 traduit HTTP en appels de méthodes. Les règles métier vivent dans le service :
// Business rules live here; the controller only maps HTTP to these calls.
@Injectable()
export class ProjectsService {
constructor(@Inject(PROJECTS_STORE) private readonly store: ProjectsStore) {}
create(dto: CreateProjectDto, user: AuthUser): Promise<Project> {
return this.store.create(dto, user.id);
}
findAll(query: ProjectQueryDto, user: AuthUser): Promise<Page<Project>> {
// Admins see everything, members only their own projects.
return this.store.findPage(query, user.role === Role.Admin ? undefined : user.id);
}
// A non-member gets 404, not 403: the answer must not reveal that the
// project exists. A soft-deleted project is not found either, even though
// its membership rows are kept.
async findOne(id: string, user: AuthUser): Promise<Project> {
const project = await this.store.findById(id);
const allowed = user.role === Role.Admin || (await this.store.isMember(id, user.id));
if (!project || !allowed) throw new NotFoundException(`Project ${id} not found`);
return project;
}
async update(id: string, dto: UpdateProjectDto, user: AuthUser): Promise<Project> {
await this.findOne(id, user);
return (await this.store.update(id, dto))!;
}
async remove(id: string, user: AuthUser): Promise<void> {
await this.findOne(id, user);
await this.store.softDelete(id);
}
async archive(id: string, user: AuthUser): Promise<Project> {
await this.findOne(id, user);
return (await this.store.archive(id))!;
}
async addMember(id: string, userId: string, user: AuthUser): Promise<void> {
await this.findOne(id, user);
await this.store.addMember(id, userId);
}
/** Used by TasksService: same rule as findOne(). */
async assertAccess(projectId: string, user: AuthUser): Promise<void> {
await this.findOne(projectId, user);
}
}- Le service ne connaît que l'interface
ProjectsStore, jamais une implémentation. import { PROJECTS_STORE, type ProjectsStore }: l'import du type seul suffit, car le token est explicite.- Les règles d'accès (un administrateur voit tout, un membre ses projets, un non-membre reçoit 404 et non 403, un projet supprimé logiquement répond 404 à tous) sont écrites une fois :
findAll()filtre la liste,findOne()contrôle l'accès à un projet.update(),remove(),archive()etaddMember()l'appellent d'abord, etassertAccess(), utilisé parTasksService, lui délègue : une règle d'accès ajoutée plus tard vaudra aussi pour les tâches. Le paramètreuserarrive en J8 (membres) ; en J3, les méthodes ne prennent que le DTO et l'id. - Le service lève des exceptions HTTP de Nest (
NotFoundException). C'est pratique ; certains préfèrent des erreurs métier traduites par un filtre (J6).
ProjectsStore : interface, token et implémentation
L'abstraction de stockage, avec son token :
// Interfaces disappear at runtime, so the container cannot resolve one by
// type. A token stands in for it: @Inject(PROJECTS_STORE) store: ProjectsStore.
export const PROJECTS_STORE = Symbol('PROJECTS_STORE');
export interface ProjectsStore {
create(data: CreateProjectDto, ownerId: string): Promise<Project>;
/** memberId undefined: every project (admin view). */
findPage(query: ProjectQueryDto, memberId?: string): Promise<Page<Project>>;
findById(id: string): Promise<Project | null>;
update(id: string, changes: UpdateProjectDto): Promise<Project | null>;
softDelete(id: string): Promise<boolean>;
/** Archives the project and, with TypeORM, all its tasks in one transaction. */
archive(id: string): Promise<Project | null>;
isMember(projectId: string, userId: string): Promise<boolean>;
addMember(projectId: string, userId: string): Promise<void>;
}L'implémentation en mémoire de J2 implémente cette interface (InMemoryProjectsStore implements ProjectsStore, code en J2). En J3, le module la branche simplement :
// Version J3 (hors TaskFlow final) : une seule implémentation
providers: [ProjectsService, { provide: PROJECTS_STORE, useClass: InMemoryProjectsStore }],En J4 arrive TypeOrmProjectsStore, et le choix devient une affaire de configuration. Voici le module final :
@Module({
imports: [TypeOrmModule.forFeature([Project, ProjectMember])],
controllers: [ProjectsController],
providers: [
ProjectsService,
InMemoryProjectsStore,
TypeOrmProjectsStore,
// A @Bean method chosen by configuration, like @ConditionalOnProperty.
{
provide: PROJECTS_STORE,
inject: [ConfigService, InMemoryProjectsStore, TypeOrmProjectsStore],
useFactory: (config: ConfigService, memory: InMemoryProjectsStore, typeorm: TypeOrmProjectsStore) =>
config.get('PROJECTS_STORE') === 'memory' ? memory : typeorm,
},
],
// TasksModule needs assertAccess(): export the service, not the store.
exports: [ProjectsService],
})
export class ProjectsModule {}- La fabrique joue le rôle d'une méthode
@Beanavec@ConditionalOnProperty: elle reçoitConfigServiceet les deux implémentations parinject, et renvoie celle que désignePROJECTS_STORE(memoryoutypeorm, valeur validée au démarrage en J7). - Elle renvoie une instance déjà créée par le conteneur, pas un
new: l'instance injectée sousPROJECTS_STOREest la même que celle déclarée dansproviders. - Les deux implémentations sont instanciées dans tous les cas ; seul le choix change. La version TypeORM a donc besoin de la base même en mode
memory. exports: [ProjectsService]: le store reste privé au module.
Le contrôleur et le service ne changent pas d'une implémentation à l'autre : c'est tout l'intérêt du token, et ce que J4 vérifie (TypeORM derrière le store).
CommonModule
Le plan demande un CommonModule pour les utilitaires partagés. Dans TaskFlow, il fournit une horloge :
// A custom token for something that is not a class: inject it with
// @Inject(CLOCK). Tests replace it with a fixed date (useValue).
export const CLOCK = Symbol('CLOCK');
export type Clock = () => Date;
@Global()
@Module({
providers: [{ provide: CLOCK, useValue: (() => new Date()) satisfies Clock }],
exports: [CLOCK],
})
export class CommonModule {}CLOCKn'est pas une classe : unSymbolsert de token, etuseValuefournit la fonction.satisfies Clockvérifie le type sans l'élargir.@Global():TasksServicepeut écrire@Inject(CLOCK)sans queTasksModuleimporteCommonModule.AppModulel'importe une fois.- Les tests remplacent l'horloge par une date fixe,
{ provide: CLOCK, useValue: () => now }, ce qui rend le marquage des tâches en retard déterministe (J10). C'est la pratique Spring qui consiste à injecter unjava.time.Clockplutôt qu'appelerInstant.now().
Le reste de src/common/ (filtre, intercepteur, middleware, décorateurs, DTO de pagination, fuseau horaire de time-zone.ts) n'est pas fourni par CommonModule : les éléments du pipeline sont enregistrés globalement dans AppModule par APP_FILTER, APP_INTERCEPTOR… (J6), et les décorateurs, DTO et fonctions utilitaires s'importent comme du code ordinaire.
Situations
- Vous devez injecter une interface → un token
Symbolexporté avec l'interface, et@Inject(TOKEN)(tokens, ProjectsStore). - L'implémentation dépend de la configuration → un provider
useFactoryqui reçoitConfigServiceet les implémentations parinject(module Projects). - Un test doit remplacer une dépendance →
{ provide: TOKEN, useClass: Fake }ouuseValue: mockdansTest.createTestingModule(providers). - Vous devez injecter une valeur qui n'est pas une classe (horloge, options) →
useValuederrière unSymbol(CommonModule). - Un service doit être utilisé par un autre module → l'ajouter aux
exportsde son module, et importer ce module (modules). - Un module tiers a besoin de valeurs de configuration →
forRootAsync({ inject, useFactory })(modules dynamiques). - Une dépendance n'existe que dans certains environnements →
@Optional()et un test surundefined(tokens). - Vous voulez l'utilisateur courant dans un service → passez-le en paramètre ;
Scope.REQUESTseulement si c'est impossible (scopes).
Pièges
« Nest can't resolve dependencies of the ProjectsService (?) »
Le ? marque le paramètre introuvable. Trois causes fréquentes :
- Le paramètre est typé par une interface, un alias ou une union, sans
@Inject: la métadonnée vautObject(résolution). Ajoutez un token et@Inject(TOKEN). - La classe est importée avec
import type: même effet. Importez-la normalement. Le message de Nest le suggère d'ailleurs explicitement. - Le provider existe, mais dans un autre module qui ne l'exporte pas, ou que le module courant n'importe pas. Le message propose alors de vérifier les
imports.
Données de requête dans un singleton
this.currentUser = user dans un service : la requête suivante, concurrente, l'écrase. Passez la donnée en paramètre (scopes).
- La même classe dans les
providersde deux modules crée deux instances distinctes, chacune avec son état. Déclarez-la dans un seul module, exportez-la, importez ce module. useClassen plus de la classe elle-même (providers: [Store, { provide: TOKEN, useClass: Store }]) crée aussi deux instances. UtilisezuseExisting, ou injectez l'instance dans une fabrique comme TaskFlow.Scope.REQUESTposé « pour essayer » rend request-scoped tous ses consommateurs, contrôleurs compris. Mesurez avant, et préférez les paramètres.- Des modules
@Global()partout : le graphe devient illisible et les tests doivent fournir des dépendances que rien n'annonce. - Des tokens en chaîne de caractères peuvent entrer en collision ; préférez un
Symbolexporté. forwardRef()pour faire taire un cycle : le cycle reste, avec un ordre d'initialisation plus fragile. Extrayez la partie commune dans un troisième module.
Exercices
- [ ] Isoler la logique dans
ProjectsService: le contrôleur ne fait plus que du routing. - [ ] Introduire l'interface
ProjectsStoreet le tokenPROJECTS_STORE(unSymbol), avecInMemoryProjectsStorecomme implémentation, injectée par@Inject(PROJECTS_STORE). - [ ] Créer
CommonModule(@Global()) avec un providerCLOCKenuseValue, et l'injecter dans un service. - [ ] Retirer
@Inject(PROJECTS_STORE)du constructeur deProjectsService, lire l'erreur au démarrage, puis le remettre. - [ ] Écrire les quatre formes de provider :
useClass(dans un test),useValue(CLOCK),useFactory(choix du store selon une variable d'environnement),useExisting(un alias). Vérifier avecapp.get(PROJECTS_STORE) === app.get(InMemoryProjectsStore)la différence entreuseExistingetuseClass. - [ ] Rendre temporairement un provider
Scope.REQUEST, logger dans son constructeur et dans celui du contrôleur qui l'utilise : observer une instance par requête, des deux côtés. Revenir au singleton. - [ ] Utiliser un service d'un module qui ne l'exporte pas, lire l'erreur, corriger avec
exportsetimports. - [ ] Noter dans
LEARNINGS.md:@Configuration+@Bean↔useFactory, profil conditionnel ↔ provider choisi selon la configuration.