Configuration
Chapter 37: Environment Configuration
Section titled “Chapter 37: Environment Configuration”Managing Configuration Across Environments
Section titled “Managing Configuration Across Environments”37.1 Configuration Overview
Section titled “37.1 Configuration Overview”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 +------------------+
================================================================================37.2 NestJS Config Module
Section titled “37.2 NestJS Config Module”Basic Setup
Section titled “Basic Setup”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 {}Configuration Schema
Section titled “Configuration Schema”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.tsimport { 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); } }}37.3 Environment Files
Section titled “37.3 Environment Files”Environment File Structure
Section titled “Environment File Structure”# .env.example (template - commit to git)# ApplicationNODE_ENV=developmentPORT=3000
# DatabaseDB_HOST=localhostDB_PORT=5432DB_USERNAME=postgresDB_PASSWORD=your_password_hereDB_DATABASE=myappDB_POOL_SIZE=20
# RedisREDIS_HOST=localhostREDIS_PORT=6379
# JWTJWT_SECRET=your_secret_hereJWT_EXPIRATION=1d
# LoggingLOG_LEVEL=debug
# .env.developmentNODE_ENV=developmentDB_HOST=localhostDB_PASSWORD=dev_passwordLOG_LEVEL=debug
# .env.productionNODE_ENV=productionDB_HOST=prod-db.example.comDB_PASSWORD=${DB_PASSWORD} # From system environmentLOG_LEVEL=info
# .env.local (not committed - overrides)DB_PASSWORD=local_dev_passwordJWT_SECRET=local_secret_key
# .env.testNODE_ENV=testDB_DATABASE=myapp_testDB_PASSWORD=test_passwordGitignore Configuration
Section titled “Gitignore Configuration”# Environment files.env.env.local.env.*.local.env.production.env.development
# Keep example!.env.example37.4 Configuration Namespaces
Section titled “37.4 Configuration Namespaces”Namespaced Configuration
Section titled “Namespaced Configuration”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.tsimport { 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.tsimport { 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.tsimport { 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' }}37.5 Async Configuration
Section titled “37.5 Async Configuration”Factory Configuration
Section titled “Factory Configuration”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.tsimport { 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 {}37.6 Secrets Management
Section titled “37.6 Secrets Management”Using AWS Secrets Manager
Section titled “Using AWS Secrets Manager”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 configexport 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'),});Using HashiCorp Vault
Section titled “Using HashiCorp Vault”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); }}37.7 Configuration Best Practices
Section titled “37.7 Configuration Best Practices”Configuration Service Pattern
Section titled “Configuration Service Pattern”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... }}37.8 Configuration Summary
Section titled “37.8 Configuration Summary” 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 | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 38: Logging & Error Handling
Last Updated: February 2026