Nestjs Config
Configuration component for NestJs.
Features
- Load your configuration files using globs
- Support for different environment configurations, 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 src/config
in our project that contains several configuration files.
/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';
import * as path from 'path';
@Module({
imports: [
ConfigModule.load(path.resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
],
})
export class AppModule {}
That's it!
Complex Project Structure
Now let's say that your application isn't located in a folder called src
, but it's located in src/app
.
We want to be able to set a different 'root path' to load our configurations from. Be it src
or dist
.
Imagine a more complex project structure like this:
/
├── 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
In this example, config files are located in the /src/config
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, the ConfigModule
is imported in the BootstrapModule
, but not directly in AppModule
.
import { Module } from '@nestjs/common';
import { BootstrapModule } from './bootstrap';
import { ConfigService } from 'nestjs-config';
ConfigService.rootPath = path.resolve(__dirname, '..');
@Module({
imports: [BootstrapModule],
})
export class AppModule {}
import * as path from 'path';
import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';
@Module({
imports: [
ConfigModule.load(path.resolve('config', '**/!(*.d).{ts,js}')),
],
})
export class BootstrapModule {}
Setting the ConfigService.rootPath
before calling ConfigModule.load(...)
will change the default root dir of where your configs are loaded from.
Another method is to invoke ConfigModule.resolveRootPath(__dirname)
from any module before loading the config and use glob with a relative path.
import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';
@Module({
imports: [
ConfigModule.resolveRootPath(__dirname).load('config/**/!(*.d).{ts,js}')
],
})
export class BootstrapModule {}
In both cases we provide the glob of our configuration as first argument, but it is relative to the src/
folder (or eventually dist/
).
Multi-modular config usage
In some cases your structure might take on this shape
/
├── src/
│ ├── cats/
│ │ ├── cats.module.ts
│ │ └── cats.config.ts
│ ├── dogs/
│ │ ├── dogs.module.ts
│ │ └── dogs.config.ts
│ ├── app.module.ts
│ └── main.ts
├── tsconfig.json
└── package.json
With the examples above you'd have to call your config like so ConfigService.get('dogs.config.bark')
. You can use the modifyConfigName
method option to change the name of your configs
import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';
import * as path from 'path';
@Module({
imports: [
ConfigModule.load(path.resolve(__dirname, '**/!(*.d).config.{ts,js}'), {
modifyConfigName: name => name.replace('.config', ''),
}),
],
})
export class AppModule {}
Now you can call your config like so ConfigService.get('dogs.bark')
.
Production environments
You might have notice the use of config/**/!(*.d).{ts,js}
in the glob. When running in production (running in JavaScript after TypeScript compilation) we want to disinclude the TypeScript definition files. The use of config/**/*.ts
is fine in dev environments but we recommend using this example config/**/!(*.d).{ts,js}
to avoid issues later on when running in a production environment.
Environment Configuration
This package ships with the amazing dotenv package that allows you to create
a .env
file in your preferred location.
Let's create one, for demo purposes!
EXPRESS_PORT=3000
Now, in our src/config/express.ts
configuration file, we can refer to that environment variable.
export default {
port: process.env.EXPRESS_PORT || 3000,
}
Note: By default the package look for a .env
file in the path that you have started your server from.
If you want to specify another path for your .env
file, use the second parameter of ConfigModule.load()
.
Usage
Now we are ready to inject our ConfigService
anywhere 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 may also use the @InjectConfig
decorator as following:
import {InjectConfig} from 'nestjs-config';
@Injectable()
class SomeService {
constructor(@InjectConfig() private readonly config) {
this.config = config;
}
}
Custom Helpers
This feature allows you to create small helper function that computes values from your configurations.
Reconsider the isProduction()
method from above. But in this case, let's define it as a helper:
export default {
environment: process.env.EXPRESS_ENVIRONMENT,
port: process.env.EXPRESS_PORT,
isProduction() {
return this.get('express.environment') === 'production';
}
}
You can use the helper function as follows:
this.config.get('express').isProduction();
this.config._isProduction();
Global Helpers
You can also attach helpers to the global instance as follow:
this.config.registerHelper('isProduction', () => {
return this.get('express.environment') === 'production';
});
And then use it like this:
this.config.isProduction();
Decorators
It's possible to use decorators instead of injecting the ConfigService
.
Note that the @Configurable()
decorator replaces the descriptor.value
for the
method with its own function. Regarding to the current nestjs implementation
(Issue-1180), this behavior will
break all decorators that FOLLOW AFTER the @Configurable()
decorator.
For the expected behavior, the @Configurable()
decorator MUST be placed at
the last position 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', 'default 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('/')
@UseInterceptors(TransformInterceptor)
index(@ConfigParam('my.parameter', 'default 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('/')
@Configurable()
@UseInterceptors(TransformInterceptor)
index(@ConfigParam('my.parameter', 'default value') parameter?: string) {
return { data: parameter };
}
}
TypeORM
Using the ConfigModule
in combination with TypeORM (e.g. in order to configure TypeORM) requires using the forRootAsync()
function supplied by the typeorm package for nestjs (@nestjs/typeorm
)
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', '**', '!(*.d).{ts,js}')),
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService],
}),
],
})
export default class AppModule {}
Your config file may look something like this:
export default {
type: 'mysql',
host: process.env.TYPEORM_HOST,
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
port: parseInt(process.env.TYPEORM_PORT),
logging: process.env.TYPEORM_LOGGING === 'true',
entities: process.env.TYPEORM_ENTITIES.split(','),
migrationsRun: process.env.TYPEORM_MIGRATIONS_RUN === 'true',
synchronize: process.env.TYPEORM_SYNCHRONIZE === 'true',
};
We recommend using a TYPEORM_
prefix so when running in production environments you're also able to use the same envs for running the typeorm cli. More options here
Custom env file path
You can specify dotenv options with the second parameter of the load method
ConfigModule.load(path.resolve(__dirname, '*/**!(*.d).config.{ts,js}'), {
path: path.resolve(__dirname, '..', '.env.staging')),
});
Using different env files
Create a .env
file at the root directory of your application had always been the best practice.
You could also create custom .env file per different environments.
EXPRESS_PORT=3000
EXPRESS_PORT=3001
EXPRESS_PORT=3002
EXPRESS_PORT=3003
const ENV = process.env.NODE_ENV;
ConfigModule.load(path.resolve(__dirname, '*/**!(*.d).config.{ts,js}'), {
path: path.resolve(process.cwd(), !ENV ? '.env' : `.env.${ENV}`),
});
Perhaps you need a custom directory to manage env files.
In this example, the name of the custom directory is env
.
/
├── dist/
├── env/
│ ├── .env.dev
│ ├── .env.testing
│ └── .env.staging
│ │
├── src/
│ ├── config/
│ │ └── typeorm.config.ts
│ └── main.ts
├── tsconfig.json
└── package.json
const ENV = process.env.NODE_ENV;
ConfigModule.load(path.resolve(__dirname, '*/**!(*.d).config.{ts,js}'), {
path: path.resolve(process.cwd(), 'env', !ENV ? '.env' : `.env.${ENV}`),
});
Note: If you place env files inside an src
directory, you won't be able to see env files included as a final output in outDir
since TS compiler will never transpile files that do not match with *.ts extension.
Support
Any support is welcome. At least you can give us a star :star:
Contributors
Code Contributors
This project exists thanks to all the people who contribute. [Contribute].
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. It returns a default value if the key does not exist.
this.config.get('server.port');
this.config.get('an.undefined.value', 'foobar');
set(param: string | string[], value: any = null): Config
Set a value at runtime, it creates the specified key / value if it doesn't already exists.
this.config.set('server.port', 2000);
has(param: string | string[]): boolean
Determine if the given path for a configuration exists and is set.
this.config.has('server.port');
merge(glob: string, options?: DotenvOptions): Promise
Load other configuration files at runtime. This is great for package development.
@Module({})
export class PackageModule implements NestModule {
constructor(@InjectConfig() private readonly config) {}
async configure(consumer: MiddlewareConsumer) {
await this.config.merge(path.resolve(__dirname, '**/!(*.d).{ts,js}'));
}
}
registerHelper(name: string, fn: (...args:any[]) => any): ConfigService
Register a custom global helper function
this.config.registerHelper('isProduction', () => {
return this.get('express.environment') === 'production';
});
resolveRootPath(path: string): typeof ConfigService
change the root path from where configs files are loaded
import { Module } from '@nestjs/common';
import { ConfigModule } from 'nestjs-config';
@Module({
imports: [
ConfigModule.resolveRootPath(__dirname).load(path.resolve(__dirname, '**/!(*.d).{ts,js}')),
],
})
export class AppModule {}
root(path: string = ''): string
Returns the current working dir or defined rootPath.
ConfigService.root();
ConfigService.root('some/path/file.html');
ConfigService.resolveRootPath(__dirname).root();