Skip to content

Configuration

Managing Configuration Across Environments

Section titled “Managing Configuration Across Environments”

Proper configuration management ensures your application works correctly across different environments.

Configuration Hierarchy
================================================================================
Priority (highest to lowest):
1. Environment Variables
+------------------+
| process.env | <-- Highest priority
+------------------+
|
v
2. .env Files
+------------------+
| .env.production |
| .env.development |
| .env.local |
+------------------+
|
v
3. Configuration Files
+------------------+
| config/*.ts |
+------------------+
|
v
4. Default Values
+------------------+
| Code defaults | <-- Lowest priority
+------------------+
================================================================================

src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
// Load environment variables
ConfigModule.forRoot({
isGlobal: true, // Available everywhere
envFilePath: [`.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
ignoreEnvFile: process.env.NODE_ENV === 'production', // Use system env in prod
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
src/config/config.schema.ts
import { z } from 'zod';
export const configSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().transform(Number).default('3000'),
// Database
DB_HOST: z.string().default('localhost'),
DB_PORT: z.string().transform(Number).default('5432'),
DB_USERNAME: z.string().default('postgres'),
DB_PASSWORD: z.string(),
DB_DATABASE: z.string().default('myapp'),
DB_POOL_SIZE: z.string().transform(Number).default('20'),
// Redis
REDIS_HOST: z.string().default('localhost'),
REDIS_PORT: z.string().transform(Number).default('6379'),
// JWT
JWT_SECRET: z.string(),
JWT_EXPIRATION: z.string().default('1d'),
// Logging
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
export type Config = z.infer<typeof configSchema>;
// src/config/config.validation.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { configSchema, Config } from './config.schema';
@Injectable()
export class ConfigValidation implements OnModuleInit {
constructor(private configService: ConfigService) {}
onModuleInit() {
const config = {
NODE_ENV: this.configService.get('NODE_ENV'),
PORT: this.configService.get('PORT'),
DB_HOST: this.configService.get('DB_HOST'),
DB_PORT: this.configService.get('DB_PORT'),
DB_USERNAME: this.configService.get('DB_USERNAME'),
DB_PASSWORD: this.configService.get('DB_PASSWORD'),
DB_DATABASE: this.configService.get('DB_DATABASE'),
// ... other config
};
const result = configSchema.safeParse(config);
if (!result.success) {
console.error('Configuration validation failed:');
result.error.issues.forEach(issue => {
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
});
process.exit(1);
}
}
}

Terminal window
# .env.example (template - commit to git)
# Application
NODE_ENV=development
PORT=3000
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your_password_here
DB_DATABASE=myapp
DB_POOL_SIZE=20
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# JWT
JWT_SECRET=your_secret_here
JWT_EXPIRATION=1d
# Logging
LOG_LEVEL=debug
# .env.development
NODE_ENV=development
DB_HOST=localhost
DB_PASSWORD=dev_password
LOG_LEVEL=debug
# .env.production
NODE_ENV=production
DB_HOST=prod-db.example.com
DB_PASSWORD=${DB_PASSWORD} # From system environment
LOG_LEVEL=info
# .env.local (not committed - overrides)
DB_PASSWORD=local_dev_password
JWT_SECRET=local_secret_key
# .env.test
NODE_ENV=test
DB_DATABASE=myapp_test
DB_PASSWORD=test_password
# Environment files
.env
.env.local
.env.*.local
.env.production
.env.development
# Keep example
!.env.example

src/config/database.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 5432,
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE || 'myapp',
poolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 20,
synchronize: process.env.DB_SYNCHRONIZE === 'true',
logging: process.env.DB_LOGGING === 'true',
}));
// src/config/redis.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('redis', () => ({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD,
db: parseInt(process.env.REDIS_DB, 10) || 0,
}));
// src/config/jwt.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET,
expiration: process.env.JWT_EXPIRATION || '1d',
refreshExpiration: process.env.JWT_REFRESH_EXPIRATION || '7d',
}));
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import databaseConfig from './config/database.config';
import redisConfig from './config/redis.config';
import jwtConfig from './config/jwt.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, redisConfig, jwtConfig],
envFilePath: [`.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
}),
],
})
export class AppModule {}
// Usage
@Injectable()
export class UsersService {
constructor(private configService: ConfigService) {}
getDatabaseConfig() {
return this.configService.get('database'); // Returns database config object
}
getRedisHost() {
return this.configService.get<string>('redis.host'); // Returns 'redis.host'
}
}

src/config/typeorm.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
export const getTypeOrmConfig = async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => {
const env = configService.get<string>('NODE_ENV');
const baseConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: configService.get<string>('database.host'),
port: configService.get<number>('database.port'),
username: configService.get<string>('database.username'),
password: configService.get<string>('database.password'),
database: configService.get<string>('database.database'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: false,
};
if (env === 'development') {
return {
...baseConfig,
logging: true,
synchronize: true, // Only in development!
};
}
if (env === 'test') {
return {
...baseConfig,
database: configService.get<string>('database.database') + '_test',
dropSchema: true, // Drop and recreate for tests
};
}
// Production
return {
...baseConfig,
poolSize: configService.get<number>('database.poolSize'),
ssl: {
rejectUnauthorized: true,
},
};
};
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getTypeOrmConfig } from './config/typeorm.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: getTypeOrmConfig,
inject: [ConfigService],
}),
],
})
export class AppModule {}

src/config/secrets.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
@Injectable()
export class SecretsService implements OnModuleInit {
private secrets: Record<string, string> = {};
private client: SecretsManager;
constructor(private configService: ConfigService) {
this.client = new SecretsManager({
region: configService.get('AWS_REGION', 'us-east-1'),
});
}
async onModuleInit() {
if (this.configService.get('NODE_ENV') === 'production') {
await this.loadSecrets();
}
}
private async loadSecrets() {
const secretId = this.configService.get('AWS_SECRET_ID');
if (!secretId) return;
try {
const response = await this.client.getSecretValue({ SecretId: secretId });
if (response.SecretString) {
this.secrets = JSON.parse(response.SecretString);
}
} catch (error) {
console.error('Failed to load secrets:', error);
throw error;
}
}
get(key: string): string | undefined {
return this.secrets[key] || this.configService.get(key);
}
getOrThrow(key: string): string {
const value = this.get(key);
if (!value) {
throw new Error(`Missing required secret: ${key}`);
}
return value;
}
}
// Usage in TypeORM config
export const getTypeOrmConfig = async (
configService: ConfigService,
secretsService: SecretsService,
): Promise<TypeOrmModuleOptions> => ({
type: 'postgres',
host: configService.get('database.host'),
port: configService.get('database.port'),
username: configService.get('database.username'),
password: secretsService.get('DB_PASSWORD'), // From secrets manager
database: configService.get('database.database'),
});
src/config/vault.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Vault from 'node-vault';
@Injectable()
export class VaultService implements OnModuleInit {
private vault: Vault;
private secrets: Record<string, any> = {};
constructor(private configService: ConfigService) {
this.vault = Vault({
endpoint: configService.get('VAULT_ADDR', 'http://localhost:8200'),
token: configService.get('VAULT_TOKEN'),
});
}
async onModuleInit() {
if (this.configService.get('NODE_ENV') === 'production') {
await this.loadSecrets();
}
}
private async loadSecrets() {
try {
const path = this.configService.get('VAULT_SECRET_PATH', 'secret/data/myapp');
const result = await this.vault.read(path);
this.secrets = result.data.data || {};
} catch (error) {
console.error('Failed to load secrets from Vault:', error);
throw error;
}
}
get(key: string): string | undefined {
return this.secrets[key] || this.configService.get(key);
}
}

src/config/app-config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AppConfigService {
constructor(private configService: ConfigService) {}
get nodeEnv(): string {
return this.configService.get<string>('NODE_ENV', 'development');
}
get isDevelopment(): boolean {
return this.nodeEnv === 'development';
}
get isProduction(): boolean {
return this.nodeEnv === 'production';
}
get isTest(): boolean {
return this.nodeEnv === 'test';
}
get port(): number {
return this.configService.get<number>('PORT', 3000);
}
get database() {
return {
host: this.configService.get<string>('database.host', 'localhost'),
port: this.configService.get<number>('database.port', 5432),
username: this.configService.get<string>('database.username', 'postgres'),
password: this.getRequired('database.password'),
database: this.configService.get<string>('database.database', 'myapp'),
poolSize: this.configService.get<number>('database.poolSize', 20),
};
}
get jwt() {
return {
secret: this.getRequired('jwt.secret'),
expiration: this.configService.get<string>('jwt.expiration', '1d'),
};
}
get redis() {
return {
host: this.configService.get<string>('redis.host', 'localhost'),
port: this.configService.get<number>('redis.port', 6379),
};
}
private getRequired(key: string): string {
const value = this.configService.get<string>(key);
if (!value) {
throw new Error(`Missing required configuration: ${key}`);
}
return value;
}
}
// Usage
@Injectable()
export class UsersService {
constructor(private appConfig: AppConfigService) {}
async createConnection() {
const { host, port, username, password, database } = this.appConfig.database;
// Use config...
}
}

Configuration Quick Reference
+------------------------------------------------------------------+
| |
| File | Purpose |
| -------------------|------------------------------------------|
| .env | Default environment variables |
| .env.local | Local overrides (not committed) |
| .env.development | Development environment |
| .env.production | Production environment |
| .env.test | Test environment |
| .env.example | Template (committed to git) |
| |
| Best Practices | Description |
| -------------------|------------------------------------------|
| Never commit secrets| Use .env.local or secrets manager |
| Validate config | Use schema validation |
| Use namespaces | Organize related config |
| Type-safe access | Create typed config service |
| Default values | Provide sensible defaults |
| |
+------------------------------------------------------------------+

Chapter 38: Logging & Error Handling


Last Updated: February 2026