Configuration
Les applications fonctionnent souvent dans différents environnements. En fonction de l’environnement, différentes configurations doivent être utilisées. Par exemple, l’environnement local repose généralement sur des identifiants de base de données spécifiques, valables uniquement pour l’instance DB locale. L’environnement de production utiliserait un ensemble séparé d’identifiants DB. Comme les variables de configuration changent, la meilleure pratique est de stocker les variables de configuration dans l’environnement.
Les variables d’environnement définies de manière externe sont visibles dans Node.js via l’objet global process.env
. Nous pourrions essayer de résoudre le problème des multiples environnements en définissant les variables d’environnement séparément dans chaque environnement. Cela peut rapidement devenir lourd, en particulier dans les environnements de développement et de test où ces valeurs doivent être facilement simulées et/ou modifiées.
Dans les applications Node.js, il est courant d’utiliser des fichiers .env
, contenant des paires clé-valeur où chaque clé représente une valeur particulière, pour représenter chaque environnement. Faire fonctionner une application dans différents environnements est alors juste une question d’inverser le bon fichier .env
.
Une bonne approche pour utiliser cette technique dans Nest est de créer un ConfigModule
qui expose un ConfigService
qui charge le fichier .env
approprié. Bien que vous puissiez choisir d’écrire un tel module vous-même, par commodité, Nest fournit le package @nestjs/config
prêt à l’emploi. Nous allons couvrir ce package dans le chapitre actuel.
Installation
Pour commencer à l’utiliser, nous installons d’abord la dépendance requise.
$ npm i --save @nestjs/config
Démarrer
Une fois le processus d’installation terminé, nous pouvons importer le ConfigModule
. Typiquement, nous l’importerons dans le module racine AppModule
et contrôlerons son comportement en utilisant la méthode statique .forRoot()
. Pendant cette étape, les paires clé/valeur des variables d’environnement sont analysées et résolues. Plus tard, nous verrons plusieurs options pour accéder à la classe ConfigService
du ConfigModule
dans nos autres modules.
import { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';
@Module({ imports: [ConfigModule.forRoot()],})export class AppModule {}
Le code ci-dessus chargera et analysera un fichier .env
depuis l’emplacement par défaut (le répertoire racine du projet), fusionnera les paires clé/valeur du fichier .env
avec les variables d’environnement assignées à process.env
et stockera le résultat dans une structure privée à laquelle vous pouvez accéder via le ConfigService
. La méthode forRoot()
enregistre le fournisseur ConfigService
, qui fournit une méthode get()
pour lire ces variables de configuration analysées/fusionnées. Puisque @nestjs/config
repose sur dotenv, il suit les règles de ce package pour résoudre les conflits dans les noms de variables d’environnement. Lorsqu’une clé existe à la fois dans l’environnement d’exécution en tant que variable d’environnement (par exemple, via les exports de shell OS comme export DATABASE_USER=test
) et dans un fichier .env
, la variable d’environnement de l’environnement d’exécution a la priorité.
Un fichier .env
d’exemple ressemble à ceci :
DATABASE_USER=testDATABASE_PASSWORD=test
Chemin du fichier env personnalisé
Par défaut, le package recherche un fichier .env
dans le répertoire racine de l’application. Pour spécifier un autre chemin pour le fichier .env
, définissez la propriété envFilePath
d’un objet d’options (optionnel) que vous passez à forRoot()
, comme suit :
ConfigModule.forRoot({ envFilePath: '.development.env',});
Vous pouvez également spécifier plusieurs chemins pour les fichiers .env
comme ceci :
ConfigModule.forRoot({ envFilePath: ['.env.development.local', '.env.development'],});
Si une variable est trouvée dans plusieurs fichiers, la première prend la priorité.
Désactiver le chargement des variables d’environnement
Si vous ne souhaitez pas charger le fichier .env
, mais plutôt accéder simplement aux variables d’environnement depuis l’environnement d’exécution (comme avec les exports de shell OS comme export DATABASE_USER=test
), définissez la propriété ignoreEnvFile
de l’objet d’options sur true
, comme suit :
ConfigModule.forRoot({ ignoreEnvFile: true,});
Utiliser le module globalement
Lorsque vous souhaitez utiliser ConfigModule
dans d’autres modules, vous devrez l’importer (comme c’est standard avec n’importe quel module Nest). En option, déclarez-le comme un module global en définissant la propriété isGlobal
de l’objet d’options sur true
, comme illustré ci-dessous. Dans ce cas, vous n’aurez pas besoin d’importer ConfigModule
dans d’autres modules une fois qu’il a été chargé dans le module racine (par exemple, AppModule
).
ConfigModule.forRoot({ isGlobal: true,});
Fichiers de configuration personnalisés
Pour des projets plus complexes, vous pouvez utiliser des fichiers de configuration personnalisés pour renvoyer des objets de configuration imbriqués. Cela vous permet de regrouper des paramètres de configuration liés par fonction (par exemple, les paramètres liés à la base de données) et de les stocker dans des fichiers individuels pour faciliter leur gestion de manière indépendante.
Un fichier de configuration personnalisé exporte une fonction de fabrique qui renvoie un objet de configuration. L’objet de configuration peut être n’importe quel objet JavaScript simple et potentiellement imbriqué. L’objet process.env
contiendra les paires de clés/valeurs de variables d’environnement complètement résolues (avec le fichier .env
et les variables externes définies résolues et fusionnées comme décrit ci-dessus). Comme vous contrôlez l’objet de configuration renvoyé, vous pouvez ajouter toute logique requise pour convertir des valeurs en un type approprié, définir des valeurs par défaut, etc. Par exemple :
export default () => ({ port: parseInt(process.env.PORT, 10) || 3000, database: { host: process.env.DATABASE_HOST, port: parseInt(process.env.DATABASE_PORT, 10) || 5432, },});
Nous chargeons ce fichier en utilisant la propriété load
de l’objet d’options que nous passons à la méthode ConfigModule.forRoot()
:
import configuration from './config/configuration';
@Module({ imports: [ ConfigModule.forRoot({ load: [configuration], }), ],})export class AppModule {}
Avec des fichiers de configuration personnalisés, nous pouvons également gérer des fichiers personnalisés tels que des fichiers YAML. Voici un exemple de configuration utilisant le format YAML :
http: host: 'localhost' port: 8080
db: postgres: url: 'localhost' port: 5432 database: 'yaml-db'
sqlite: database: 'sqlite.db'
Pour lire et analyser des fichiers YAML, nous pouvons tirer parti du package js-yaml
.
$ npm i js-yaml$ npm i -D @types/js-yaml
Une fois le package installé, utilisons la fonction load
pour charger le fichier YAML que nous venons de créer ci-dessus.
import { readFileSync } from 'fs';import * as yaml from 'js-yaml';import { join } from 'path';
const YAML_CONFIG_FILENAME = 'config.yaml';
export default () => { return yaml.load( readFileSync(join(__dirname, YAML_CONFIG_FILENAME), 'utf8'), ) as Record<string, any>;};
Utilisation de ConfigService
Pour accéder aux valeurs de configuration depuis notre ConfigService
, nous devons d’abord injecter ConfigService
. Comme pour tout fournisseur, nous devons importer son module contenant - le ConfigModule
- dans le module qui l’utilisera (à moins que vous ne définissiez la propriété isGlobal
dans l’objet d’options passé à la méthode ConfigModule.forRoot()
sur true
). Importez-le dans un module de fonctionnalité comme montré ci-dessous.
import { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';
@Module({ imports: [ConfigModule], // ...})export class FeatureModule {}
Ensuite, nous pouvons l’injecter en utilisant l’injection constructeur standard :
constructor(private configService: ConfigService) {}
Et l’utiliser dans notre classe :
// obtenir une variable d'environnementconst dbUser = this.configService.get<string>('DATABASE_USER');
// obtenir une valeur de configuration personnaliséeconst dbHost = this.configService.get<string>('database.host');
Comme montré ci-dessus, utilisez la méthode configService.get()
pour obtenir une simple variable d’environnement en passant le nom de la variable. Vous pouvez faire un hint de type TypeScript en passant le type, comme montré ci-dessus (par exemple, get<string>(...)
). La méthode get()
peut également parcourir un objet de configuration personnalisé imbriqué (créé via un fichier de configuration personnalisé), comme montré dans le deuxième exemple ci-dessus.
Vous pouvez également obtenir l’ensemble de l’objet de configuration personnalisé imbriqué en utilisant une interface comme hint de type :
interface DatabaseConfig { host: string; port: number;}
const dbConfig = this.configService.get<DatabaseConfig>('database');
// vous pouvez maintenant utiliser `dbConfig.port` et `dbConfig.host`const port = dbConfig.port;
La méthode get()
prend également un second argument optionnel définissant une valeur par défaut, qui sera renvoyée lorsque la clé n’existe pas, comme montré ci-dessous :
// utiliser "localhost" quand "database.host" n'est pas définiconst dbHost = this.configService.get<string>('database.host', 'localhost');
Espaces de noms de configuration
Le ConfigModule
vous permet de définir et de charger plusieurs fichiers de configuration personnalisés, comme montré dans les Fichiers de configuration personnalisés ci-dessus. Vous pouvez gérer des hiérarchies d’objet de configuration complexes avec des objets de configuration imbriqués comme montré dans cette section. Alternativement, vous pouvez retourner un objet de configuration “nominatif” avec la fonction registerAs()
comme suit :
export default registerAs('database', () => ({ host: process.env.DATABASE_HOST, port: process.env.DATABASE_PORT || 5432,}));
Comme avec les fichiers de configuration personnalisés, à l’intérieur de votre fonction de fabrique registerAs()
, l’objet process.env
contiendra les paires de clés/valeurs de variables d’environnement complètement résolues (avec le fichier .env
et les variables externes définies résolues et fusionnées comme décrit ci-dessus).
Chargez une configuration nominative avec la propriété load
de l’objet d’options de la méthode forRoot()
, de la même manière que vous chargez un fichier de configuration personnalisé :
import databaseConfig from './config/database.config';
@Module({ imports: [ ConfigModule.forRoot({ load: [databaseConfig], }), ],})export class AppModule {}
Maintenant, pour obtenir la valeur host
de l’espace de noms database
, utilisez la notation par points. Utilisez 'database'
comme préfixe au nom de la propriété, correspondant au nom de l’espace de noms (passé comme premier argument à la fonction registerAs()
).
const dbHost = this.configService.get<string>('database.host');
Une alternative raisonnable consiste à injecter directement l’espace de noms database
. Cela nous permet de bénéficier d’un typage fort :
constructor( @Inject(databaseConfig.KEY) private dbConfig: ConfigType<typeof databaseConfig>,) {}
Mise en cache des variables d’environnement
Comme l’accès à process.env
peut être lent, vous pouvez définir la propriété cache
de l’objet d’options passé à ConfigModule.forRoot()
pour augmenter les performances de la méthode ConfigService#get
en ce qui concerne les variables stockées dans process.env
.
ConfigModule.forRoot({ cache: true,});
Inscription partielle
Jusqu’à présent, nous avons traité des fichiers de configuration dans notre module racine (par exemple, AppModule
), avec la méthode forRoot()
. Peut-être avez-vous une structure de projet plus complexe, avec des fichiers de configuration spécifiques à chaque fonctionnalité situés dans plusieurs répertoires différents. Plutôt que de charger tous ces fichiers dans le module racine, le package @nestjs/config
fournit une fonctionnalité appelée inscription partielle, qui ne fait appel qu’aux fichiers de configuration associés à chaque module de fonctionnalité. Utilisez la méthode statique forFeature()
dans un module de fonctionnalité pour effectuer cette inscription partielle, comme suit :
import databaseConfig from './config/database.config';
@Module({ imports: [ConfigModule.forFeature(databaseConfig)],})export class DatabaseModule {}
Dans certaines circonstances, vous devrez peut-être accéder aux propriétés chargées via l’inscription partielle en utilisant le hook onModuleInit()
, plutôt que dans un constructeur. Cela est dû au fait que la méthode forFeature()
est exécutée lors de l’initialisation du module, et l’ordre d’initialisation des modules est indéterminé. Si vous accédez aux valeurs chargées de cette manière par un autre module, dans un constructeur, le module dont la configuration dépend peut ne pas encore être initialisé. La méthode onModuleInit()
s’exécute uniquement après que tous les modules dont elle dépend ont été initialisés, donc cette technique est sécurisée.
Validation du schéma
Il est d’usage de lancer une exception durant le démarrage de l’application si les variables d’environnement requises n’ont pas été fournies ou si elles ne répondent pas à certaines règles de validation. Le package @nestjs/config
permet deux manières différentes de le faire :
- Un validateur intégré Joi. Avec Joi, vous définissez un schéma d’objet et validez les objets JavaScript par rapport à celui-ci.
- Une fonction
validate()
personnalisée qui prend les variables d’environnement en entrée.
Pour utiliser Joi, nous devons installer le package Joi :
$ npm install --save joi
Maintenant, nous pouvons définir un schéma de validation Joi et le passer via la propriété validationSchema
de l’objet d’options de la méthode forRoot()
, comme montré ci-dessous :
import * as Joi from 'joi';
@Module({ imports: [ ConfigModule.forRoot({ validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().port().default(3000), }), }), ],})export class AppModule {}
Par défaut, toutes les clés de schéma sont considérées comme optionnelles. Ici, nous définissons des valeurs par défaut pour NODE_ENV
et PORT
qui seront utilisées si nous ne fournissons pas ces variables dans l’environnement (fichier .env
ou environnement de processus). Alternativement, nous pouvons utiliser la méthode de validation required()
pour exiger qu’une valeur doit être définie dans l’environnement (fichier .env
ou environnement de processus). Dans ce cas, l’étape de validation lancera une exception si nous ne fournissons pas la variable dans l’environnement. Consultez les méthodes de validation Joi pour plus d’informations sur la façon de construire des schémas de validation.
Par défaut, les variables d’environnement inconnues (variables d’environnement dont les clés ne sont pas présentes dans le schéma) sont autorisées et ne déclenchent pas d’exception de validation. Par défaut, toutes les erreurs de validation sont signalées. Vous pouvez modifier ces comportements en passant un objet d’options via la clé validationOptions
de l’objet d’options forRoot()
. Cet objet d’options peut contenir n’importe quelle propriété standard des options de validation fournies par les options de validation Joi. Par exemple, pour inverser les deux paramètres ci-dessus, passez des options comme ceci :
import * as Joi from 'joi';
@Module({ imports: [ ConfigModule.forRoot({ validationSchema: Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().port().default(3000), }), validationOptions: { allowUnknown: false, abortEarly: true, }, }), ],})export class AppModule {}
Le package @nestjs/config
utilise des paramètres par défaut de :
allowUnknown
: contrôle s’il faut autoriser ou non des clés inconnues dans les variables d’environnement. Par défaut c’esttrue
.abortEarly
: sitrue
, stoppe la validation à la première erreur ; sifalse
, renvoie toutes les erreurs. Par défaut c’estfalse
.
Notez qu’une fois que vous décidez de passer un objet validationOptions
, tous les paramètres que vous ne passez pas explicitement utiliseront les valeurs par défaut standard de Joi (et non les valeurs par défaut du package @nestjs/config
). Par exemple, si vous laissez allowUnknown
non spécifié dans votre objet validationOptions
personnalisé, il prendra la valeur de par défaut de Joi qui est false
. Par conséquent, il est probablement plus sûr de spécifier les deux de ces paramètres dans votre objet personnalisé.
Fonction de validation personnalisée
En alternative, vous pouvez spécifier une fonction de validation synchronisée qui prend un objet contenant les variables d’environnement (du fichier .env et du processus) et renvoie un objet contenant les variables d’environnement validées afin que vous puissiez les convertir/mutater si nécessaire. Si la fonction lance une erreur, elle empêchera l’application de démarrer.
Dans cet exemple, nous allons procéder avec les packages class-transformer
et class-validator
. Tout d’abord, nous devons définir :
- une classe avec des contraintes de validation,
- une fonction de validation qui utilise les fonctions
plainToInstance
etvalidateSync
.
import { plainToInstance } from 'class-transformer';import { IsEnum, IsNumber, Max, Min, validateSync } from 'class-validator';
enum Environment { Development = 'development', Production = 'production', Test = 'test', Provision = 'provision',}
class EnvironmentVariables { @IsEnum(Environment) NODE_ENV: Environment;
@IsNumber() @Min(0) @Max(65535) PORT: number;}
export function validate(config: Record<string, unknown>) { const validatedConfig = plainToInstance(EnvironmentVariables, config, { enableImplicitConversion: true, });
const errors = validateSync(validatedConfig, { skipMissingProperties: false });
if (errors.length > 0) { throw new Error(errors.toString()); } return validatedConfig;}
Avec cela en place, utilisez la fonction validate
comme option de configuration du ConfigModule
, comme suit :
import { validate } from './env.validation';
@Module({ imports: [ ConfigModule.forRoot({ validate, }), ],})export class AppModule {}
Fonctions de getter personnalisées
Le ConfigService
définit une méthode générique get()
pour récupérer une valeur de configuration par clé. Nous pouvons également ajouter des fonctions de getter pour permettre un style d’écriture un peu plus naturel :
import { Injectable } from '@nestjs/common';import { ConfigService } from '@nestjs/config';
@Injectable()export class ApiConfigService { constructor(private configService: ConfigService) {}
get isAuthEnabled(): boolean { return this.configService.get<string>('AUTH_ENABLED') === 'true'; }}
Nous pouvons maintenant utiliser la fonction getter comme suit :
import { Injectable } from '@nestjs/common';import { ApiConfigService } from './app.config.service';
@Injectable()export class AppService { constructor(apiConfigService: ApiConfigService) { if (apiConfigService.isAuthEnabled) { // L'authentification est activée } }}
Hook de chargement des variables d’environnement
Si une configuration de module dépend des variables d’environnement et que ces variables sont chargées depuis le fichier .env
, vous pouvez utiliser le hook ConfigModule.envVariablesLoaded
pour vous assurer que le fichier a été chargé avant d’interagir avec l’objet process.env
, voir l’exemple suivant :
export async function getStorageModule() { await ConfigModule.envVariablesLoaded; return process.env.STORAGE === 'S3' ? S3StorageModule : DefaultStorageModule;}
Cette construction garantit qu’après la résolution de la promesse ConfigModule.envVariablesLoaded
, toutes les variables de configuration sont chargées.
Configuration conditionnelle de module
Il peut y avoir des fois où vous souhaitez charger un module de manière conditionnelle et spécifier la condition dans une variable d’environnement. Heureusement, @nestjs/config
fournit un ConditionalModule
qui permet de le faire.
@Module({ imports: [ ConfigModule.forRoot(), ConditionalModule.registerWhen(FooModule, 'USE_FOO'), ],})export class AppModule {}
Le module ci-dessus ne chargerait le FooModule
que s’il n’y a pas de valeur false
pour la variable d’environnement USE_FOO
dans le fichier .env
. Vous pouvez également passer une condition personnalisée vous-même, une fonction recevant la référence process.env
qui devrait renvoyer un booléen pour que le ConditionalModule
puisse le gérer :
@Module({ imports: [ ConfigModule.forRoot(), ConditionalModule.registerWhen(FooBarModule, (env: NodeJS.ProcessEnv) => !!env['foo'] && !!env['bar']), ],})export class AppModule {}
Il est important de s’assurer que lors de l’utilisation du ConditionalModule
vous avez également le ConfigModule
chargé dans l’application, afin que le hook ConfigModule.envVariablesLoaded
puisse être correctement référencé et utilisé. Si le hook n’est pas activé dans les 5 secondes ou un délai en millisecondes, défini par l’utilisateur dans le troisième paramètre d’options de la méthode registerWhen
, alors le ConditionalModule
lancera une erreur et Nest annulera le démarrage de l’application.
Variables extensibles
Le package @nestjs/config
prend en charge l’expansion des variables d’environnement. Avec cette technique, vous pouvez créer des variables d’environnement imbriquées, où une variable est référencée dans la définition d’une autre. Par exemple :
APP_URL=mywebsite.comSUPPORT_EMAIL=support@${APP_URL}
Avec cette construction, la variable SUPPORT_EMAIL
se résout à '[email protected]'
. Notez l’utilisation de la syntaxe ${...}
pour déclencher la résolution de la valeur de la variable APP_URL
à l’intérieur de la définition de SUPPORT_EMAIL
.
Activez l’expansion des variables d’environnement en utilisant la propriété expandVariables
dans l’objet d’options passé à la méthode forRoot()
du ConfigModule
, comme montré ci-dessous :
@Module({ imports: [ ConfigModule.forRoot({ expandVariables: true, }), ],})export class AppModule {}
Utilisation dans main.ts
Bien que notre configuration soit stockée dans un service, elle peut encore être utilisée dans le fichier main.ts
. De cette manière, vous pouvez l’utiliser pour stocker des variables telles que le port d’application ou l’hôte CORS.
Pour y accéder, vous devez utiliser la méthode app.get()
, suivie de la référence au service :
const configService = app.get(ConfigService);
Vous pouvez ensuite l’utiliser comme d’habitude, en appelant la méthode get
avec la clé de configuration :
const port = configService.get('PORT');