Skip to content
John Biundo edited this page Aug 24, 2019 · 9 revisions

Table of Contents

The public API for the ConfigManager, which is inherited by the ConfigService (name it whatever you want) that you implement, has only two methods.

get() method

The ConfigManager builds a hash of the environment variables it parses, cascades and validates. This hash (which is private to the ConfigManager) ends up looking something like this:

  {
    'DB_HOST': 'localhost',
    'DB_USER': 'devdbuser',
    'DB_PASS': 'devdbpass',
    'DB_PORT': 3278
  }

The ConfigManager (and therefore your ConfigService) provides a type-safe get<T>() method to get a value from the hash. Use it like this:

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

@Injectable()
export class AppService {
  private readonly DB_HOST: string;
  private readonly DB_USER: string;
  private readonly DB_PASS: string;
  private readonly DB_PORT: number;
  constructor(private readonly configService: ConfigService) {
    this.DB_HOST = configService.get<string>('DB_HOST');
    this.DB_USER = configService.get<string>('DB_USER');
    this.DB_PASS = configService.get<string>('DB_PASS');
    this.DB_PORT = configService.get<number>('DB_PORT');
  }
}

Adding custom getters

You may want to add custom methods to your ConfigService to make it easier to integrate with other parts of your application.

Using the NestJSConfigManager with Nest Dynamic Modules

Note: see https://github.com/nestjs/nest/issues/1300#issuecomment-444870454 for more from the NestJS author on how to inject into a Dynamic Module]

As a convenience, you may want to provide methods that return composed objects representing configuration options for services like database services and other dynamically configured modules. You can easily inject NestJSConfigManager into these modules using standard Nest Dependency Injection

For example, consider this example of TypeOrm module registration from the Nest documentation:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.getString('HOST'),
    port: configService.getString('PORT'),
    username: configService.getString('USERNAME'),
    password: configService.getString('PASSWORD'),
    database: configService.getString('DATABASE'),
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
  }),
  inject: [ConfigService],
});

To make this a little more concise, you could implement a createTypeOrmOptions() method in your ConfigService (shown below), and simplify the module registration to:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) =>
    configService.createTypeOrmOptions(),
  inject: [ConfigService],
});

Your ConfigService to implement this would look like (note the createTypeOrmOptions() method):

// src/config/config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigManager } from '@nestjsplus/config';
import * as Joi from 'joi';

@Injectable()
export class ConfigService extends ConfigManager {
  provideConfigSpec() {
    return {
      DB_HOST: {
        validate: Joi.string(),
        required: false,
        default: 'localhost',
      },
      DB_PORT: {
        validate: Joi.number()
          .min(5000)
          .max(65535),
        required: false,
        default: 5432,
      },
      DB_USERNAME: {
        validate: Joi.string(),
        required: true,
      },
      DB_PASSWORD: {
        validate: Joi.string(),
        required: true,
      },
      DB_NAME: {
        validate: Joi.string(),
        required: true,
      },
    };
  }

  // return the options expected by the TypeOrmModule's
  // forRootAsync() configuration method
  public createTypeOrmOptions() {
    return {
      type: 'mysql',
      host: this.get<string>('DB_HOST'),
      port: this.get<number>('DB_PORT'),
      username: this.get<string>('DB_USERNAME'),
      password: this.get<string>('DB_PASSWORD'),
      database: this.get<string>('DB_NAME'),
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }
  }
}

You can create any number of these dynamic module configuration methods. For example, for the @nestjs/jwt module's JwtModule, you might want:

// in the module where you register the JwtModule
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) =>
        configService.createJwtOptions(),
      inject: [ConfigService],
    }),

And in your ConfigService:

.
.
.
  public createJwtOptions() {
    return {
      signOptions: {
        expiresIn: this.get<number>('JWT_EXPIRATION')
      }
      secret: this.get<string>('JWT_SECRET'),
    };
  }
.
.
.
Namespaced environment variables

You can also create a namespaced API to your environment variables.

// src/config/config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigManager } from '@nestjsplus/config';
import * as Joi from 'joi';

@Injectable()
export class ConfigService extends ConfigManager {
  provideConfigSpec() {
    return {
      DB_HOST: {
        validate: Joi.string(),
        required: false,
        default: 'localhost',
      },
      DB_PORT: {
        validate: Joi.number()
          .min(5000)
          .max(65535),
        required: false,
        default: 5432,
      },
      DB_USERNAME: {
        validate: Joi.string(),
        required: true,
      },
      DB_PASSWORD: {
        validate: Joi.string(),
        required: true,
      }
      DB_NAME: {
        validate: Joi.string(),
        required: true,
      },
    };
  }

  getDB() {
    return {
      HOST: this.get('DB_HOST'),
      PORT: this.get('DB_PORT'),
      USERNAME: this.get('DB_USERNAME'),
      PASSWORD: this.get('DB_PASSWORD'),
      DATABASE: this.get('DB_NAME'),
    }
  }
}

To use this elsewhere in your code, you could write:

  const DB = configService.getDB();
  console.log(`DB Name = ${DB.DATABASE}`);
  // alternatively
  console.log(`DB NAME = ${configService.getDB().DATABASE}`);

trace() method

The trace method returns a hash object called a resolveMap that looks like the sample below. It's primary purpose is to debug issues with how the environment has been resolved at runtime, where it can sometimes be tricky to discover how the cascade resolved.

For each environment variable in the schema, resolveMap has an object keyed by that variable name, with 6 properties:

  • dotenv - shows the value for this variable in the .env file; shows '--' if there is none
  • env - shows the value for this variable in the external environment; shows '--' if there is none
  • default - shows the default value for this variable provided by the schema; shows '--' if there is none
  • resolvedFrom - shows how the environment variable was resolved (from the .env file, the external environment, or the default value from the schema)
  • isExtra - true if the field is present in the .env file but not in the schema and allowExtras is set to true
  • resolvedValue - shows the final resolved value for the environment variable

A `resolveMap` looks like this:

{
  DB_HOST: {
    dotenv: '--',
    env: '--',
    default: 'localhost',
    resolvedFrom: 'default',
    isExtra: false,
    resolvedValue: 'localhost'
  },
  DB_PORT: {
    dotenv: '--',
    env: '--',
    default: 5432,
    resolvedFrom: 'default',
    isExtra: false,
    resolvedValue: 5432
  },
  DB_USERNAME: {
    dotenv: 'john',
    env: '--',
    default: '--',
    resolvedFrom: 'dotenv',
    isExtra: false,
    resolvedValue: 'john'
  },
  DB_PASSWORD: {
    dotenv: 'mypassword',
    env: '--',
    default: '--',
    resolvedFrom: 'dotenv',
    isExtra: false,
    resolvedValue: 'mypassword'
  },
  DB_NAME: {
    dotenv: 'mydb',
    env: '--',
    default: '--',
    resolvedFrom: 'dotenv',
    isExtra: false,
    resolvedValue: 'mydb'
  },
}

Clone this wiki locally