Cours NestJS · Module 1: Bases de NestJS

Gestion de la configuration - le registre des provinces

7 min de lecture
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 :

  1. Créer le fichier .env avec les clés et les valeurs.
  2. Enregistrer ConfigModule.forRoot() dans le module racine.
  3. Injecter ConfigService par le constructeur.
  4. 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-key

Le 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/config

Nous 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 de ConfigService → configService.get('CLE'),
  • les variables vivent dans un fichier .env - pas config.json, pas settings.yaml, pas environment.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/settings et @nestjs/environment n'existent pas,
  • enregistrement : ConfigModule.forRoot({, des options séparées par des virgules (par exemple envFilePath: '.env', et isGlobal: true), et enfin }),
  • isGlobal: true rend ConfigService disponible 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 .env arrivent toujours sous forme de texte,
  • le second argument de get est une valeur par défaut - n'en donne pas aux mots de passe ni aux secrets,
  • validationSchema avec Joi vé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 dans envFilePath, c'est le premier qui l'emporte,
  • registerAs('database', () => ({ ... })) regroupe des clés ; tu charges le groupe avec load et tu le lis avec get('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");
103

Tu 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. 1. Quel paquet NestJS sert à gérer la configuration et les variables d'environnement ?

  2. 2. Dans quel fichier stocke-t-on les variables d'environnement dans un projet NestJS ?

  3. 3. Que signifie définir isGlobal: true dans ConfigModule.forRoot() ?

  4. 4. Quel paquet est le plus couramment utilisé pour valider les variables d'environnement dans NestJS ?

  5. 5. Quel fichier du dossier legion/ définit le module et relie le contrôleur au service ?

  6. 6. Quelle est la fonction principale du fichier main.ts dans NestJS ?

  7. 7. Quelle est la convention de nommage des fichiers dans NestJS ?

  8. 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