Config
Contract
@modularityjs/config provides a pluggable, priority-based configuration pipeline with schema-driven defaults and boot-time validation.
abstract class ConfigSource {
abstract readonly name: string;
abstract readonly priority: number;
abstract get(path: string): unknown | undefined;
}
class ConfigService {
get<T>(path: string): T | undefined;
getOrThrow<T>(path: string): T;
resolve(path: string): ResolvedConfigValue | undefined;
}Sources are registered in ConfigSourcePool and sorted by priority (descending). The first source returning a non-undefined value wins.
Built-in Sources
| Source | Priority | Package | Description |
|---|---|---|---|
DefaultsSource | 0 | @modularityjs/config | Reads defaults from ConfigSchemaPool entries |
EnvConfigSource | 1000 | @modularityjs/config-env | Reads environment variables |
Reading Values
@Injectable()
class MyService {
constructor(@Inject(ConfigService) private readonly config: ConfigService) {}
getPort(): number {
return this.config.get<number>('app/port') ?? 3000;
}
getSecret(): string {
return this.config.getOrThrow<string>('app/secret'); // throws if missing
}
}Schema Defaults
Register defaults and validation via 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',
},
},
{
pool: ConfigSchemaPool,
key: 'app/secret',
useValue: {
path: 'app/secret',
type: 'string',
required: true, // boot fails if no source provides this
},
},
],
})
class MyModule {}Schema types: 'string', 'number', 'boolean', 'json'.
At boot, the config system validates all schema entries — type mismatches and missing required values are reported together.
Environment Variables (@modularityjs/config-env)
const modules = [ConfigModule, ConfigEnvModule];Naming convention — paths map to env vars:
| 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/arrays are parsed. Any other string is returned as-is.
Custom Sources
Extend ConfigSource to add your own configuration backends:
@Injectable()
class DatabaseConfigSource extends ConfigSource {
readonly name = 'database';
readonly priority = 500; // between defaults (0) and env (1000)
constructor(
@Inject(DatabaseConnection) private readonly db: DatabaseConnection,
) {}
get(path: string): unknown | undefined {
return this.cache.get(path);
}
}Register in ConfigSourcePool:
@Module({
name: 'db-config',
imports: [ConfigModule, DatabaseModule],
providers: [DatabaseConfigSource],
pools: [
{ pool: ConfigSourcePool, key: 'database', useClass: DatabaseConfigSource },
],
})
class DbConfigModule {}Scope-Aware Resolution
See the Scope guide and Configuration guide for details on ConfigScopeModule and ConfigEnvScopeModule.
Type Coercion
ConfigService.resolve() automatically coerces values based on the schema type registered in ConfigSchemaPool. This is useful when a source returns a string (e.g., environment variables) but the schema declares a different type.
| Schema Type | Coercion Behavior |
|---|---|
'string' | Non-strings are converted via String(value) |
'number' | String values are parsed with Number() — must be finite and non-empty |
'boolean' | 'true' and '1' become true, 'false' and '0' become false |
'json' | String values are parsed with JSON.parse() — objects/arrays pass through as-is |
Values that already match the target type pass through unchanged. If coercion fails (e.g., 'abc' for a 'number' schema), resolve() throws ValidationException with the offending path and the source name.
// Schema declares 'number' type
{ path: 'app/port', type: 'number', default: 3000 }
// EnvConfigSource returns string '8080'
// resolve() coerces it to number 8080
const resolved = config.resolve('app/port');
// { value: 8080, source: 'env' }Resolution Pipeline
When config.get('path') is called:
- Sources are sorted by priority (descending)
- Each source is queried with
source.get(path) - The first non-undefined value is returned
- If a schema entry exists for the path, the value is coerced to the declared type
- If no source has a value,
undefinedis returned
With ConfigScopeModule, the resolution also walks the scope chain per source before falling back to unscoped resolution. Note that ScopedConfigService.resolve() returns the raw value from the source without invoking schema coercion, so a scoped resolve can diverge from ConfigService.resolve() — callers that rely on coerced types (e.g. number from an env string) should coerce explicitly when consuming scoped results.