On this page
Configuration and environment management
Why configuration management matters
Hard-coding configuration values (database URLs, API keys, ports) is one of the most common mistakes in backend development. The problems become obvious when you try to deploy to multiple environments (development, staging, production):
- Database host is
localhostin development butdb.prod.example.comin production - JWT secret must be different across environments
- Log level should be
debugin development anderrorin production - CORS origins differ between environments
NestJS provides @nestjs/config — a wrapper around the popular dotenv library — with type-safe configuration namespaces and built-in validation.
Installing @nestjs/config
npm install @nestjs/config joijoi is used for schema validation of environment variables.
Environment files
Create separate .env files for each environment:
.env # Defaults (committed to git — no secrets)
.env.development # Development overrides (committed)
.env.production # Production template (committed, no real values)
.env.test # Test environment (committed)
.env.local # Local overrides (gitignored)Example .env.development:
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=postgres
DB_NAME=bookstore_dev
JWT_SECRET=dev-secret-at-least-32-characters-long
JWT_REFRESH_SECRET=dev-refresh-secret-at-least-32-charsAlways add .env.local and any file with real secrets to .gitignore.
Configuration namespaces
The registerAs function creates a configuration namespace — a typed slice of configuration:
import { registerAs } from '@nestjs/config';
export const mailConfig = registerAs('mail', () => ({
host: process.env['MAIL_HOST'] ?? 'smtp.mailtrap.io',
port: parseInt(process.env['MAIL_PORT'] ?? '587', 10),
user: process.env['MAIL_USER'],
pass: process.env['MAIL_PASS'],
from: process.env['MAIL_FROM'] ?? '[email protected]',
}));Injecting ConfigService
Inject ConfigService to read configuration values anywhere in the application:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class BooksService {
private readonly maxBooksPerUser: number;
constructor(private readonly configService: ConfigService) {
this.maxBooksPerUser = this.configService.get<number>('app.maxBooksPerUser', 10);
}
}The second argument to get() is the default value if the key is not found.
Injecting namespaced config
For namespaced configs, inject with @InjectConfig():
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { jwtConfig } from '../config/app.config';
@Injectable()
export class AuthService {
constructor(
@Inject(jwtConfig.KEY)
private readonly jwt: ConfigType<typeof jwtConfig>,
) {}
getSecret(): string {
return this.jwt.secret ?? ''; // Fully typed
}
}This approach gives you full TypeScript type inference for your configuration — no string keys and no any types.
Dynamic TypeORM configuration
Use TypeOrmModule.forRootAsync() to build the configuration from ConfigService:
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.getOrThrow<string>('database.host'),
port: config.getOrThrow<number>('database.port'),
username: config.getOrThrow<string>('database.username'),
password: config.getOrThrow<string>('database.password'),
database: config.getOrThrow<string>('database.name'),
autoLoadEntities: true,
synchronize: config.get('app.env') === 'development',
logging: config.get('app.env') === 'development',
}),
})getOrThrow() throws an error if the key is missing — useful for critical configuration that must exist.
Main.ts configuration
Read configuration in main.ts before bootstrapping:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const port = configService.get<number>('app.port', 3000);
const prefix = configService.get<string>('app.apiPrefix', 'api');
const corsOrigin = configService.get<string>('app.corsOrigin', '*');
app.setGlobalPrefix(prefix);
app.enableCors({ origin: corsOrigin, credentials: true });
await app.listen(port);
console.log(`Server running on http://localhost:${port}/${prefix}`);
}Feature flags
Use environment variables for feature flags:
export const featuresConfig = registerAs('features', () => ({
enableWebSockets: process.env['FEATURE_WEBSOCKETS'] === 'true',
enableSwagger: process.env['FEATURE_SWAGGER'] !== 'false',
enableRateLimiting: process.env['FEATURE_RATE_LIMITING'] !== 'false',
maxUploadSizeMb: parseInt(process.env['MAX_UPLOAD_SIZE_MB'] ?? '10', 10),
}));Custom config loading
For complex configuration needs, implement ConfigFactory:
export default async (): Promise<Record<string, unknown>> => {
// Load config from a remote source, database, or secrets manager
const secretsManagerConfig = await fetchFromSecretsManager();
return {
database: {
host: secretsManagerConfig.dbHost,
password: secretsManagerConfig.dbPassword,
},
};
};Configuration in tests
For tests, use ConfigModule.forRoot({ load: [testConfig] }) or override specific values with process.env:
beforeAll(async () => {
process.env['DB_NAME'] = 'bookstore_test';
process.env['JWT_SECRET'] = 'test-secret-at-least-32-characters';
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
});import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
export const appConfig = registerAs('app', () => ({
port: parseInt(process.env['PORT'] ?? '3000', 10),
env: process.env['NODE_ENV'] ?? 'development',
apiPrefix: process.env['API_PREFIX'] ?? 'api',
corsOrigin: process.env['CORS_ORIGIN'] ?? 'http://localhost:4200',
}));
export const databaseConfig = registerAs('database', () => ({
host: process.env['DB_HOST'] ?? 'localhost',
port: parseInt(process.env['DB_PORT'] ?? '5432', 10),
username: process.env['DB_USER'] ?? 'postgres',
password: process.env['DB_PASS'] ?? '',
name: process.env['DB_NAME'] ?? 'bookstore',
}));
export const jwtConfig = registerAs('jwt', () => ({
secret: process.env['JWT_SECRET'],
expiresIn: process.env['JWT_EXPIRES_IN'] ?? '15m',
refreshSecret: process.env['JWT_REFRESH_SECRET'],
refreshExpiresIn: process.env['JWT_REFRESH_EXPIRES_IN'] ?? '7d',
}));
// Validation schema for all env variables
export const envValidationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
DB_USER: Joi.string().required(),
DB_PASS: Joi.string().required(),
DB_NAME: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
JWT_REFRESH_SECRET: Joi.string().min(32).required(),
});
Sign in to track your progress