Nest-Typed-Config
Never write strings to read config again.
Features
- Provide a type-safe and intuitive way to config your Nest projects, just as smooth as request DTO.
- Load your configuration with environment variables, json/yaml/toml configuration files or remote endpoints.
- Validate your configuration with class-validator and class-transformer.
- Provide easy to use options by default, meanwhile everything is customizable.
Installation
$ yarn add nest-typed-config
$ npm i --save nest-typed-config
Inspiration
There are various popular configuration modules for Nest framework, such as the official configuration module, nestjs-config and nestjs-easyconfig. These modules can help to manage configurations, validate them, and load them through the ConfigService
. But that's when type-safety is gone. For example:
const dbUser = this.configService.get<string>('DATABASE_USER');
const env = this.config.get('app.environment');
const value = this.config.get('key');
Writing type casting is a pain and hard to maintain, and it's common to use non-string configurations in real-world projects. This module aims to provide an intuitive and type-safe way to load, validate and use configurations. Just import any config model, and inject it with full TypeScript support. In a nutshell:
export class Config {
@IsString()
public readonly host!: string;
@IsNumber()
public readonly port!: number;
}
import { Config } from './config';
@Injectable()
export class AppService {
constructor(private readonly config: Config) {}
show() {
console.log(`http://${this.config.host}:${this.config.port}`)
}
}
Quick Start
Let's define the configuration model first. It can be nested at arbitrary depth.
import { Allow } from 'class-validator';
export class TableConfig {
@Allow()
public readonly name!: string;
}
export class DatabaseConfig {
@Type(() => TableConfig)
@Allow()
public readonly table!: TableConfig;
}
export class RootConfig {
@Type(() => DatabaseConfig)
@Allow()
public readonly database!: DatabaseConfig;
}
Then, add a configuration file such as .env.yaml
under project root directory:
database:
table:
name: example
After creating the configuration file, import TypedConfigModule
and fileLoader
to load configuration from file system.
import { Module } from '@nestjs/common';
import { TypedConfigModule, fileLoader } from 'nest-typed-config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RootConfig } from './config';
@Module({
imports: [
TypedConfigModule.forRoot({
schema: RootConfig,
load: fileLoader(),
}),
],
providers: [AppService],
controllers: [AppController],
})
export class AppModule {}
That's it! You can use any config or sub-config you defined as injectable services now!
import { Injectable } from '@nestjs/common';
import { RootConfig, DatabaseConfig, TableConfig } from './config';
@Injectable()
export class AppService {
constructor(
private config: RootConfig,
private databaseConfig: DatabaseConfig,
private tableConfig: TableConfig,
) {}
public show(): any {
const out = [
`root.name: ${this.config.name}`,
`root.database.name: ${this.databaseConfig.name}`,
`root.database.table.name: ${this.tableConfig.name}`,
].join('\n');
return `${out}\n`;
}
}
For a full example, please visit our examples folder
Using loaders
Using dotenv loader
The dotenvLoader
function allows you to load configuration with dotenv, which is similar to the official configuration module. You can use this loader to load configuration from .env
files or environment variables.
Example
TypedConfigModule.forRoot({
schema: RootConfig,
load: dotenvLoader({ }),
})
Passing options
The dotenvLoader
function optionally expects a DotenvLoaderOptions
object as a first parameter:
export interface DotenvLoaderOptions {
separator?: string;
ignoreEnvFile?: boolean;
ignoreEnvVars?: boolean;
envFilePath?: string | string[];
expandVariables?: boolean;
}
Using file loader
The fileLoader
function allows you to load configuration with cosmiconfig. You can use this loader to load configuration from files with various extensions, such as .json
, .yaml
, .toml
or .js
.
By default, fileLoader
searches for .env.{ext}
(ext = json, yaml, toml, js) configuration file starting at process.cwd()
, and continues to search up the directory tree until it finds some acceptable configuration (or hits the home directory). Moreover, configuration of current environment takes precedence over general configuration (.env.development.toml
is loaded instead of .env.toml
when NODE_ENV=development
)
Example
TypedConfigModule.forRoot({
schema: RootConfig,
load: fileLoader({ }),
})
Passing options
The fileLoader
function optionally expects a FileLoaderOptions
object as a first parameter:
import { Options } from 'cosmiconfig';
export interface FileLoaderOptions extends Partial<Options> {
basename?: string;
absolutePath?: string;
searchFrom?: string;
}
If you want to add support for other extensions, you can use loaders
property provided by cosmiconfig
:
TypedConfigModule.forRoot({
schema: RootConfig,
load: fileLoader({
loaders: {
'.ini': iniLoader
}
}),
})
Using remote loader
The remoteLoader
function allows you to load configuration from a remote endpoint, such as configuration center. Internally axios is used to perform http requests.
Example
TypedConfigModule.forRoot({
schema: RootConfig,
load: remoteLoader('http://localhost:8080', { }),
})
Passing options
The remoteLoader
function optionally expects a RemoteLoaderOptions
object as a second parameter, which accepts all axios
request configuration except url
.
export interface RemoteLoaderOptions extends AxiosRequestConfigWithoutUrl {
type?: ((response: any) => RemoteLoaderConfigType) | RemoteLoaderConfigType;
mapResponse?: (config: any) => Promise<any> | any;
shouldRetry?: (response: AxiosResponse) => boolean;
retries?: number;
retryInterval?: number;
}
You can use the mapResponse
function to preprocess the server response before parsing with type
, and use shouldRetry
function to determine whether server response is valid or not. When server response is not valid, you can use retries
and retryInterval
to adjust retry strategies. For example:
TypedConfigModule.forRoot({
schema: RootConfig,
load: remoteLoader('http://localhost:8080', {
type: response => response.fileType,
mapResponse: response => response.fileContent
shouldRetry: response => response.data.code !== 0
retries: 3,
retryInterval: 3000
}),
})
Using multiple loaders
Loading configuration from file system is convenient for development, but when it comes to deployment, you may need to load configuration from environment variables, especially in a dockerized environment. This can be easily achieved by providing multiple loaders. For example:
TypedConfigModule.forRoot({
schema: RootConfig,
load: [
fileLoader({ }),
dotenvLoader({ }),
]
})
Using custom loader
If native loaders provided by nest-typed-config
can't meet your needs, you can implement a custom loader. This can be achieved by providing a function which returns the configuration object synchronously or asynchronously through the load
option. For example:
TypedConfigModule.forRoot({
schema: RootConfig,
load: async () => {
return {
host: '127.0.0.1',
port: 3000
}
},
})
Default values
Just define your default values in config schema, and you are ready to go:
export class Config {
@IsString()
public readonly host: string = '127.0.0.1';
@IsNumber()
public readonly port: number = 3000;
}
API
TypedConfigModule.forRoot
export type ConfigLoader = () =>
| Promise<Record<string, any>>
| Record<string, any>;
export interface TypedConfigModuleOptions {
schema: ClassConstructor<any>;
load: ConfigLoader | ConfigLoader[];
isGlobal?: boolean;
normalize?: (config: Record<string, any>) => Record<string, any>;
validate?: (config: Record<string, any>) => Record<string, any>;
validationOptions?: ValidatorOptions;
}
changelog
Please refer to CHANGELOD.md
License
MIT.