Skip to content

Configuration

Overview

The config system provides a pluggable, priority-based configuration pipeline with schema-driven defaults, scope cascade, and boot-time validation.

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, scope?: ScopeLevel): 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, 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:
    • If scopes are active, walk the scope chain from most-specific to least-specific
    • Then try unscoped resolution
    • Return the first non-undefined value
  3. If no source has a value, return undefined