@vinsidious/nestjs-config

Easy to use Nest congifuration module

Usage no npm install needed!

<script type="module">
  import vinsidiousNestjsConfig from 'https://cdn.skypack.dev/@vinsidious/nestjs-config';
</script>

README

Nestjs Config

Configuration component for NestJs.

Note

This is a fork of the original nestjs-config which has been modified to use dotenv-flow instead of dotenv.

Features

  • Load your configurations with globs
  • Support for different environment configuration, thanks to dotenv
  • Change and Load configuration at runtime

Installation

Yarn

yarn add nestjs-config

NPM

npm install nestjs-config --save

Getting Started

Let's imagine that we have a folder called config in our project under src

/src
├── app.module.ts
├── config
│   ├── express.ts
│   ├── graphql.ts
│   └── grpc.ts

Let's register the config module in app.module.ts

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

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

That's it!

Now let's say that your application isn't in a folder called src, it's in ./app.

import * as path from 'path';
import { Module } from '@nestjs/common';
import { ConfigModule } from "nestjs-config";

@Module({
    imports: [
        ConfigModule.load(
            path.resolve(__dirname, 'config/**/*.{ts,js}')
        ),
    ],
})
export class AppModule {}

We provide as first argument the glob of our interested configuration that we want to load.

Complex project structure

For example project has the next structure:

/
├── dist/
├── src/
│   ├── app/
│   │   ├── app.module.ts
│   │   └── bootstrap/
│   │   │   ├── index.ts
│   │   │   └── bootstrap.module.ts
│   ├── migrations/
│   ├── cli/
│   ├── config/
│   │   ├── app.ts
│   │   └── database.ts
│   └── main.ts
├── tsconfig.json
└── package.json

On this example, config files are located near the app folder, because they are shared between app, migrations and cli scripts.

Also during typescript compilation all files from src/ folder will be moved to the dist/ folder.

Moreover ConfigModule is imported in BootstrapModule, but not directly in AppModule.

// app.module.ts
import { Module } from '@nestjs/common';
import { BootstrapModule } from "./bootstrap";

@Module({
    imports: [BootstrapModule],
})
export class AppModule {}
// bootstrap.module.ts
import * as path from 'path';
import { Module } from '@nestjs/common';
import { ConfigModule } from "nestjs-config";

@Module({
    imports: [
      ConfigModule.load(path.resolve(__dirname, '../../config/**/*.{ts,js}')),
    ],
})
export class BootstrapModule {}

We still provide as first argument the glob of our configuration, but an example above looks a little bit ugly.

Also we will always have to remember about this glob path when we want to move the BootstrapModule to different place.

There is two ways to avoid such situations:

  • Explicitly set absolute path to the project sources from AppModule and use glob with relative path:

    // app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigService } from "nestjs-config";
    import * as path from "path";
    import { BootstrapModule } from "./bootstrap";
    
    ConfigService.srcPath = path.resolve(__dirname, '..');
    
    @Module({
        imports: [BootstrapModule],
    })
    export class AppModule {}
    
    // bootstrap.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from "nestjs-config";
    
    @Module({
        imports: [
          ConfigModule.load('config/**/*.{ts,js}')
        ],
    })
    export class BootstrapModule {}
    
  • Invoke ConfigModule.resolveSrcPath(__dirname) from any your module before config loading and use glob with relative path.

    // bootstrap.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from "nestjs-config";
    
    @Module({
        imports: [
          ConfigModule.resolveSrcPath(__dirname).load('config/**/*.{ts,js}')
        ],
    })
    export class BootstrapModule {}
    

On these examples we provide as first argument the glob of our configuration, but it is relative to the src/ folder.

Environment configuration

This package ship with the amazing dotenv so that you can create a .env file in your preferred location.

let's create one!

# .env

EXPRESS_PORT=3000

now in our src/config/express.ts file we can refer to that environment variable

// src/config/express.ts


export default {

    port: process.env.EXPRESS_PORT
}

Note: By default the package look for a .env file in the path that you started your server from. If you want to specify a path for your .env file use the second parameter of ConfigModule.load.

Usage

Now we are ready to inject our ConfigService everywhere we'd like.

import {ConfigService} from 'nestjs-config'


@Injectable()
class SomeService {

    constructor(private readonly config: ConfigService) {
        this.config = config;
    }
    
    isProduction() {
        
        const env = this.config.get('app.environment');
        
        return env === 'production';
    }
}

You can also use the @InjectConfig decorator instead, as following:

import {InjectConfig} from 'nestjs-config';


@Injectable()
class SomeService {

    constructor(@InjectConfig() private readonly config) {
        this.config = config;
    }
}

Customer Helpers

This feature allows you to create small helper function that computes values from configurations.

example isProduction helper:

// src/config/express.ts


export default {

    environment: process.env.EXPRESS_PORT
    port: process.env.EXPRESS_PORT,
    
    // helpers
    isProduction() {
        return this.get('express.environment') === 'production';
    }
}

usage:

this.config.get('express').isProduction();

// or

this.config._isProduction(); // note the underscore prefix.

Global Helpers

You can also attach helpers to the global instance as follow:


this.config.registerHelper('isProduction', () => {
    return this.get('express.environment') === 'production';
});

Then use it:

this.config.isProduction();

ConfigService API

get(param: string | string[], value: any = undefined): any

Get a configuration value via path, you can use dot notation to traverse nested object.

this.config.get('server.port'); // 3000

set(param: string | string[], value: any = null): Config

Set a value at runtime, it creates one if doesn't exists.

this.config.set('server.port', 2000); // {server:{ port: 2000 }}

has(param: string | string[]): boolean

Determine if the given path for a configuration exists and set

this.config.has('server.port'); // true or false

merge(glob: string, options?: DotenvOptions): Promise

You can load other configuration at runtime. Great for package development.

@Module({})
export class PackageModule implements NestModule {

    constructor(@InjectConfig() private readonly config) {}

    async configure(consumer: MiddlewareConsumer) {
        await this.config.merge(path.join(__dirname, '**/*.config.{ts,js}'));
    }
}

registerHelper(name: string, fn: (...args:any[]) => any): ConfigService

Register custom global helper

this.config.registerHelper('isProduction', () => {
    return this.get('express.environment') === 'production';
});

Decorators

It's possible to use decorators instead of injecting the ConfigService. But note that @Configurable() decorator replaces descriptor.value for the method with own function. Regarding to the current nestjs implementation (Issue-1180) this behavior will break all decorators that follow after Configurable() decorator.

For the right behavior @Configurable() decorator MUST be placed at the last place when you use several decorators for one method.

Working example:

import {Injectable, Get} from '@nestjs/common';
import {Configurable, ConfigParam} from 'nestjs-config';

@Injectable()
export default class UserController {
    
    @Get("/")
    @Configurable()
    index(@ConfigParam('my.parameter', 'deafult value') parameter?: string) {
        return { data: parameter };
    }
}

Broken example:

import {Injectable, Get, UseInterceptors} from '@nestjs/common';
import {Configurable, ConfigParam} from 'nestjs-config';
import {TransformInterceptor} from '../interceptors';

@Injectable()
export default class UserController {
    
    @Configurable()
    @Get("/")   // <-- nestjs decorator won't work because it placed after @Configurable()
    @UseInterceptors(TransformInterceptor)// <-- nestjs decorator won't work because it placed after @Configurable()
    index(@ConfigParam('my.parameter', 'deafult value') parameter?: string) {
        return { data: parameter };
    }
}

Broken example 2:

import {Injectable, Get, UseInterceptors} from '@nestjs/common';
import {Configurable, ConfigParam} from 'nestjs-config';
import {TransformInterceptor} from '../interceptors';

@Injectable()
export default class UserController {
    
    
    @Get("/") // <-- nestjs decorator will work fine because it placed after @Configurable()
    @Configurable()
    @UseInterceptors(TransformInterceptor) // <-- nestjs decorator won't work because it placed after @Configurable()
    index(@ConfigParam('my.parameter', 'deafult value') parameter?: string) {
        return { data: parameter };
    }
}

Typeorm

Usage with typeorm requires the use of the forRootAsync function supplied by the typeorm package for nestjs

import {Module} from '@nestjs/common';
import {ConfigModule, ConfigService} from 'nestjs-config';
import {TypeOrmModule} from '@nestjs/typeorm';
import * as path from 'path';

@Module({
    imports: [
        ConfigModule.load(path.resolve(__dirname, 'config/**/*.{ts,js}')),
        TypeOrmModule.forRootAsync({
            useFactory: (config: ConfigService) => config.get('database'),
            inject: [ConfigService],
        }),
    ],
})
export default class AppModule {}

And your config file:

//config/database.ts
export default {
    type: 'mysql',
    host: process.env.DB_HOST,
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    port: parseInt(process.env.DB_PORT),
};

Built from Fenos, Shekohex and Bashleigh