Cours NestJS · Module 1: Bases de NestJS
Gestion de la configuration - le registre des provinces
Dans cette leçon8
Une adresse de base de données écrite dans le code fonctionne à merveille - sur ton portable. Sur le serveur de préproduction, la base se trouve ailleurs, en production ailleurs encore, et son mot de passe ne devrait jamais atteindre le dépôt. Trois environnements, un seul code, trois valeurs différentes - et aucune ne peut être écrite en dur.
Rome avait un usage pour cela. Les ordres s'écrivaient une fois, mais les ressources d'une province étaient consignées dans le registre local : combien de blé, quelle route, qui gouverne. La même légion entrait en Gaule et en Égypte en lisant chaque fois le registre du lieu. Dans une application, ce registre, ce sont les variables d'environnement.
Quatre étapes
La configuration entre dans un projet dans un ordre fixé - il vaut la peine de le connaître en entier, car sauter une étape produit des erreurs trompeuses :
- Créer le fichier
.envavec les clés et les valeurs. - Enregistrer
ConfigModule.forRoot()dans le module racine. - Injecter
ConfigServicepar le constructeur. - Lire les valeurs par
configService.get('CLE').
Première étape : le fichier .env
Les variables d'environnement vivent dans un fichier .env à la racine du projet :
1DB_HOST=localhost
2DB_PORT=5432
3DB_PASSWORD=tribute123
4JWT_SECRET=super-secret-keyLe format est minimal : nom, signe égal, valeur. Sans guillemets, sans espaces autour du signe égal, sans point-virgule à la fin.
Le nom du fichier n'est pas une convention que tu choisis - c'est .env, et non config.json, settings.yaml ou environment.xml. Tu rencontreras ces formats dans d'autres écosystèmes, mais pas ici.
Le fichier lui-même n'entre jamais dans le dépôt - tu l'ajoutes au .gitignore. À sa place, tu commites un .env.example avec les mêmes clés et des valeurs vides ou d'exemple, pour qu'un collègue sache quoi remplir.
Deuxième étape : enregistrer le module
La gestion de la configuration vient du paquet @nestjs/config. Attention au nom - il n'existe pas de paquets @nestjs/env, @nestjs/settings ni @nestjs/environment. Tu l'installes avec la commande :
1npm i @nestjs/configNous l'enregistrons dans le module racine :
1@Module({
2 imports: [
3 ConfigModule.forRoot({
4 envFilePath: '.env',
5 isGlobal: true,
6 }),
7 ],
8})
9export class AppModule {}L'écriture a un squelette fixe : ConfigModule.forRoot({, puis les options - ici envFilePath: '.env', et isGlobal: true - et enfin }). L'ordre des options dans l'objet n'a pas d'importance ; ce qui compte, c'est qu'une virgule les sépare.
envFilePath désigne le fichier à charger ; avec le nom par défaut, on peut l'omettre.
isGlobal: true rend ConfigService disponible dans tous les modules sans imports supplémentaires. Sans cette option, tu devrais importer ConfigModule dans chaque module séparément - et la moitié de l'application s'en sert d'ordinaire. Note ce que cette option ne fait pas : elle ne chiffre rien, ne limite pas la visibilité au module racine et ne rafraîchit pas la configuration au redémarrage.
Troisième et quatrième étapes : la lecture
Tu injectes ConfigService comme n'importe quelle autre dépendance :
1@Injectable()
2export class DatabaseService {
3 constructor(private configService: ConfigService) {}
4
5 getConnection() {
6 const host = this.configService.get('DB_HOST');
7 const port = this.configService.get<number>('DB_PORT', 5432);
8
9 return { host, port };
10 }
11}Une lecture comporte trois parties dans un ordre fixe : this.configService, .get, ('DB_HOST').
Deux détails méritent d'être connus. L'écriture <number> est un paramètre de type - elle dit à TypeScript ce que tu attends, car les valeurs d'un fichier .env arrivent toujours sous forme de texte. Le second argument, ici 5432, est une valeur par défaut utilisée quand la clé manque. C'est pratique, mais prudence : un mot de passe par défaut ou une clé JWT par défaut est une faille toute prête - pour ces valeurs-là, mieux vaut que l'application ne démarre pas plutôt qu'elle démarre avec n'importe quoi.
La vérification au démarrage
Puisqu'une clé manquante donne undefined, la panne ne se révèle qu'au premier usage - parfois des heures plus tard. Mieux vaut vérifier l'ensemble tout de suite :
1ConfigModule.forRoot({
2 isGlobal: true,
3 validationSchema: Joi.object({
4 DB_HOST: Joi.string().required(),
5 DB_PORT: Joi.number().default(5432),
6 JWT_SECRET: Joi.string().required(),
7 }),
8});validationSchema décrit quelles clés tu attends et de quel type. Au démarrage, ConfigModule la compare au contenu du .env et interrompt le lancement s'il manque quelque chose - avec un message nommant la clé sans détour.
C'est échanger une panne à trois heures du matin contre une erreur au moment du déploiement. Joi est une bibliothèque de description de schémas distincte, qui ne fait pas partie de NestJS : tu l'installes avec npm i joi (NestJS 12 exige Joi 18 ou plus récent) et tu l'importes avec import * as Joi from 'joi';. @nestjs/config accepte un tel schéma dans l'option validationSchema.
Remarque que chaque clé a soit .required(), soit .default(...). Combiner les deux n'a pas de sens : quand une clé obligatoire manque, la validation arrête le démarrage avant que la valeur par défaut ait pu servir.
D'où vient une valeur
Une même clé peut se trouver à plusieurs endroits à la fois. Quand elle est à la fois dans les variables d'environnement du système (par exemple définies sur le serveur) et dans le fichier .env, c'est la variable système qui gagne - le fichier ne l'écrase pas. NestJS ne charge lui-même que .env ; les autres fichiers se déclarent dans envFilePath, par exemple envFilePath: ['.env.production', '.env'], et alors, pour une clé répétée, c'est le premier fichier de la liste qui gagne. La valeur par défaut de configService.get('CLE', valeurParDefaut) ne s'applique que si la clé n'existe nulle part.
Des groupes de configuration : registerAs
Quand les clés se multiplient, il est pratique de les regrouper par thème. La fonction registerAs de @nestjs/config crée un groupe nommé :
1import { registerAs } from '@nestjs/config';
2
3export const databaseConfig = registerAs('database', () => ({
4 host: process.env.DB_HOST,
5 port: parseInt(process.env.DB_PORT ?? '5432', 10),
6 name: process.env.DB_NAME,
7}));Le premier argument est le nom du groupe, le second une fabrique qui renvoie un objet avec les valeurs. Remarque le parseInt : les variables d'environnement sont du texte, tu convertis donc toi-même les nombres. Tu charges le groupe avec l'option load - ConfigModule.forRoot({ load: [databaseConfig] }) - et tu le lis avec un point : this.configService.get('database.host').
Récapitulatif
Le registre de la province se lit sur place, le code reste unique pour toutes :
- la configuration vit hors du code, car la même application tourne dans plusieurs environnements,
- quatre étapes dans l'ordre : le fichier
.env→ConfigModule.forRoot()dans le module racine → l'injection deConfigService→configService.get('CLE'), - les variables vivent dans un fichier
.env- pasconfig.json, passettings.yaml, pasenvironment.xml, - format : nom, signe égal, valeur ; le fichier reste hors du dépôt, tu commites
.env.example, - le paquet est
@nestjs/config;@nestjs/env,@nestjs/settingset@nestjs/environmentn'existent pas, - enregistrement :
ConfigModule.forRoot({, des options séparées par des virgules (par exempleenvFilePath: '.env',etisGlobal: true), et enfin}), isGlobal: truerendConfigServicedisponible dans tous les modules sans imports supplémentaires - il ne chiffre rien et ne restreint rien,- lecture :
this.configService+.get+('DB_HOST'); les valeurs du.envarrivent toujours sous forme de texte, - le second argument de
getest une valeur par défaut - n'en donne pas aux mots de passe ni aux secrets, validationSchemaavecJoivérifie l'ensemble des clés au démarrage et interrompt le lancement s'il en manque une ; une clé a.required()ou.default(...), pas les deux,- une variable système l'emporte sur le fichier
.env, et avec plusieurs fichiers dansenvFilePath, c'est le premier qui l'emporte, registerAs('database', () => ({ ... }))regroupe des clés ; tu charges le groupe avecloadet tu le lis avecget('database.host').
C'est la dernière leçon de ce module. Tu sais désormais bâtir un module, un contrôleur et un service, exposer une API REST, décrire les données par des DTO et sortir la configuration du code - tout ce qu'il faut à une application pour partir dans le monde. En attendant, retiens ceci : le code est le même pour toutes les provinces ; seul diffère le registre qu'elles lisent en arrivant.
Code de cette leçon : src/config-management.ts
1// Configuration Management - Gérer les ressources de l'Imperium
2import { Module, Injectable } from '@nestjs/common';
3
4console.log("Configuration Management - quartier-maître de l'imperium !");
5
6// ===========================================
7// 1. ConfigModule - configuration de base
8// ===========================================
9
10// app.module.ts
11// import { ConfigModule } from '@nestjs/config';
12//
13// @Module({
14// imports: [
15// ConfigModule.forRoot({
16// isGlobal: true, // Disponible dans tout l'imperium
17// envFilePath: '.env', // Chemin du fichier de config
18// }),
19// ],
20// })
21// export class AppModule {}
22
23// ===========================================
24// 2. Utiliser ConfigService
25// ===========================================
26
27// import { ConfigService } from '@nestjs/config';
28
29@Injectable()
30export class LegionConfigService {
31 // Dans une vraie appli : constructor(private configService: ConfigService)
32
33 getPort(): number {
34 // return this.configService.get<number>('PORT', 3000);
35 return 4000; // Port par défaut de l'imperium
36 }
37
38 getDatabaseUrl(): string {
39 // return this.configService.get<string>('DATABASE_URL');
40 return 'mongodb://localhost:27017/imperium';
41 }
42
43 getJwtSecret(): string {
44 // return this.configService.get<string>('JWT_SECRET');
45 return 'roma-aeterna-secret';
46 }
47
48 isProduction(): boolean {
49 // return this.configService.get('NODE_ENV') === 'production';
50 return false;
51 }
52}
53
54// ===========================================
55// 3. Fichiers de configuration personnalisés
56// ===========================================
57
58// config/legion.config.ts
59export const legionConfig = () => ({
60 legion: {
61 maxSoldiers: 6000,
62 minExperience: 1,
63 ranks: ['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'],
64 defaultProvince: 'Roma',
65 },
66 database: {
67 host: process.env.DB_HOST || 'localhost',
68 port: parseInt(process.env.DB_PORT || '5432', 10),
69 name: process.env.DB_NAME || 'imperium_db',
70 },
71 security: {
72 jwtExpiration: '7d',
73 bcryptRounds: 12,
74 maxLoginAttempts: 5,
75 },
76});
77
78// ===========================================
79// 4. Validation de la configuration avec Joi
80// ===========================================
81
82// import * as Joi from 'joi';
83//
84// ConfigModule.forRoot({
85// validationSchema: Joi.object({
86// NODE_ENV: Joi.string()
87// .valid('development', 'production', 'test')
88// .default('development'),
89// PORT: Joi.number().default(4000),
90// DATABASE_URL: Joi.string().required(),
91// JWT_SECRET: Joi.string().required(),
92// }),
93// validationOptions: {
94// abortEarly: true,
95// },
96// })
97
98console.log("\n=== RÉCAPITULATIF DE LA CONFIGURATION ===");
99console.log("ConfigModule.forRoot() - chargement de la configuration");
100console.log("ConfigService.get() - récupération des valeurs");
101console.log("load: [config] - fichiers de configuration personnalisés");
102console.log("validationSchema - validation des variables d'environnement");
103Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel paquet NestJS sert à gérer la configuration et les variables d'environnement ?
2. Dans quel fichier stocke-t-on les variables d'environnement dans un projet NestJS ?
3. Que signifie définir isGlobal: true dans ConfigModule.forRoot() ?
4. Quel paquet est le plus couramment utilisé pour valider les variables d'environnement dans NestJS ?
5. Quel fichier du dossier legion/ définit le module et relie le contrôleur au service ?
6. Quelle est la fonction principale du fichier main.ts dans NestJS ?
7. Quelle est la convention de nommage des fichiers dans NestJS ?
8. Quel est le bon ordre de traitement d'une requête HTTP dans NestJS ?
Tâches pratiques dans le jeu
- Éditeur de code
Enregistre ConfigModule.forRoot({ isGlobal: true }) dans les imports du module AppModule et écris un service DatabaseService qui reçoit ConfigService via le constructeur et renvoie this.configService.get('DB_HOST') dans une méthode getHost().
- Tri vertical
Remets dans l'ordre, du premier au dernier, les étapes de configuration des variables d'environnement :
- Clique dans l'ordre
Remets dans le bon ordre les éléments de l'enregistrement de ConfigModule :
- Tri horizontal
Remets dans l'ordre les éléments pour lire une variable d'environnement via ConfigService :
- Éditeur de code
Écris et exporte une constante databaseConfig = registerAs('database', ...), dont la fabrique renvoie un objet { host, port, name } avec les valeurs de process.env.DB_HOST, process.env.DB_PORT (converti en nombre) et process.env.DB_NAME.
- Tri vertical
L'application enregistre ConfigModule.forRoot({ envFilePath: ['.env.production', '.env'] }). Range les sources de la valeur d'une même clé de la priorité la plus haute à la plus basse :
- Tri horizontal
Remets dans l'ordre les éléments de la validation Joi du schéma pour la variable PORT :
- Clique dans l'ordre
Remets dans l'ordre les éléments de la définition d'une variable d'environnement dans le fichier .env :
- Tri vertical
Remets dans l'ordre, du début à la fin, les étapes du démarrage d'une application NestJS :
- Éditeur de code
Dans les imports du module AppModule, ajoute ConfigModule.forRoot() avec l'option validationSchema : un schéma Joi.object() dans lequel PORT est un nombre (Joi.number() avec la valeur par défaut 3000) et DATABASE_URL une chaîne obligatoire (Joi.string().required()).
- Tri horizontal
Remets dans le bon ordre les éléments de l'import de ConfigModule :
- Tri horizontal
Remets dans l'ordre les éléments pour créer une instance d'application NestJS :
- Clique dans l'ordre
Remets dans le bon ordre les éléments du cycle de vie d'une requête HTTP dans NestJS :
- Éditeur de code
Crée une fonction bootstrap qui crée une application avec NestFactory.create(), met en place un ValidationPipe global et écoute sur le port 3000