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}`);
}
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);
return defineConfig({
...config,
entities: ["dist/**/*.entity.js"],
driver: MySqlDriver,
});
})();
Tip
The options
of ConfigModule.preload(options)
is the options
of ConfigModule.register(options)
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 {}