This is an easy-to-use nestjs config module with many surprising features.
- Config verification by
class-validator
- Config transform by
class-transformer
- Load configuration files from anywhere
- Perfect coding tips
- Automatically handle naming styles
- Injectable config class
npm install @buka/nestjs-config
yarn install @buka/nestjs-config
pnpm install @buka/nestjs-config
@buka/nestjs-config
load config from process.env
and .env
(local process.cwd()
) by defaulted. let us create .env
first:
# .env
CACHE_DIR="./tmp"
BROKERS="test01.test.com,test02.test.com,test03.test.com"
Then, define a AppConfig
class with the @Configuration()
decorator.
// app.config.ts
import { Configuration } from "@buka/nestjs-config";
import { IsString, IsOptional, IsIn, IsIp } from "class-validator";
import { Split } from "@miaooo/class-transformer-split";
@Configuration()
export class AppConfig {
// set default value
@IsIp()
host = "0.0.0.0";
// CACHE_DIR in .env
@IsString()
@IsOptional()
cacheDir?: string;
// process.env.NODE_ENV
@IsIn(["dev", "test", "prod"])
nodeEnv: string;
@Split(",")
brokers: string[];
}
Tip
@buka/nestjs-config
automatically convert naming styles. For example: cache_dir
、CACHE_DIR
、cacheDir
、CacheDir
、cache-dir
、Cache_Dir
are considered to be the same config name.
Import ConfigModule
in your AppModule
:
// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@buka/nestjs-config";
import { AppConfig } from "./app.config";
@Module({
// use process.env and read .env by defaulted
imports: [ConfigModule.register({ isGlobal: true })],
})
export class AppModule {}
Inject and use AppConfig
in your service:
import { Injectable } from "@nestjs/common";
import { AppConfig } from "./app.config";
@Injectable()
export class AppService {
constructor(private readonly appConfig: AppConfig) {}
}
Nested configuration is the same as using class-validator
and class-transformer
:
import { Configuration } from "@buka/nestjs-config";
import { IsString } from "class-validator";
export class SubConfig {
// process.env.{ParentFieldName}__KEY
@IsString()
key: string;
}
@Configuration()
export class AppConfig {
// process.env.SUB_FIRST__KEY
@ValidateNested()
@Type(() => SmsTemplate)
subFirst!: SubConfig;
// process.env.SUB_SECOND__KEY
@ValidateNested()
@Type(() => SmsTemplate)
subSecond!: SubConfig;
}
import { Module } from "@nestjs/common";
import {
ConfigModule,
processEnvLoader,
dotenvLoader,
} from "@buka/nestjs-config";
import { AppConfig } from "./app.config";
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
loaders: [
processEnvLoader,
// transform DATABASE__HOST="0.0.0.0"
// to DATABASE = { HOST: "0.0.0.0" }
// transform LOG="true"
// to LOG = true
dotenvLoader(".env", { separator: "__", jsonParse: true }),
dotenvLoader(`.${process.env.NODE_ENV}.env`),
],
}),
],
})
export class AppModule {}
// yaml-config-loader.ts
import { ConfigLoader } from "@buka/nestjs-config";
import { parse } from "yaml";
export async function yamlConfigLoader(filepath: string): ConfigLoader {
return (options: ConfigModuleOptions) => {
if (!existsSync(filepath)) {
if (!options.suppressWarnings) {
Logger.warn(`yaml file not found: ${filepath}`, '@buka/nestjs-config');
}
return {};
}
const content = await readFile(filepath);
return parse(content);
};
}
Use yamlConfigLoader
:
import { Module } from "@nestjs/common";
import { ConfigModule } from "@buka/nestjs-config";
import { AppConfig } from "./app.config";
import { yamlConfigLoader } from "./yamlConfigLoader";
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
loaders: [yamlConfigLoader("my-yaml-config.yaml")],
}),
],
})
export class AppModule {}
// mysql.config.ts
import { Configuration } from "@buka/nestjs-config";
import { IsString } from "class-validator";
@Configuration("mysql.master")
export class MysqlConfig {
// process : process.env.MYSQL__MASTER__HOST
// .env : MYSQL__MASTER__HOST
// .json : { mysql: { master: { host: "" } } }
@IsString()
host: string;
}
// app.config.ts
import { Configuration, ConfigKey } from "@buka/nestjs-config";
import { IsString } from "class-validator";
@Configuration("mysql.master")
export class MysqlConfig {
// process : process.env.DATABASE_HOST
// .env : DATABASE_HOST
// .json : { databaseHost: "" }
@ConfigKey("DATABASE_HOST")
@IsString()
host: string;
}
@ConfigKey(name)
will overwrite the prefix of@Configuration([prefix])
import { Module } from "@nestjs/common";
import { ConfigModule } from "@buka/nestjs-config";
import { AppConfig } from "./app.config";
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
suppressWarnings: true,
}),
],
})
export class AppModule {}
Simplify the writing of .forRootAsync
/.registerAsync
.
// pino.config.ts
@Configuration("pino")
export class PinoConfig implements Pick<Params, "assignResponse"> {
@ToBoolean()
@IsBoolean()
assignResponse?: boolean | undefined;
}
// app.module.ts
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
ConfigModule.inject(PinoConfig, LoggerModule),
],
})
class AppModule {}
If the config class implement options of module .forRootAsync
/.registerAsync
,
The code will become very beautiful.
And implement
is not necessary:
import { Module } from "@nestjs/common";
import { ConfigModule } from "@buka/nestjs-config";
// pino.config.ts
@Configuration("pino")
export class PinoConfig {
@IsIn(["fatal", "error", "warn", "info", "debug", "trace"])
level: string = "info";
}
// app.module.ts
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
// map .level to .pinoHttp.level
ConfigModule.inject(PinoConfig, LoggerModule, (config) => ({
pinoHttp: { level: config.level },
})),
],
})
class AppModule {}
Sometimes, a name
property is need by options of .forRootAsync
/.registerAsync
,
like add multiple database in @nestjs/typeorm
.
Another one is
isGlobal
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
ConfigModule.inject(
TypeOrmConfig,
TypeOrmModule,
{ name: "my-orm" },
(config) => config // config mapping function is optional
),
// this is equal to
TypeOrmModule.forRootAsync({
name: "my-orm",
inject: [TypeOrmConfig],
useFactory: (config: TypeOrmConfig) => config,
}),
],
})
export class AppModule {}
Sometimes, we have to get config outside the nestjs lifecycle. ConfigModule.preload(options)
is designed for this.
There is an example of MikroORM config file:
// mikro-orm.config.ts
import { ConfigModule } from "@buka/nestjs-config";
import { MySqlDriver, defineConfig } from "@mikro-orm/mysql";
import { MysqlConfig } from "./config/mysql.config";
import { Migrator } from "@mikro-orm/migrations";
import { BadRequestException } from "@nestjs/common";
export default (async function loadConfig() {
// Load MysqlConfig
await ConfigModule.preload();
// Get MysqlConfig Instance
const config = await ConfigModule.get(MysqlConfig);
if (!config) throw new Error("Config Not Founded");
// or
// const config = await ConfigModule.getOrFail(MysqlConfig);
return defineConfig({
...config,
entities: ["dist/**/*.entity.js"],
driver: MySqlDriver,
});
})();
Tip
The options
of ConfigModule.preload(options)
is the options
of ConfigModule.register(options)
Name | Description |
---|---|
processEnvLoader |
load from process.env |
dotenvLoader |
load .env file by dotenv |
dotenvxLoader |
load .env file by @dotenvx/dotenvx |
jsonFileLoader |
load json file by JSON.parse |
yamlFileLoader |
load yaml file by yaml |
tomlFileLoader |
load toml file by smol-toml |
This may be due to target
in tsconfig.json is ES2021
or lower. We recommend using ES2022
and above.
But, if you must use ES2021
, every property key should add @ConfigKey()
decorator (See More):
// app.config.ts
import { Configuration, ConfigKey } from "@buka/nestjs-config";
import { IsIp, IsIn } from "class-validator";
@Configuration()
export class AppConfig {
@ConfigKey()
@IsIp()
host = "0.0.0.0";
@ConfigKey()
@IsIn(["dev", "test", "prod"])
nodeEnv: string;
}
@buka/nestjs-config
will autoload all the config classes injected by service.
However, a config that is not used by any service may not be injected into the nestjs app.
And this will causes you to get this error when attempt to app.get(YourConfig)
.
One solution is use ConfigModule.get(YourConfig)
replace app.get(YourConfig)
:
await ConfigModule.preload();
const yourConfig = await ConfigModule.get(YourConfig);
// If you do this, you probably want to do something outside of the nestjs runtime.
// do...
If you have to inject config class which is not used by any service, you can do it like this:
@Module({
ConfigModule.register({
isGlobal: true,
providers: [YourConfig]
}),
})
class AppModule {}