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:
@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:
| Source | Priority | Package |
|---|---|---|
EnvConfigSource | 1000 | @modularityjs/config-env |
DefaultsSource | 0 | @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():
@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:
const secret = this.config.getOrThrow<string>('app/secret');Schema Defaults
Register defaults via the ConfigSchemaPool:
@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 Path | Environment Variable |
|---|---|
app/port | MODULARITYJS__APP__PORT |
cache/ttl | MODULARITYJS__CACHE__TTL |
app/debug-mode | MODULARITYJS__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: truemust have a value from some source
{
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:
- Get all config sources, sorted by priority (descending)
- For each source, call
source.get(path) - Return the first non-undefined value
- 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:
- Gets the current scope chain from
ScopeService.getCurrentScope() - For each source (by priority), for each scope level in the chain:
- Runs
source.get(path)inside that scope's context viaScopeService.runInChain() - Returns the first match (with
scopeLevelmetadata)
- Runs
- 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:
| Scope | Config Path | Environment Variable |
|---|---|---|
{ level: 'tenant', id: 'acme' } | db/host | MODULARITYJS__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
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
],
});