À propos de ce cours
Objectifs de la leçon
- Connaître les cinq événements du cycle de vie et leur ordre.
- Implémenter une interface de cycle de vie dans un service ou un module.
- Écrire des traitements asynchrones au démarrage.
- Arrêter proprement une application avec enableShutdownHooks.
Deux événements au démarrage, trois à l'arrêt.
Les cinq événements
| Interface | Méthode | Appelée quand… |
|---|---|---|
OnModuleInit | onModuleInit() | les dépendances du module sont résolues |
OnApplicationBootstrap | onApplicationBootstrap() | tous les modules sont initialisés, juste avant l'écoute des connexions |
OnModuleDestroy | onModuleDestroy() | un arrêt commence (app.close() ou signal) |
BeforeApplicationShutdown | beforeApplicationShutdown(signal?) | après tous les onModuleDestroy, avant la fermeture des connexions |
OnApplicationShutdown | onApplicationShutdown(signal?) | les connexions sont fermées |
Ces méthodes peuvent être écrites dans les providers (services), les contrôleurs et les classes de module. Implémenter l'interface (implements OnModuleInit) n'est pas obligatoire mais fortement conseillé : TypeScript vérifie alors le nom et la signature de la méthode.
La démonstration
Un service de stagiaires charge ses données au démarrage (de façon asynchrone, pour simuler une lecture en base) ; un service de statistiques, dans un autre module, en dépend.
📄 src/stagiaires.module.ts
import {
BeforeApplicationShutdown, Injectable, Module, OnApplicationBootstrap, OnApplicationShutdown,
OnModuleDestroy, OnModuleInit,
} from '@nestjs/common';
const attendre = (ms: number) => new Promise((r) => setTimeout(r, ms));
@Injectable()
export class StagiairesService
implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown
{
private stagiaires: string[] = [];
async onModuleInit() {
await attendre(50); // NestJS attend la fin de la promesse
this.stagiaires = ['Ben Ali', 'Trabelsi', 'Gharbi'];
console.log(`StagiairesService.onModuleInit : ${this.stagiaires.length} stagiaires chargés`);
}
onApplicationBootstrap() {
console.log('StagiairesService.onApplicationBootstrap');
}
onModuleDestroy() {
console.log('StagiairesService.onModuleDestroy');
}
beforeApplicationShutdown(signal?: string) {
console.log(`StagiairesService.beforeApplicationShutdown (signal : ${signal ?? 'aucun'})`);
}
onApplicationShutdown(signal?: string) {
console.log(`StagiairesService.onApplicationShutdown (signal : ${signal ?? 'aucun'})`);
}
compter() {
return this.stagiaires.length;
}
}
@Module({ providers: [StagiairesService], exports: [StagiairesService] })
export class StagiairesModule implements OnModuleInit {
onModuleInit() {
console.log('StagiairesModule.onModuleInit');
}
}
📄 src/app.module.ts
import { Injectable, Module, OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
import { StagiairesModule, StagiairesService } from './stagiaires.module';
@Injectable()
export class StatistiquesService implements OnModuleInit, OnApplicationBootstrap {
constructor(private readonly stagiaires: StagiairesService) {}
onModuleInit() {
console.log(`StatistiquesService.onModuleInit : ${this.stagiaires.compter()} stagiaires visibles`);
}
onApplicationBootstrap() {
console.log('StatistiquesService.onApplicationBootstrap');
}
}
@Module({ imports: [StagiairesModule], providers: [StatistiquesService] })
export class AppModule {}
📄 src/main.ts
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { logger: false });
app.enableShutdownHooks();
console.log('--- app.listen()');
await app.listen(0);
console.log('--- application démarrée');
console.log('--- app.close()');
await app.close();
console.log('--- terminé');
}
bootstrap();
Terminal :
--- app.listen()
StagiairesService.onModuleInit : 3 stagiaires chargés
StagiairesModule.onModuleInit
StatistiquesService.onModuleInit : 3 stagiaires visibles
StagiairesService.onApplicationBootstrap
StatistiquesService.onApplicationBootstrap
--- application démarrée
--- app.close()
StagiairesService.onModuleDestroy
StagiairesService.beforeApplicationShutdown (signal : aucun)
StagiairesService.onApplicationShutdown (signal : aucun)
--- terminé
Lecture de la trace
- Les événements de démarrage sont déclenchés par
app.listen()(ouapp.init()), pas parNestFactory.create(). - Les modules importés sont initialisés avant le module qui les importe :
StatistiquesServicetrouve donc les 3 stagiaires déjà chargés. Dans un module, les providers passent avant la classe du module. onModuleInitestasync: NestJS attend la fin de la promesse (50 ms ici) avant de continuer. L'application ne répond donc pas aux requêtes tant que les données ne sont pas chargées.onApplicationBootstrapn'est appelé qu'une fois tous les modules initialisés : c'est le bon moment pour une action qui suppose que tout est prêt.- À l'arrêt avec
app.close(), le paramètresignalest vide. Il vaut'SIGTERM'ou'SIGINT'quand l'arrêt vient du système.
enableShutdownHooks et les signaux
En production, l'application n'est pas arrêtée par app.close() mais par un signal du système : SIGTERM envoyé par PM2, Docker ou un redéploiement, SIGINT quand vous appuyez sur Ctrl+C. Par défaut, NestJS n'écoute pas ces signaux (cela consomme des ressources) : les trois événements d'arrêt ne seraient pas appelés. Il faut l'activer explicitement :
// main.ts
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // les événements d'arrêt sont déclenchés par SIGTERM, SIGINT…
await app.listen(3000);
Lancez npm run start:dev puis Ctrl+C : les méthodes d'arrêt s'exécutent et affichent signal : SIGINT.
Usages courants
| Événement | Usages |
|---|---|
onModuleInit | charger des données de référence, préparer un cache, vérifier une configuration obligatoire |
onApplicationBootstrap | afficher un récapitulatif, démarrer une tâche qui utilise plusieurs modules |
onModuleDestroy | arrêter des minuteries, cesser d'accepter de nouveaux travaux |
beforeApplicationShutdown | terminer les traitements en cours (file d'e-mails à vider) |
onApplicationShutdown | fermer les connexions (base de données, WebSocket), écrire un dernier message de journal |
À savoir: les modules officiels (TypeORM, Mongoose…) gèrent déjà l'ouverture et la fermeture de leurs connexions avec ces événements. Vous les utiliserez surtout pour vos propres ressources. Attention : les providers à portée « requête » (chapitre 07) ne reçoivent pas ces événements.
Activités
Activité 1 Reproduire et observer Ctrl+C
Ajoutez à un service de api-formation les cinq méthodes du cycle de vie, chacune affichant son nom et le signal. Lancez avec npm run start:dev, puis arrêtez avec Ctrl+C : sans enableShutdownHooks(), puis avec. Comparez.
Voir le corrigé
Sans enableShutdownHooks(), seules les méthodes de démarrage s'affichent : le processus est tué sans prévenir l'application. Avec, on voit onModuleDestroy, puis beforeApplicationShutdown (signal : SIGINT) et onApplicationShutdown (signal : SIGINT).
Activité 2 Charger des données de départ
Dans GroupesService, au lieu d'initialiser le tableau dans la déclaration, chargez les groupes dans onModuleInit à partir d'un fichier src/donnees/groupes.json lu avec fs/promises. Que se passe-t-il si le fichier est absent ? Comment le signaler clairement ?
Voir le corrigé
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
@Injectable()
export class GroupesService implements OnModuleInit {
private groupes: Groupe[] = [];
async onModuleInit() {
const chemin = join(process.cwd(), 'src', 'donnees', 'groupes.json');
try {
this.groupes = JSON.parse(await readFile(chemin, 'utf8'));
} catch (e) {
throw new Error(`Impossible de charger les groupes depuis ${chemin} : ${(e as Error).message}`);
}
}
}
Si le fichier est absent, l'exception levée dans onModuleInit empêche l'application de démarrer, avec le message explicite. C'est voulu : mieux vaut un échec immédiat et clair qu'une API qui démarre sans données. On utilise process.cwd() (dossier du projet) car le code compilé s'exécute depuis dist/.
Activité 3 Vérifier une configuration
Dans ParametresService (leçon 5.3), ajoutez onModuleInit qui lève une erreur si etablissement est vide ou si anneeScolaire ne respecte pas le format AAAA-AAAA (deux années consécutives). Testez avec '2026-2027' puis '2026'.
Voir le corrigé
onModuleInit() {
const { etablissement, anneeScolaire } = this.options;
if (!etablissement?.trim()) throw new Error('ParametresModule : etablissement est obligatoire');
const m = /^(\d{4})-(\d{4})$/.exec(anneeScolaire);
if (!m || Number(m[2]) !== Number(m[1]) + 1) {
throw new Error(`ParametresModule : anneeScolaire invalide (« ${anneeScolaire} »), format attendu 2026-2027`);
}
}
Avec '2026', l'application s'arrête au démarrage avec le message. Au chapitre 10, ConfigModule proposera une validation complète du fichier .env sur le même principe.
Activité 4 Le bon événement
Quel événement choisir ? (a) Afficher « API prête : 3 modules, 12 routes » une fois tout initialisé ; (b) fermer un fichier de journal ouvert en écriture ; (c) remplir un cache des formations à partir du service des formations ; (d) refuser de nouvelles inscriptions pendant l'arrêt, tout en terminant celles en cours ; (e) vérifier qu'une clé d'API obligatoire est présente.
Voir le corrigé
(a) onApplicationBootstrap; (b) onApplicationShutdown; (c) onModuleInit du service de cache (les formations sont prêtes si son module importe celui des formations) ; (d) onModuleDestroy pour bloquer les nouvelles, beforeApplicationShutdown pour attendre la fin des inscriptions en cours ; (e) onModuleInit du service concerné.
À retenir
- Démarrage : onModuleInit (module prêt) puis onApplicationBootstrap (tout est prêt).
- Arrêt : onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown.
- Les modules importés sont initialisés avant ceux qui les importent ; les méthodes async sont attendues.
- enableShutdownHooks() est nécessaire pour réagir à SIGTERM / SIGINT.
- Une erreur levée dans onModuleInit empêche le démarrage : idéal pour vérifier une configuration.