ConfigurationModule Usage Guide
The ConfigurationModule provides a flexible and type-safe way to manage application configuration. It supports environment variables, validation, and custom decorators for easy access to configuration values.
✅ Why Use ConfigurationModule?
The ConfigurationModule offers several key advantages over traditional configuration approaches:
-
Module Encapsulation Principle: Environment variables should be defined in the modules they belong to (e.g., board features in
board.config.ts, team features inteam.config.ts). This ensures better encapsulation, clearer ownership, and easier maintenance. Only add general server-wide configuration toserver.config.tsif it doesn't belong to any specific module. -
Type Safety: Configuration values are strongly typed with TypeScript, preventing runtime errors caused by type mismatches.
-
Validation: Uses class-validator decorators (
@IsBoolean,@IsString,@IsUrl, etc.) to ensure configuration values meet expected formats at runtime. -
Transformation: Automatically converts environment variables (e.g., string "true"/"false" to boolean) using transformers like
@StringToBoolean(). -
Default Values: Properties can have sensible defaults, reducing the number of required environment variables.
-
Discoverability: All configuration options are clearly visible in one place with their types, making it easy for developers to understand what can be configured.
-
Maintainability: Changes to configuration structure are easier to track and refactor across the codebase.
❌ Forbidden Practices
When using the ConfigurationModule, the following practices are forbidden and should be avoided:
- Direct
process.envaccess: Never useprocess.env.VARIABLE_NAMEdirectly in your code. This bypasses validation, type safety, and defaults. - Legacy
Configuration.get(): Do not use the oldConfiguration.get()method. This is deprecated and doesn't provide type safety. - NestJS
ConfigService: Avoid using NestJS's built-inConfigService. Use the ConfigurationModule pattern instead for consistency and better type safety.
These approaches lack the validation, transformation, and type safety benefits that the ConfigurationModule provides.
1. Creating a Configuration Class
Define a class to represent your configuration in your module with @Configuration(). Use the @ConfigProperty decorator to mark properties that should be loaded from environment variables or other sources:
import { ConfigProperty, Configuration } from '@infra/configuration';
import { IsBoolean, IsNumber } from 'class-validator';
import { StringToBoolean, StringToNumber } from '@shared/controller/transformer';
export const MY_FEATURE_CONFIG_TOKEN = 'MY_FEATURE_CONFIG_TOKEN';
@Configuration()
export class MyFeatureConfig {
@IsBoolean()
@StringToBoolean()
@ConfigProperty('MY_FEATURE_ENABLED')
public readonly enabled!: boolean;
@IsNumber()
@StringToNumber()
@ConfigProperty('MY_FEATURE_TIMEOUT')
public readonly timeout = 3000;
}
2. Registering the ConfigurationModule
Import and add the ConfigurationModule to your feature or root module:
import { Module } from '@nestjs/common';
import { ConfigurationModule } from '@infra/configuration';
import { MyFeatureConfig, MY_FEATURE_CONFIG_TOKEN } from './my-feature.config';
@Module({
imports: [ConfigurationModule.register(MY_FEATURE_CONFIG_TOKEN, MyFeatureConfig)],
})
export class AppModule {}
3. Injecting and Using the Configuration
Inject your configuration class into services or controllers as needed:
import { Inject, Injectable } from '@nestjs/common';
import { MyFeatureConfig, MY_FEATURE_CONFIG_TOKEN } from './my-feature.config';
@Injectable()
export class MyFeatureService {
constructor(@Inject(MY_FEATURE_CONFIG_TOKEN) private readonly config: MyFeatureConfig) {}
doSomething() {
if (this.config.enabled) {
// ...
}
}
}
4. Environment Variables
The project uses multiple environment variable files to manage different deployment scenarios:
.env.default
- Purpose: Contains default values for all locally configured properties
- When to use: Provides a template for developers
- Content: Safe default values that work for local development
- Committed: Yes, this file is committed to version control
# Example from .env.default
SESSION_VALKEY__MODE=in-memory
SC_DOMAIN=localhost
ALERT_STATUS_URL=https://status.dbildungscloud.dev/
.env.test
- Purpose: Configuration specifically for test environments
- When to use: Automatically loaded during test execution
- Content: Test-specific values (test databases, mock endpoints, simplified settings)
- Committed: Yes, safe for version control as it contains only test configuration
# Example from .env.test
AES_KEY=test-key-with-32-characters-long
ADMIN_API__ALLOWED_API_KEYS=onlyusedintests:thisistheadminapitokeninthetestconfig
.env (local)
- Purpose: Your personal development environment overrides
- When to use: Override defaults with your local development settings
- Content: Personal API keys, local service URLs, custom feature flags
- Committed: No, this file is in
.gitignoreand should not be committed
# Example .env (create this file locally)
MY_FEATURE_ENABLED=true
MY_FEATURE_TIMEOUT=5000
DATABASE_URL=postgresql://localhost:5432/mylocal_db
API_KEY=your-personal-development-key
5. Validation and Transformation
The ConfigurationModule supports comprehensive validation and transformation using class-validator decorators and custom transformers. This ensures that configuration values are correctly typed, validated, and transformed from environment variable strings.
5.1. Basic Type Validation
Use these decorators for basic type validation:
import { IsBoolean, IsNumber, IsString, IsInt } from 'class-validator';
import { StringToBoolean, StringToNumber } from '@shared/controller/transformer';
@Configuration()
export class MyConfig {
@IsBoolean()
@StringToBoolean()
@ConfigProperty('FEATURE_ENABLED')
public featureEnabled = false;
@IsNumber()
@StringToNumber()
@ConfigProperty('TIMEOUT_MS')
public timeoutMs = 5000;
@IsString()
@ConfigProperty('API_KEY')
public apiKey!: string;
@IsInt()
@StringToNumber()
@ConfigProperty('MAX_CONNECTIONS')
public maxConnections = 100;
}
5.2. URL Validation
For URL properties, use @IsUrl() with appropriate options:
import { IsUrl } from 'class-validator';
@Configuration()
export class ServiceConfig {
@IsUrl({ require_tld: false }) // Allow localhost URLs
@ConfigProperty('SERVICE_URL')
public serviceUrl = 'http://localhost:3000';
@IsUrl() // Require valid TLD for production URLs
@ConfigProperty('EXTERNAL_API_URL')
public externalApiUrl!: string;
}
5.3. Optional Properties
Mark properties as optional when they may not be provided:
import { IsOptional, IsString } from 'class-validator';
@Configuration()
export class OptionalConfig {
@IsOptional()
@IsString()
@ConfigProperty('OPTIONAL_SETTING')
public optionalSetting?: string;
}
5.4. Conditional Validation
Use @ValidateIf() to validate properties only when certain conditions are met:
import { ValidateIf, IsString, IsBoolean } from 'class-validator';
import { StringToBoolean } from '@shared/controller/transformer';
@Configuration()
export class ConditionalConfig {
@IsBoolean()
@StringToBoolean()
@ConfigProperty('FEATURE_ENABLED')
public featureEnabled = false;
@ValidateIf((config: ConditionalConfig) => config.featureEnabled)
@IsString()
@ConfigProperty('FEATURE_API_KEY')
public featureApiKey?: string;
@ValidateIf((config: ConditionalConfig) => config.featureEnabled)
@IsUrl({ require_tld: false })
@ConfigProperty('FEATURE_SERVICE_URL')
public featureServiceUrl?: string;
}
5.5. Enum Validation
Validate against specific enum values:
import { IsEnum } from 'class-validator';
enum LogLevel {
ERROR = 'error',
WARN = 'warn',
INFO = 'info',
DEBUG = 'debug',
}
@Configuration()
export class LoggingConfig {
@IsEnum(LogLevel)
@ConfigProperty('LOG_LEVEL')
public logLevel = LogLevel.INFO;
}
5.6. Array Validation
For array properties:
import { IsArray, IsString } from 'class-validator';
@Configuration()
export class ArrayConfig {
@IsArray()
@IsString({ each: true })
@ConfigProperty('ALLOWED_ORIGINS')
public allowedOrigins: string[] = ['http://localhost:3000'];
}
6. PublicApiConfig Pattern
The PublicApiConfig pattern allows modules to expose their configuration values to the public API endpoint /config/public. This is useful for client applications that need to know about certain feature flags or configuration values.
6.1. Passing Configuration to Client Applications
The PublicApiConfig pattern exposes server configuration to client applications through public API endpoints. Client applications can fetch this configuration during startup to determine feature flags, non-sensitive service URLs, UI settings, and localization options—without requiring environment-specific builds.
⚠️ Security Warning: Never expose secrets through this endpoint, as the data is readable in the browser and in request/response traffic.
Available Endpoints
- Main config endpoint:
http://{{HOST}}:{{PORT}}/api/v3/config/public - Files config endpoint:
http://{{HOST}}:{{PORT}}/api/v3/files/config/public
The endpoints are implemented in the ServerConfigController, which aggregates all registered PublicApiConfig classes.
6.2. Creating a PublicApiConfig Class
Create a separate configuration class containing only the properties you want to expose publicly:
// my-feature.config.ts
import { ConfigProperty, Configuration } from '@infra/configuration';
import { StringToBoolean } from '@shared/controller/transformer';
import { IsBoolean } from 'class-validator';
export const MY_FEATURE_PUBLIC_API_CONFIG_TOKEN = 'MY_FEATURE_PUBLIC_API_CONFIG_TOKEN';
@Configuration()
export class MyFeaturePublicApiConfig {
@ConfigProperty('FEATURE_MY_FEATURE_ENABLED')
@IsBoolean()
@StringToBoolean()
public featureMyFeatureEnabled = false;
}
// Optional: Extend for full configuration with private settings
export const MY_FEATURE_CONFIG_TOKEN = 'MY_FEATURE_CONFIG_TOKEN';
@Configuration()
export class MyFeatureConfig extends MyFeaturePublicApiConfig {
@ConfigProperty('MY_FEATURE_PRIVATE_KEY')
@IsString()
public privateKey!: string; // Not exposed publicly
}
6.3. Export from Module Index
Export your PublicApiConfig from your module's index file:
// modules/my-feature/index.ts
export { MY_FEATURE_PUBLIC_API_CONFIG_TOKEN, MyFeaturePublicApiConfig } from './my-feature.config';
export { MyFeatureModule } from './my-feature.module';
6.4. Register in Server Module
Register the PublicApiConfig in your module's API module (if you have one):
// my-feature-api.module.ts
import { ConfigurationModule } from '@infra/configuration';
import { MY_FEATURE_PUBLIC_API_CONFIG_TOKEN, MyFeaturePublicApiConfig } from './my-feature.config';
@Module({
imports: [
ConfigurationModule.register(MY_FEATURE_PUBLIC_API_CONFIG_TOKEN, MyFeaturePublicApiConfig),
],
})
export class MyFeatureApiModule {}
6.5. Adding to ConfigResponse
To make your module's configuration available through the public config API, you need to modify several files:
Step 1: Add Import to config.response.ts
Add the import for your PublicApiConfig to apps/server/src/modules/server/api/dto/config.response.ts:
import { MyFeaturePublicApiConfig } from '@modules/my-feature';
Step 2: Add Properties to ConfigResponse Class
Add the properties you want to expose to the ConfigResponse class:
export class ConfigResponse {
// ... existing properties ...
@ApiProperty()
FEATURE_MY_FEATURE_ENABLED: boolean;
// ... rest of properties ...
}
Step 3: Add to Constructor Type and Assignment
Update the constructor parameter type and assignment:
export class ConfigResponse {
constructor(
config: ServerConfig &
VideoConferencePublicApiConfig &
// ... other existing PublicApiConfigs &
MyFeaturePublicApiConfig // Add your config here
) {
// ... existing assignments ...
this.FEATURE_MY_FEATURE_ENABLED = config.featureMyFeatureEnabled;
// ... rest of assignments ...
}
}
Step 4: Update ServerUc
Add your config to the ServerUc constructor and getConfig method:
@Injectable()
export class ServerUc {
constructor(
// ... existing configs ...
@Inject(MY_FEATURE_PUBLIC_API_CONFIG_TOKEN)
private readonly myFeatureConfig: MyFeaturePublicApiConfig
) {}
public getConfig(): ConfigResponse {
const configDto = ConfigResponseMapper.mapToResponse(
this.config,
// ... existing configs ...
this.myFeatureConfig
);
return configDto;
}
}
Step 5: Update ConfigResponseMapper
Update the ConfigResponseMapper:
export class ConfigResponseMapper {
public static mapToResponse(
serverConfig: ServerConfig,
// ... existing configs ...
myFeatureConfig: MyFeaturePublicApiConfig
): ConfigResponse {
const configResponse = new ConfigResponse({
...serverConfig,
// ... existing configs ...
...myFeatureConfig,
});
return configResponse;
}
}
6.6. Best Practices for PublicApiConfig
- Only expose necessary values: Don't include sensitive information like API keys or secrets
- Use descriptive names: Follow the pattern
FEATURE_[MODULE]_[FEATURE]_ENABLED - Consistent typing: Use boolean for feature flags, strings for URLs, etc.
- Documentation: Add
@ApiProperty()decorators with descriptions for Swagger documentation - Default values: Provide sensible defaults for all configuration properties
7. Best Practices
7.1. Module-Level Configuration
Each configuration class should be placed at the top level of its respective module whenever possible. This makes it easy to locate and manage module-specific settings.
src/modules/my-feature/
├── my-feature.config.ts // ← Configuration at module root
├── my-feature.module.ts
├── index.ts
└── services/
└── my-feature.service.ts
7.2. Infrastructure Module Pattern
Modules located in apps/server/src/infra should always receive their configuration from the outside as arguments to their register() function. This promotes reusability and decoupling.
// Example from apps/server/src/infra/calendar/calendar.module.ts
@Module({})
export class CalendarModule {
public static register(injectionToken: string, Constructor: new () => CalendarConfig): DynamicModule {
return {
module: CalendarModule,
imports: [HttpModule, CqrsModule, LoggerModule, ConfigurationModule.register(injectionToken, Constructor)],
providers: [CalendarMapper, CalendarService],
exports: [CalendarService],
};
}
}
This pattern allows the consuming module to provide the appropriate configuration:
// Usage in a business module
@Module({
imports: [
CalendarModule.register(MY_CALENDAR_CONFIG_TOKEN, MyCalendarConfig),
],
})
export class MyBusinessModule {}
7.3. Default Configuration with Interface Pattern
For more flexibility, infrastructure modules often provide a default configuration class alongside an internal interface. This approach allows consumers to either use the provided default configuration or implement their own custom configuration while maintaining type safety. The interface ensures all implementations provide the necessary properties with the correct types.
// Example: apps/server/src/infra/schulconnex-client/schulconnex-client.config.ts
export interface InternalSchulconnexClientConfig {
personInfoTimeoutInMs: number;
apiUrl?: string;
clientId?: string;
}
/**
* Default configuration for the SchulconnexClient.
* Create your own config class implementing InternalSchulconnexClientConfig
* if you need different environment variables or behavior.
*/
@Configuration()
export class SchulconnexClientConfig implements InternalSchulconnexClientConfig {
@ConfigProperty('SCHULCONNEX_CLIENT__PERSON_INFO_TIMEOUT_IN_MS')
@StringToNumber()
@IsNumber()
public personInfoTimeoutInMs = 3000;
@ConfigProperty('SCHULCONNEX_CLIENT__API_URL')
@IsOptional()
@IsString()
public apiUrl?: string;
@ConfigProperty('SCHULCONNEX_CLIENT__CLIENT_ID')
@IsOptional()
@IsString()
public clientId?: string;
}
The module accepts any constructor implementing the interface:
export class SchulconnexClientModule {
public static register(
injectionToken: string,
Constructor: new () => InternalSchulconnexClientConfig
): DynamicModule {
// ... module configuration
}
}
For complete encapsulation, the infrastructure module's internal services should depend only on the interface, not the concrete configuration class. This ensures type safety and flexibility while keeping the module decoupled from specific implementations.
// Internal service using the interface
@Injectable()
export class SchulconnexRestClient {
constructor(
@Inject(SCHULCONNEX_CLIENT_CONFIG_TOKEN)
private readonly config: InternalSchulconnexClientConfig // ← Interface type
) {}
async getPersonInfo(): Promise<PersonInfo> {
const timeout = this.config.personInfoTimeoutInMs;
// Implementation works with any config satisfying the interface
}
}
7.4. Module Options Pattern
When an infrastructure module requires more than one injection token and constructor (i.e., multiple configuration dependencies), introduce a module options interface to keep the API clean and organized.
// Example: apps/server/src/infra/tsp-client/types/module-options.ts
export interface TspClientModuleOptions {
encryptionConfig: { configInjectionToken: string; configConstructor: new () => EncryptionConfig };
tspClientConfig: { configInjectionToken: string; configConstructor: new () => TspClientConfig };
}
Use this options interface in your infrastructure module's register method:
// Example: apps/server/src/infra/tsp-client/tsp-client.module.ts
@Module({})
export class TspClientModule {
public static register(options: TspClientModuleOptions): DynamicModule {
const { encryptionConfig, tspClientConfig } = options;
return {
module: TspClientModule,
imports: [
LoggerModule,
OauthAdapterModule,
EncryptionModule.register(encryptionConfig.configConstructor, encryptionConfig.configInjectionToken),
ConfigurationModule.register(tspClientConfig.configInjectionToken, tspClientConfig.configConstructor),
],
providers: [TspClientFactory],
exports: [TspClientFactory],
};
}
}
This pattern provides several benefits:
- Clean API: Single parameter instead of multiple individual parameters
- Type Safety: All required configurations are defined in the interface
- Maintainability: Easy to add new configuration dependencies
- Self-Documentation: The interface clearly shows all required dependencies
7.5. Timeout Configuration Pattern
For modules that need custom request timeouts, use the special timeout configuration pattern. Timeout configurations extend the TimeoutConfig base class and provide configurable timeout values for specific endpoints.
// Example: apps/server/src/modules/room/timeout.config.ts
import { TimeoutConfig } from '@core/interceptor/timeout-interceptor-config.interface';
import { ConfigProperty, Configuration } from '@infra/configuration';
import { StringToNumber } from '@shared/controller/transformer';
import { IsNumber } from 'class-validator';
export const ROOM_TIMEOUT_CONFIG_TOKEN = 'ROOM_TIMEOUT_CONFIG_TOKEN';
export const ROOM_INCOMING_REQUEST_TIMEOUT_COPY_API_KEY = 'roomIncomingRequestTimeoutCopyApi';
@Configuration()
export class RoomTimeoutConfig extends TimeoutConfig {
@ConfigProperty('INCOMING_REQUEST_TIMEOUT_COPY_API')
@IsNumber()
@StringToNumber()
public [ROOM_INCOMING_REQUEST_TIMEOUT_COPY_API_KEY] = 60000;
}
Use the @RequestTimeout decorator on controller endpoints to apply custom timeouts. The decorator references the property key from your timeout configuration:
import { RequestTimeout } from '@shared/common/decorators/timeout.decorator';
@Controller('rooms')
export class RoomController {
@RequestTimeout('roomIncomingRequestTimeoutCopyApi')
@Post('copy')
async copyRoom(): Promise<void> {
// This endpoint will use the configured timeout value
}
}
Key Points:
- Timeout configs must extend
TimeoutConfigbase class - Export property key constants for use with the decorator
- Property keys should match the string values used in the decorator
- Register the timeout config with
CoreModule.register()in your module - If a property doesn't exist, the interceptor falls back to default timeout
8. Summary
- Create a config class decorated with
@Configuration(). - Use
@ConfigProperty()to bind properties to environment variables. - Add validation decorators (
@IsBoolean,@IsString, etc.) and transformers (@StringToBoolean,@StringToNumber). - Register the configuration using
ConfigurationModule.register(). - Inject the configuration into your services using
@Inject(). - Set values via
.env,.env.default, or.env.testfiles. - For infrastructure modules, use the
register()pattern with external configuration. - For public API exposure, create a separate
PublicApiConfigclass and integrate it into theConfigResponsesystem. - For custom timeouts, extend
TimeoutConfigand use the@RequestTimeout()decorator.