Configuration

Applications are often run in different environments. Depending on the environment, different configuration settings should be used. For example, usually the local environment relies on specific database credentials, valid only for the local DB instance. The production environment would use a separate set of DB credentials. Since configuration variables change, best practice is to store configuration variables in the environment.

Externally defined environment variables are visible inside Node.js through the process.env global. We could try to solve the problem of multiple environments by setting the environment variables separately in each environment. This can quickly get unwieldy, especially in the development and testing environments where these values need to be easily mocked and/or changed.

In Node.js applications, it’s common to use .env files, holding key-value pairs where each key represents a particular value, to represent each environment. Running an app in different environments is then just a matter of swapping in the correct .env file.

A good approach for using this technique in Nest is to create a ConfigModule that exposes a ConfigService which loads the appropriate .env file, depending on the $NODE_ENV environment variable.

Installation

In order to parse our environment files, we’ll use the dotenv package.

  1. $ npm i --save dotenv
  2. $ npm i --save-dev @types/dotenv

Service

First, we create a ConfigService class that will perform the necessary .env file parsing and provide an interface for reading configuration variables.

  1. @@filename(config/config.service)
  2. import * as dotenv from 'dotenv';
  3. import * as fs from 'fs';
  4. export class ConfigService {
  5. private readonly envConfig: Record<string, string>;
  6. constructor(filePath: string) {
  7. this.envConfig = dotenv.parse(fs.readFileSync(filePath))
  8. }
  9. get(key: string): string {
  10. return this.envConfig[key];
  11. }
  12. }
  13. @@switch
  14. import * as dotenv from 'dotenv';
  15. import * as fs from 'fs';
  16. export class ConfigService {
  17. constructor(filePath) {
  18. this.envConfig = dotenv.parse(fs.readFileSync(filePath))
  19. }
  20. get(key) {
  21. return this.envConfig[key];
  22. }
  23. }

This class takes a single argument, a filePath, which is a path to your .env file. The get() method enables access to a private envConfig object that holds each property defined in the parsed environment file.

The next step is to create a ConfigModule.

  1. @@filename()
  2. import { Module } from '@nestjs/common';
  3. import { ConfigService } from './config.service';
  4. @Module({
  5. providers: [
  6. {
  7. provide: ConfigService,
  8. useValue: new ConfigService(`${process.env.NODE_ENV || 'development'}.env`),
  9. },
  10. ],
  11. exports: [ConfigService],
  12. })
  13. export class ConfigModule {}

The ConfigModule registers a ConfigService and exports it for visibility in other consuming modules. Additionally, we used the useValue syntax (see Custom providers) to pass the path to the .env file. This path will be different depending on the actual execution environment as contained in the NODE_ENV environment variable (e.g., 'development', 'production', etc.).

Now you can simply inject ConfigService anywhere, and retrieve a particular configuration value based on a passed key.

A sample development.env file could look like this:

  1. DATABASE_USER = test
  2. DATABASE_PASSWORD = test

Using the ConfigService

To access environment variables from our ConfigService, we first need to inject it. Therefore we need to import the ConfigModule into the module that will use it.

  1. @@filename(app.module)
  2. @Module({
  3. imports: [ConfigModule],
  4. ...
  5. })

Then we can inject it using standard constructor injection, and use it in our class:

  1. @@filename(app.service)
  2. import { Injectable } from '@nestjs/common';
  3. import { ConfigService } from './config/config.service';
  4. @Injectable()
  5. export class AppService {
  6. private isAuthEnabled: boolean;
  7. constructor(config: ConfigService) {
  8. // Please take note that this check is case sensitive!
  9. this.isAuthEnabled = config.get('IS_AUTH_ENABLED') === 'true';
  10. }
  11. }

info Hint Instead of importing ConfigModule in each module, you can alternatively declare ConfigModule as a global module.

Advanced configuration

We just implemented a basic ConfigService. However, this simple version has a couple of disadvantages, which we’ll address now:

  • missing names & types for the environment variables (no IntelliSense)
  • no validation of the provided .env file
  • the service treat a boolean value as a string ('true'), so we subsequently have to cast them to boolean after retrieving them

Validation

We’ll start with validation of the provided environment variables. A good technique is to throw an exception if required environment variables haven’t been provided or if they don’t meet certain validation rules. For this purpose, we are going to use the Joi npm package. With Joi, you define an object schema and validate JavaScript objects against it.

Install Joi (and its types, for TypeScript users):

  1. $ npm install --save @hapi/joi
  2. $ npm install --save-dev @types/hapi__joi

Now we can utilize Joi validation features in our ConfigService.

  1. @@filename(config.service)
  2. import * as dotenv from 'dotenv';
  3. import * as Joi from '@hapi/joi';
  4. import * as fs from 'fs';
  5. export type EnvConfig = Record<string, string>;
  6. export class ConfigService {
  7. private readonly envConfig: EnvConfig;
  8. constructor(filePath: string) {
  9. const config = dotenv.parse(fs.readFileSync(filePath));
  10. this.envConfig = this.validateInput(config);
  11. }
  12. /**
  13. * Ensures all needed variables are set, and returns the validated JavaScript object
  14. * including the applied default values.
  15. */
  16. private validateInput(envConfig: EnvConfig): EnvConfig {
  17. const envVarsSchema: Joi.ObjectSchema = Joi.object({
  18. NODE_ENV: Joi.string()
  19. .valid('development', 'production', 'test', 'provision')
  20. .default('development'),
  21. PORT: Joi.number().default(3000),
  22. API_AUTH_ENABLED: Joi.boolean().required(),
  23. });
  24. const { error, value: validatedEnvConfig } = envVarsSchema.validate(
  25. envConfig,
  26. );
  27. if (error) {
  28. throw new Error(`Config validation error: ${error.message}`);
  29. }
  30. return validatedEnvConfig;
  31. }
  32. }

Since we set default values for NODE_ENV and PORT the validation will not fail if we don’t provide these variables in the environment file. Conversely, because there’s no default value, our env file needs to explicitly provide API_AUTH_ENABLED. The validation step will also throw an exception if we have variables in our .env file which aren’t part of the schema. Finally, Joi tries to convert the string values from the .env file into the right type, solving our “booleans as strings” problem from above.

Custom getter functions

We already defined a generic get() method to retrieve a configuration value by key. We may also add getter functions to enable a little more natural coding style:

  1. @@filename(config.service)
  2. get isApiAuthEnabled(): boolean {
  3. return Boolean(this.envConfig.API_AUTH_ENABLED);
  4. }

Now we can use the getter function as follows:

  1. @@filename(app.service)
  2. @Injectable()
  3. export class AppService {
  4. constructor(config: ConfigService) {
  5. if (config.isApiAuthEnabled) {
  6. // Authorization is enabled
  7. }
  8. }
  9. }