Skip to content

buka-inc/npm.nestjs-config

Repository files navigation

@buka/nestjs-config

version downloads dependencies license Codecov

An easy-to-use NestJS config module with powerful features and type safety.

Features

  • 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

Installation

npm install @buka/nestjs-config
# or
yarn add @buka/nestjs-config
# or
pnpm add @buka/nestjs-config

Quick Start

1. Create Configuration Class

Define 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[];
}

2. Create Environment File

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.

3. Import ConfigModule

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.

4. Inject Configuration

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}`;
  }
}

Configuration

Global Configuration (Recommended)

When you need to use configuration in multiple places (NestJS modules, ORM configs, scripts), use ConfigModule.configure() to avoid duplication:

Step 1: Create Configuration File

// config/index.ts
import { ConfigModule, processEnvLoader, yamlFileLoader } from "@buka/nestjs-config";

ConfigModule.configure({
  loaders: [
    processEnvLoader(),
    yamlFileLoader("config.yaml"),
  ],
  suppressWarnings: true,
});

Step 2: Use in NestJS Module

// 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 {}

Step 3: Use in External Files (ORM configs, scripts, etc.)

// 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() and register()
  • Guaranteed consistency across your application
  • Can still override settings when needed by passing options directly

Configuration Options

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[];
}

Custom Loaders

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}`),
  ],
});

Advanced Usage

Nested Configuration

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

Configuration Prefix

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
  • .env file: MYSQL__MASTER__HOST
  • JSON file: { mysql: { master: { host: "..." } } }

Custom Config Keys

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)

Preload Config (Outside NestJS)

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...
}

Module Integration

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 {}

Configuration Hot Reload

The module supports hot-reloading of configuration files, allowing your application to automatically pick up configuration changes without restarting.

Basic Usage

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 {}

Advanced Usage - Watch Types and Hooks

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,
      },
    }),
  ],
});

Manual Reload

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

How It Works

  • 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)

Notes

  • Hot reload is configured per loader (opt-in for each file)
  • Supports both watch mode (file system events) and interval mode (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,
})

Built-in Loaders

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

Custom Loader Example

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);
  };
}

Troubleshooting

Error: "Every field in my Config class was missing"

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;
}

Error: "Nest could not find YourConfig element"

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 {}

License

MIT

About

An easy to use nestjs config module

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages