Any real-world application will run in multiple environments. Each environment will require its own settings and parameters. For example, a development environment will have its own database instance. Production will have another instance with different credentials. To manage these differences in a typical application, NestJS Config plays a key role.

NestJS Config allows us to manage environment variables of our application. It provides a one-stop solution to handle all configuration aspects of your application. Using the NestJS Config Service, we can make our application portable across various run-time environments without the need to modify code and build environment-specific artifacts.

1 – NestJS Config Package Installation

To use NestJS Config, we first need to install the necessary package:

$ npm install --save @nestjs/config

This packages provides support for managing environment variables and configuration in NestJS. Internally, the @nestjs/config package uses dotenv.

2 – NestJS Configuration Basic Example

To see NestJS Config in action, let us consider a basic example.

The first step is to configure the ConfigModule by importing it in the App module. See below:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

The above piece of code (specifically adding the ConfigModule in the imports array) is the bare minimum requirement to manage configuration objects.

By default, NestJS will look for a .env file inside the project root directory. If the file is present, it will merge the key/value pairs present in the .env file with other environment variables from the process.env.

See below example of .env file. Basically, this is where we place our environment variables as key/value pairs. In this case, we only have one variable GREETING_MSG.

GREETING_MSG=Greetings from NestJS Configuration

We can now access this environment variable using the ConfigService. See below:

import { Controller, Get } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

@Controller('config')
export class DemoController {
    constructor(private readonly configService: ConfigService) {}

    @Get()
    async getGreetingMessage() {
        const greetingMessage = this.configService.get<string>('GREETING_MSG')

        return greetingMessage;
    }
}

Basically, we inject an instance of the ConfigService within a simple NestJS Controller. Then, we can simply use the get() method to pull out properties from the environment variables.

In other words, NestJS Config merges the various properties or key/value pairs and stores the result in a private structure. We can access this structure using the get() method. In this case, we pull out the property with key value GREETING_MSG.

info

INFO

The @nestjs/config package uses dotenv behind the scenes. In other words, the rules applicable to dotenv apply to @nestjs/config as well. In case a key exists in the runtime environment (via OS shell exports) and also in a .env file, the runtime value takes precedence.

3 – NestJS Custom Config File Path

By default, the @nestjs/config looks for the .env file in the root directory. However, we can also specify another path or name for the .env file.

See below example:

ConfigModule.forRoot({
  envFilePath: '.env.dev',
});

We can also specify multiple environment files as below:

ConfigModule.forRoot({
  envFilePath: ['.env.dev', '.env.prod'],
});

Basically, the envFilePath property can take an array of file names as input.

If both of these files contain the same key GREETING_MSG, the value from the first file takes precedence. In other words, the value from the .env.dev file.

4 – Handling NestJS Multiple Configuration Files

The previous section raises the question on the purpose of having multiple configuration files if only the first file values get precedence.

What if we wish to apply different properties based on the runtime environment (development or production)?

With NestJS Multiple Configuration setup, we can apply the required file based on the run-time of the application. To do so, we need to pass NODE_ENV while starting the application. This can be done by tweaking the package.json scripts as below:

"scripts": {
    "start": "NODE_ENV=dev nest start",
}

Now, we can use the NODE_ENV variable to determine the correct configuration file.

ConfigModule.forRoot({
      envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
}

Basically, we use process.env to access the NODE_ENV variable and use it to build the environment file path. When NODE_ENV has a value dev, the .env.dev file will be picked up. If the NODE_ENV value is prod, the .env.prod file will be used.

5 – NestJS Config Module TypeORM properties

TypeORM is a popular ORM we use to connect our application to database. We usually provide database connection details while configuring the TypeORM module as below:

TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'library',
      entities: [Book],
      synchronize: true,
      dropSchema: true
}), 

As you can see, we hard-code the database host and even the credentials. However, in real-life, these parameters can vary depending on whether the application is running in development or production.

With NestJS Config Module, we can configure TypeORM properties based on the runtime environment.

See below example.

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm'
import { async } from 'rxjs';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DemoModule } from './demo/demo.module';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async(configService: ConfigService) => ({
        type: 'mysql',
        host: configService.get('DATABASE_HOST'),
        port: configService.get('DATABASE_PORT'),
        username: 'root',
        password: 'password',
        database: 'library',
        entities: [Book],
        synchronize: true,
        dropSchema: true
      }),
      inject: [ConfigService]
    }),
    DemoModule,
    ConfigModule.forRoot({   
      envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Here, instead of forRoot() static method, we use the static method forRootAsync() on the TypeOrmModule. This is to load the properties asynchronously. Within the context of the function, we import the ConfigModule and use the factory method to setup the database properties. In order to make the database properties available within the context, we inject an instance of the ConfigService and call its get() method to pull the properties from the environment variable object.

In the above example, we pull the DATABASE_HOST and DATABASE_PORT values from the configuration. These properties are available within the .env.dev and .env.prod files. See below example from the .env.dev file.

GREETING_MSG=This is greeting from Development Environment
DATABASE_HOST=localhost
DATABASE_PORT=3306

6 – Making NestJS Config Service Global

Typically, when we want to use ConfigModule in other modules, we need to import it. This is in-line with how NestJS Module System works.

However, we can also declare the ConfigModule as a global module by setting the isGlobal property to true.

See below:

ConfigModule.forRoot({
  envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
  isGlobal: true    
}

With this property, we don’t need to explicitly import ConfigModule in other modules.

7 – NestJS Custom Configuration File

With NestJS Config, we can also load environment variables using custom configuration files.

To do so, our custom configuration file exports a factory function. Basically, this factory function returns a configuration object. See below example:

export default () => ({
   greeting_msg: process.env.GREETING_MSG,
   database: {
     host: process.env.DATABASE_HOST,
     port: parseInt(process.env.DATABASE_PORT, 10) || 3306
   }
})

As you can see, we return a simple Javascript object with some properties. The process.env object contains environment variables after merging and resolving the properties from .env files and also external variables. We can also add arbitrary logic to set values depending on the requirement.

We can now load this file in the ConfigModule. See below:

import configuration from './config/configuration';

ConfigModule.forRoot({
   envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
   load: [configuration]
})

As you can see, we are using the load property of the options object. Note that the load property accepts an array. In other words, you can load multiple configuration files.

The advantage of this approach is that you can have different configuration files for different configuration groupings. For example, one file for databaseConfig, one for redisConfig and so on. This allows developers to isolate configuration into smaller segments for easy maintenance.

8 – NestJS Config YAML Approach

NestJS Config also supports YAML files for setting up the environment variables. To work with YAML files, we need to install a couple of packages as below:

$ npm install js-yaml
$ npm install -D @types/js-yaml

After installation, we can create a YAML configuration file as below:

http:
  port: 3000

db:
  mysql:
    host: 'localhost'
    port: 3306

Now, we need to load this YAML file using a custom configuration file as below:

import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';
const YAML_CONFIG = 'config.yaml';

export default () => {
    return yaml.load(
        readFileSync(join(__dirname, YAML_CONFIG), 'utf8'),
    )as Record<string, any>
}

Finally, we can load this configuration by specifying the same in the ConfigModule load property.

import configurationYaml from './config/configuration.yaml';

ConfigModule.forRoot({
   load: [configurationYaml]
})

Basically, in this case, the environment variables are coming from the YAML file. We can then use the variables as needed using the ConfigService.get() method.

const databaseConfiguration = this.configService.get<string>('db.mysql.host')

An important point to note here is that NestJS does not automatically copy non-TS files to the dist folder during the build process. In other words, our YAML config file won’t be automatically copied to the distribution. This will cause errors while running the application. To copy the files properly, we need to add compilerOptions as below to the nest-cli.json.

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}],
  }
}

The assets property specifies the path to the config folder where the YAML files are present and where we want to actually copy them (the dist folder). Here, the config folder is at the same level as the src folder.

9 – NestJS Config Namespaces

Similar to custom configuration files, NestJS Config also supports namespaced configuration object. This allows us to separate configuration scopes such as database configuration, redis configuration and so on.

See below example:

import { registerAs } from "@nestjs/config";

export default registerAs('redis', () => ({
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT
}))

Inside the registerAs() factory function, we can directly access the process.env object. Basically, this object contains the merged and resolved key/value pairs from the environment files.

We can now load the namespaced configuration file using the load property. See below example:

import redisConfig from './config/redis.config';

ConfigModule.forRoot({
   envFilePath: `${process.cwd()}/.env.${process.env.NODE_ENV}`,
   load: [redisConfig]
})

We can access a specific property using the ConfigService as below:

const redisHost = this.configService.get<string>('redis.host')

10 – NestJS Config Cache Environment Variables

NestJS Config also provides the option of caching environment variables. We can enable caching by setting the cache property to true.

ConfigModule.forRoot({
  cache: true,
});

This helps speed up the loading process since accessing process.env is generally slower.

11 – NestJS Config in main.ts

We have seen all examples where the configuration is stored in a service.

However, we can also use NestJS Config in main.ts. This is particularly useful to access variables such as application port.

See below example:

import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const message = configService.get("GREETING_MSG")
  console.log(message);
  await app.listen(3000);
}
bootstrap();

Here, we obtain an instance of the ConfigService using app.get(). Then, we can simply use the get() method from the configService to access a particular property.

Conclusion

NestJS Config is one of the most flexible packages with a plethora of options to manage environment variables. In this post, we covered all the important patterns for setting up configuration in a NestJS application.

However, we also need to sometimes validate our environment variables. To learn about it, check out this post NestJS Config Validation.

If you have any comments or queries about this post, please feel free to mention them in the comments section below.


0 Comments

Leave a Reply

Your email address will not be published.