Skip to content

Configuration

Overview

The config system provides a pluggable, priority-based configuration pipeline with schema-driven defaults and boot-time validation. Scope-aware resolution is available via optional extension packages.

Config Sources

Configuration values come from sources — classes that extend ConfigSource and are registered in the ConfigSourcePool:

typescript
@Injectable()
export abstract class ConfigSource {
  abstract readonly name: string;
  abstract readonly priority: number;
  abstract get(path: string): unknown | undefined;
}

Higher priority sources are consulted first. The built-in sources are:

SourcePriorityPackage
EnvConfigSource1000@modularityjs/config-env
DefaultsSource0@modularityjs/config (built-in)

You can add custom sources at any priority level by extending ConfigSource and registering in ConfigSourcePool.

Reading Config Values

Inject ConfigService and call get():

typescript
@Injectable()
class MyService {
  constructor(@Inject(ConfigService) private readonly config: ConfigService) {}

  getPort(): number {
    return this.config.get<number>('app/port') ?? 3000;
  }
}

getOrThrow() throws if no value is found:

typescript
const secret = this.config.getOrThrow<string>('app/secret');

Schema Defaults

Register defaults via the ConfigSchemaPool:

typescript
@Module({
  name: 'my-module',
  imports: [ConfigModule],
  pools: [
    {
      pool: ConfigSchemaPool,
      key: 'app/port',
      useValue: {
        path: 'app/port',
        type: 'number',
        default: 3000,
        description: 'HTTP server port',
      },
    },
  ],
})
class MyModule {}

The DefaultsSource (priority 0) reads these entries and serves them as the lowest-priority fallback.

Environment Variables

With ConfigEnvModule, environment variables override all other sources. The naming convention:

Config PathEnvironment Variable
app/portMODULARITYJS__APP__PORT
cache/ttlMODULARITYJS__CACHE__TTL
app/debug-modeMODULARITYJS__APP__DEBUG_MODE

Slashes become __, hyphens become _, everything is uppercased, prefixed with MODULARITYJS__.

Values are auto-parsed: true/false become booleans, null becomes null, numeric strings become numbers, JSON objects are parsed.

Boot-Time Validation

At boot, the config system validates all registered schema entries:

  • Type checking: Resolved values must match their declared type (string, number, boolean, json)
  • Required enforcement: Entries with required: true must have a value from some source
typescript
{
  pool: ConfigSchemaPool,
  key: 'app/secret',
  useValue: {
    path: 'app/secret',
    type: 'string',
    required: true,  // boot fails if no source provides this
  },
}

All validation failures are reported together so you can fix everything in one pass.

Resolution Pipeline

When config.get('path') is called:

  1. Get all config sources, sorted by priority (descending)
  2. For each source, call source.get(path)
  3. Return the first non-undefined value
  4. If no source has a value, return undefined

Scope-Aware Resolution

Scope-aware configuration is provided by two optional extension packages that layer on top of the core config system using the preference pattern.

ConfigScopeModule (@modularityjs/config-scope)

Imports ConfigModule and ScopeModule. Registers ScopedConfigService as a preference over ConfigService, making all config resolution scope-aware automatically.

When resolving a value, ScopedConfigService:

  1. Gets the current scope chain from ScopeService.getCurrentScope()
  2. For each source (by priority), for each scope level in the chain:
    • Runs source.get(path) inside that scope's context via ScopeService.runInChain()
    • Returns the first match (with scopeLevel metadata)
  3. Falls back to default (unscoped) resolution

This means any ConfigSource can be scope-aware by reading the scope context from ScopeService inside its get() method.

ConfigEnvScopeModule (@modularityjs/config-env-scope)

Imports ConfigEnvModule and ScopeModule. Registers ScopedEnvConfigSource as a preference over EnvConfigSource.

When a scope is active, it looks up scope-specific environment variables:

ScopeConfig PathEnvironment Variable
{ level: 'tenant', id: 'acme' }db/hostMODULARITYJS__SCOPES__TENANT__ACME__DB__HOST

If no scoped variable is found and a scope is active, the source returns undefined (no fallback to the global env var at this source level — the global EnvConfigSource is a separate source in the pool). Without an active scope, it falls back to global env var resolution.

App Setup

typescript
const app = await createApp({
  di: inversify,
  modules: [
    ConfigModule, // core config
    ConfigEnvModule, // env var source
    ScopeModule, // scope system
    ConfigScopeModule, // scope-aware resolution
    ConfigEnvScopeModule, // scope-aware env vars
  ],
});