Nest JS Managing Env in all differant ways

Nest JS Managing Environment in all differant ways for Production Apps

https://github.com/tkssharma/blogs/tree/master/nestjs-env-using-configs

When i talk about managing env in any application, then there are many possible ways to so that, Managing env means how to isolate env for different application environment and how to populate env variable for node js runtime environment by populating then in process.env

in many places its simple

There can be two simple question in context of nestjs app as for node js app its simple

  • how to manage multiple environment in different files for local development like .env, env.developent, .env.staging, .env.testing
  • how to populate env for node js runtime environment for nest js App

Different pattens in nestjs app to populate env

i hope you already know hoe to create basic nestjs app using CLI Nest.js Application Let’s continue with NestJS! We are going to install the NestJS CLI, so open the terminal of your choice and type:

$ npm i -g @nestjs/cli
$ nest new nest-env

We initialize a new NestJS project with its CLI. That might take up to a minute. The “-p npm” flag means, that we going to choose NPM as our package manager. If you want to choose another package manager, just get rid of this flag.

$ mkdir src/common
$ mkdir src/common/envs
$ mkdir src/common/helper

Additionally, let’s create some files.

$ touch src/common/envs/.env
$ touch src/common/envs/development.env
$ touch src/common/envs/production.env
$ touch src/common/helper/env.helper.ts

After this command is done you can open your project in your code editor. Since I use Visual Studio Code, I gonna open the project by typing:

$ cd nest-env
$ code .

Now lets follow what all you need to have for configuration

Option-1

nestjs provides @nestjs/config module for managing configuration

$ npm i --save @nestjs/config
$ npm i --save-dev @types/node

Our .env file can be a simple example file and we already know we don't push .env to repository we can push only some simple .env.example file

DATABASE_HOST=localhost
DATABASE_NAME=sample

we are creating a simple helper function to load things from .env and provided fallback to .env of .env.development is not there

  • this script is reading env from a file based on env set in process.env.NODE_ENV, if its not set then it will pick things from .env fallback file
import { existsSync } from 'fs';
import { resolve } from 'path';

export function getEnvPath(dest: string): string {
  const env: string | undefined = process.env.NODE_ENV;
  const fallback: string = resolve(`${dest}/.env`);
  const filename: string = env ? `${env}.env` : 'development.env';
  let filePath: string = resolve(`${dest}/${filename}`);

  if (!existsSync(filePath)) {
    filePath = fallback;
  }

  return filePath;
}

Now we have to use this in app module, and load .env file

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

const envFilePath: string = getEnvPath(`${__dirname}/common/envs`);

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

Finally how to use these env variable in our application, ConfigService will do the rest of the magaic in captuting the date

import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
  @Inject(ConfigService)
  public config: ConfigService;

  public getHello(): string {
    const databaseName: string = this.config.get('DATABASE_NAME');

    console.log({ databaseName });

    return 'Hello World!';
  }
}

Option-2

Default Implementation Provided by ConfigModule by loading a .env file

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

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

The above code will load and parse a .env file from the default location (the project root directory), merge key/value pairs from the .env file with environment variables assigned to process.env, and store the result in a private structure that you can access through the ConfigService. The forRoot() method registers the ConfigService provider, which provides a get() method for reading these parsed/merged configuration variables. Since @nestjs/config relies on dotenv, it uses that package's rules for resolving conflicts in environment variable names. When a key exists both in the runtime environment as an environment variable (e.g., via OS shell exports like export DATABASE_USER=test) and in a .env file, the runtime environment variable takes precedence.

A sample .env file looks something like this:

DATABASE_USER=test
DATABASE_PASSWORD=test

Custom env file path#

By default, the package looks for a .env file in the root directory of the application. To specify another path for the .env file, set the envFilePath property of an (optional) options object you pass to forRoot(), as follows:

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

You can also specify multiple paths for .env files like this:

ConfigModule.forRoot({
  envFilePath: ['.env.development.local', '.env.development'],
});

On top of that we can also schema validation It is standard practice to throw an exception during application startup if required environment variables haven't been provided or if they don't meet certain validation rules. The @nestjs/config package enables two different ways to do this:

Joi built-in validator. With Joi, you define an object schema and validate JavaScript objects against it. A custom validate() function which takes environment variables as an input. To use Joi, we must install Joi package:

$ npm install --save joi

Updated app module with schema validation of env variables

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().default(3000),
      }),
    }),
  ],
})
export class AppModule {}

By default, all schema keys are considered optional. Here, we set default values for NODE_ENV and PORT which will be used if we don't provide these variables in the environment (.env file or process environment). Alternatively,

we can use the required() validation method to require that a value must be defined in the environment (.env file or process environment). In this case, the validation step will throw an exception if we don't provide the variable in the environment. See Joi validation methods for more on how to construct validation schemas.

with custom validate function

import { plainToClass } from 'class-transformer';
import { IsEnum, IsNumber, validateSync } from 'class-validator';

enum Environment {
  Development = "development",
  Production = "production",
  Test = "test",
  Provision = "provision",
}

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToClass(
    EnvironmentVariables,
    config,
    { enableImplicitConversion: true },
  );
  const errors = validateSync(validatedConfig, { skipMissingProperties: false });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}

With this in place, use the validate function as a configuration option of the ConfigModule, as follows:

app.module.ts

import { validate } from './env.validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      validate,
    }),
  ],
})
export class AppModule {}

Option-3

Write your own config module with config service if we don't want to use config Module, we can build a simple config service with config module which will act same as nestjs provided config Module and service

import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';

const configFactory = {
  provide: ConfigService,
  useFactory: () => {
    const config = new ConfigService();
    config.loadFromEnv();
    return config;
  },
};

@Module({
  imports: [],
  controllers: [],
  providers: [configFactory],
  exports: [configFactory],
})
export class ConfigModule {}

Config service

import { Injectable } from '@nestjs/common';
import { urlJoin } from 'url-join-ts';
import { DEFAULT_CONFIG } from './config.default';
import {
  ConfigAuthData,
  ConfigData,
  ConfigDBData, KafkaConfig, PlatformAPIs,
  SwaggerUserConfig
} from './config.interface';


/**
 * Provides a means to access the application configuration.
 */
@Injectable()
export class ConfigService {
  private config: ConfigData;

  constructor(data: ConfigData = DEFAULT_CONFIG) {
    this.config = data;
  }

  /**
   * Loads the config from environment variables.
   */
  public loadFromEnv() {
    this.config = this.parseConfigFromEnv(process.env);
  }

  private parseConfigFromEnv(env: NodeJS.ProcessEnv): ConfigData {
    return {
      env: env.NODE_ENV || DEFAULT_CONFIG.env,
      port: parseInt(env.PORT!, 10),
      db: this.parseDbConfigFromEnv(env, DEFAULT_CONFIG.db),
      logLevel: env.LOG_LEVEL || DEFAULT_CONFIG.logLevel,
      newRelicKey: env.NEW_RELIC_KEY || DEFAULT_CONFIG.
    }
  }
    /**
   * Retrieves the config.
   * @returns immutable view of the config data
   */
  public get(): Readonly<ConfigData> {
    return this.config;
  }
}

If we look at config moule which is loading variable from process.env and populating data in process.env and returning config

const configFactory = {
  provide: ConfigService,
  useFactory: () => {
    const config = new ConfigService();
    config.loadFromEnv();
    return config;
  },
};

Now in our code we can get env variable using config.get() function, example like in DB module you can pass this or inject this config service to get DB connection details const dbdata = config.get().db;

@Module({})
export class DbModule {
  private static getConnectionOptions(config: ConfigService, dbconfig: DbConfig): TypeOrmModuleOptions {
    const dbdata = config.get().db;
    if (!dbdata) {
      throw new DbConfigError('Database config is missing');
    }
    const connectionOptions = DbModule.getConnectionOptionsPostgres(dbdata);
    return {
      ...connectionOptions,
      entities: dbconfig.entities,
      synchronize: false,
      logging: false,
    };
  }

Conclusion

There can be other options but in all either we write custom config service or use @nestjs provided config service, I hope these options are helpful and you can deisgn your own

References

Comments