An easy-to-use NestJS config module with powerful features and type safety.
- ✅ Type-Safe Configuration: Config verification by
class-validator - ✅ Config Transformation: Config transform by
class-transformer - ✅ Flexible Loading: Load configuration from multiple sources (env, files, etc.)
- ✅ Hot Reload: Automatically reload configuration when files change (opt-in)
- ✅ Auto Naming Convention: Automatically handles naming styles (camelCase, snake_case, etc.)
- ✅ Injectable Classes: Configuration classes can be injected like any other provider
- ✅ Global Configuration: Define configuration once, use everywhere
- ✅ Perfect Type Hints: Full TypeScript support with excellent IDE autocomplete
npm install @buka/nestjs-config
# or
yarn add @buka/nestjs-config
# or
pnpm add @buka/nestjs-configDefine your configuration using decorators from class-validator:
// 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 {
@IsIp()
host = "0.0.0.0"; // default value
@IsString()
@IsOptional()
cacheDir?: string;
@IsIn(["dev", "test", "prod"])
nodeEnv: string;
@Split(",")
brokers: string[];
}Create a .env file in your project root:
# .env
CACHE_DIR="./tmp"
NODE_ENV="dev"
BROKERS="test01.test.com,test02.test.com,test03.test.com"Tip
@buka/nestjs-config automatically converts naming styles. cache_dir, CACHE_DIR, cacheDir, CacheDir, cache-dir, Cache_Dir are all recognized as the same config name.
Import ConfigModule in your NestJS application:
// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@buka/nestjs-config";
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
],
})
export class AppModule {}By default, ConfigModule loads config from process.env and .env file.
Use configuration in your services:
// app.service.ts
import { Injectable } from "@nestjs/common";
import { AppConfig } from "./app.config";
@Injectable()
export class AppService {
constructor(private readonly appConfig: AppConfig) {}
getInfo() {
return `Host: ${this.appConfig.host}, Env: ${this.appConfig.nodeEnv}`;
}
}When you need to use configuration in multiple places (NestJS modules, ORM configs, scripts), use ConfigModule.configure() to avoid duplication:
// config/index.ts
import { ConfigModule, processEnvLoader, yamlFileLoader } from "@buka/nestjs-config";
ConfigModule.configure({
loaders: [
processEnvLoader(),
yamlFileLoader("config.yaml"),
],
suppressWarnings: true,
});// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@buka/nestjs-config";
import "./config"; // Import configuration
@Module({
imports: [
ConfigModule.register({ isGlobal: true }), // Uses global config
],
})
export class AppModule {}// mikro-orm.config.ts
import { ConfigModule } from "@buka/nestjs-config";
import { DatabaseConfig } from "./config/database.config";
import "./config"; // Import configuration
export default (async function () {
await ConfigModule.preload(); // Uses global config
const config = await ConfigModule.getOrFail(DatabaseConfig);
return { ...config };
})();Tip
Benefits of Global Configuration:
- Configure once, use everywhere
- No duplication between
preload()andregister() - Guaranteed consistency across your application
- Can still override settings when needed by passing options directly
interface ConfigModuleOptions {
/**
* Configuration loaders (default: processEnvLoader + .env file)
*/
loaders?: (string | ConfigLoader)[];
/**
* Suppress warning messages
*/
suppressWarnings?: boolean;
/**
* Enable debug logging
*/
debug?: boolean;
/**
* Manually specify config providers (usually auto-detected)
*/
providers?: Type[];
}You can customize how configuration is loaded:
import { ConfigModule, processEnvLoader, dotenvLoader } from "@buka/nestjs-config";
ConfigModule.configure({
loaders: [
processEnvLoader(),
dotenvLoader(".env", { separator: "__", jsonParse: true }),
dotenvLoader(`.env.${process.env.NODE_ENV}`),
],
});import { Configuration } from "@buka/nestjs-config";
import { ValidateNested, Type } from "class-transformer";
import { IsString } from "class-validator";
export class DatabaseConfig {
@IsString()
host: string; // DATABASE__HOST
@IsString()
password: string; // DATABASE__PASSWORD
}
@Configuration()
export class AppConfig {
@ValidateNested()
@Type(() => DatabaseConfig)
database: DatabaseConfig;
}Environment variables: DATABASE__HOST=localhost, DATABASE__PASSWORD=secret
Use @Configuration(prefix) to add a prefix to all properties:
import { Configuration } from "@buka/nestjs-config";
import { IsString } from "class-validator";
@Configuration("mysql.master")
export class MysqlConfig {
@IsString()
host: string; // MYSQL__MASTER__HOST
}Mapping:
process.env:MYSQL__MASTER__HOST.envfile:MYSQL__MASTER__HOST- JSON file:
{ mysql: { master: { host: "..." } } }
Use @ConfigKey() to override the property name:
import { Configuration, ConfigKey } from "@buka/nestjs-config";
import { IsString } from "class-validator";
@Configuration("mysql")
export class MysqlConfig {
@ConfigKey("DATABASE_HOST")
@IsString()
host: string; // Now reads from DATABASE_HOST instead of MYSQL__HOST
}Note
@ConfigKey(name) overwrites the prefix from @Configuration(prefix)
Use ConfigModule.preload() to load configuration before NestJS initialization:
// database-migration.ts
import { ConfigModule } from "@buka/nestjs-config";
import { DatabaseConfig } from "./config/database.config";
import "./config"; // Import global configuration
async function migrate() {
await ConfigModule.preload(); // Uses global config
// Get configuration
const config = await ConfigModule.getOrFail(DatabaseConfig);
// Use config for migration...
}Simplify configuration injection into other modules using ConfigModule.inject():
import { Module } from "@nestjs/common";
import { ConfigModule, Configuration } from "@buka/nestjs-config";
import { LoggerModule } from "nestjs-pino";
import { IsIn } from "class-validator";
@Configuration("logger")
class LoggerConfig {
@IsIn(["fatal", "error", "warn", "info", "debug", "trace"])
level: string = "info";
}
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
// Using ConfigModule.inject() - Simple and clean
ConfigModule.inject(LoggerConfig, LoggerModule, (config) => ({
pinoHttp: { level: config.level },
})),
],
})
class AppModule {}The module supports hot-reloading of configuration files, allowing your application to automatically pick up configuration changes without restarting.
Enable hot reload per loader using the hotReload option:
import { ConfigModule, dotenvLoader, jsonFileLoader, yamlFileLoader } from "@buka/nestjs-config";
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
loaders: [
// Enable hot reload with default settings (file watcher)
dotenvLoader(".env", { hotReload: true }),
// Enable hot reload for JSON file
jsonFileLoader("./config.json", 'utf-8', { hotReload: true }),
// Enable hot reload for YAML file
yamlFileLoader("./config.yaml", 'utf-8', { hotReload: true }),
],
}),
],
})
export class AppModule {}You can customize the watch behavior for each loader:
ConfigModule.register({
loaders: [
// File system watch (default, recommended)
dotenvLoader(".env", {
hotReload: {
type: 'watch', // Use chokidar file system watcher
debounceMs: 500, // Wait 500ms before reloading (default: 300ms)
onChange: async (newConfig) => {
console.log("Config file changed, new config:", newConfig);
},
onError: async (error) => {
console.error("Failed to reload config:", error);
},
},
}),
// Polling mode (for network drives or special filesystems)
jsonFileLoader("./config.json", 'utf-8', {
hotReload: {
type: 'interval', // Use polling instead of file watching
intervalMs: 5000, // Check every 5 seconds (default: 5000ms)
debounceMs: 300,
},
}),
],
});You can also manually trigger configuration reload:
import { ConfigModule } from "@buka/nestjs-config";
// In your service or controller
async reloadConfig() {
try {
await ConfigModule.reload();
return { message: "Configuration reloaded successfully" };
} catch (error) {
throw new Error(`Failed to reload config: ${error.message}`);
}
}This is useful when:
- Configuration is updated programmatically (not via file changes)
- You need to reload configuration on demand (e.g., admin endpoint)
- Testing configuration changes
- Each loader can independently enable hot reload with its own settings
- When a file changes, the loader triggers a global configuration reload
- All configuration provider instances are updated in-place
- Injected configuration objects automatically reflect the new values
- If validation fails, the old configuration is preserved (rollback)
- Hot reload is configured per loader (opt-in for each file)
- Supports both
watchmode (file system events) andintervalmode (polling) - Environment variable loader (
processEnvLoader) does not support watching - Each loader has independent debouncing (default: 300ms)
- Configuration validation still applies on reload
- Manual reload via
ConfigModule.reload()is also available
Equivalent without ConfigModule.inject():
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
// Using forRootAsync directly - More verbose
LoggerModule.forRootAsync({
inject: [LoggerConfig],
useFactory: (config: LoggerConfig) => ({
pinoHttp: { level: config.level },
}),
}),
],
})
class AppModule {}As you can see, ConfigModule.inject() provides a cleaner syntax for injecting configuration into other modules.
Advanced: Inject with additional options
import { TypeOrmModule } from "@nestjs/typeorm";
@Configuration("database")
class DatabaseConfig {
@IsString()
host: string;
@IsNumber()
port: number;
@IsString()
database: string;
}
@Module({
imports: [
ConfigModule.register({ isGlobal: true }),
// With additional module options (e.g., multiple databases)
ConfigModule.inject(
DatabaseConfig,
TypeOrmModule,
{ name: "primary" }, // Additional options
(config) => config // Config mapper (optional)
),
],
})
class AppModule {}Equivalent to:
TypeOrmModule.forRootAsync({
name: "primary",
inject: [DatabaseConfig],
useFactory: (config: DatabaseConfig) => config,
})| Loader | Description |
|---|---|
processEnvLoader() |
Load from process.env |
dotenvLoader(path, options?) |
Load .env file using dotenv |
dotenvxLoader(path, options?) |
Load .env file using @dotenvx/dotenvx |
jsonFileLoader(path) |
Load JSON file |
yamlFileLoader(path, encoding?) |
Load YAML file using yaml |
tomlFileLoader(path, encoding?) |
Load TOML file using smol-toml |
import { ConfigLoader, ConfigModuleOptions } from "@buka/nestjs-config";
import { readFileSync } from "fs";
export function customLoader(filepath: string): ConfigLoader {
return (options: ConfigModuleOptions) => {
const content = readFileSync(filepath, "utf-8");
// Parse and return configuration object
return JSON.parse(content);
};
}This may occur when using TypeScript with target set to ES2021 or lower.
Solution 1 (Recommended): Upgrade to ES2022 or higher in tsconfig.json
Solution 2: Add @ConfigKey() decorator to every property:
import { Configuration, ConfigKey } from "@buka/nestjs-config";
@Configuration()
export class AppConfig {
@ConfigKey()
@IsIp()
host = "0.0.0.0";
@ConfigKey()
@IsIn(["dev", "test", "prod"])
nodeEnv: string;
}This occurs when a config class is not injected into any service.
Solution 1: Use ConfigModule.get() instead of app.get():
await ConfigModule.preload();
const config = await ConfigModule.get(YourConfig);Solution 2: Manually register the config class:
@Module({
imports: [
ConfigModule.register({
isGlobal: true,
providers: [YourConfig],
}),
],
})
class AppModule {}MIT