Environment Variables & Configuration
Different Settings for Different Worlds
Open interactive version (quiz + challenge)Real-world analogy
Think of your app like you at different places. At home, you wear pajamas, eat from the fridge, and have your own WiFi password. At the office, you wear business clothes, eat from the cafeteria, and use the company network. Same YOU, different settings! Environment variables let your app behave differently in development (home) vs production (office) without changing the code.
What is it?
Environment variables are key-value pairs that configure your application without hardcoding values in source code. They allow the same codebase to behave differently across environments (development, staging, production). The @nestjs/config module provides a clean, type-safe way to manage configuration in NestJS.
Real-world relevance
Every deployed application uses environment variables. Your local development uses a local database URL. Staging points to a test database. Production points to the real database. The code is identical — only the environment variables change.
Key points
- .env Files — A .env file is a simple text file with KEY=VALUE pairs like DATABASE_URL=mongodb://localhost:27017. It stores sensitive configuration outside your source code so secrets never end up in version control. Always add .env to your .gitignore — committing secrets to Git is a serious security risk.
- process.env — Node.js reads environment variables through the process.env object: process.env.DATABASE_URL. The dotenv package loads .env files automatically at startup. In NestJS, the @nestjs/config module provides a cleaner approach with dependency injection and type safety built in from the start.
- @nestjs/config Module — ConfigModule.forRoot() loads your .env files and registers ConfigService as an injectable provider throughout your app. Access values anywhere with this.config.get('DATABASE_URL'). You can also define typed configuration objects for complex settings, making your config type-safe and IDE-friendly.
- Environment-Specific Configs — Create separate files for each environment: .env.development for local work, .env.production for live servers, and .env.test for running tests. Your app loads the correct file based on the NODE_ENV variable, ensuring each environment uses its own database URLs, API keys, and feature flags.
- Secrets Management — API keys, database passwords, JWT secrets — never hardcode these in your source code! In production, use your cloud provider's secrets manager (AWS Secrets Manager, Vercel env vars, Railway variables). These services encrypt secrets at rest and rotate them automatically.
- .env.example Template — Commit a .env.example file with empty placeholder values as documentation: DATABASE_URL= JWT_SECRET=. When a new developer joins the team, they copy .env.example to .env and fill in their own local values. This documents every required variable without exposing actual secrets in the repository.
- Config Validation — Validate environment variables at startup using Joi or class-validator. If DATABASE_URL is missing, your app should crash immediately with a clear error message — not five minutes later with a cryptic database connection error that takes an hour to debug.
- Feature Flags with Config — Use environment variables as feature flags to enable or disable functionality: ENABLE_NEW_DASHBOARD=true. This lets you deploy code for a new feature to production but keep it hidden until ready. Toggle features per environment without redeploying — essential for safe, incremental rollouts.
- Config Namespaces — For large applications, organize configuration into namespaces: database config, auth config, email config each in their own file. NestJS supports registerAs to create namespaced configs accessed via this.config.get('database.host'). This keeps configuration organized and prevents naming collisions across modules.
Code example
// .env file (NEVER commit this!)
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=super-secret-key-change-in-production
PORT=3000
NODE_ENV=development
// .env.example (DO commit this)
DATABASE_URL=
JWT_SECRET=
PORT=3000
NODE_ENV=development
// .gitignore
.env
.env.local
.env.production
// app.module.ts — Load config
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // available everywhere
envFilePath: '.env',
}),
],
})
export class AppModule {}
// any.service.ts — Use config values
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(private config: ConfigService) {}
getJwtSecret(): string {
return this.config.get<string>('JWT_SECRET');
}
}
// Config validation with Joi
import * as Joi from 'joi';
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
PORT: Joi.number().default(3000),
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
}),
});
// If DATABASE_URL is missing, app crashes at startup:
// Error: "DATABASE_URL" is requiredLine-by-line walkthrough
- 1. .env file (NEVER commit this!)
- 2.
- 3.
- 4.
- 5.
- 6.
- 7. .env.example (DO commit this)
- 8.
- 9.
- 10.
- 11.
- 12.
- 13. .gitignore
- 14. Method chaining on the previous expression
- 15. Method chaining on the previous expression
- 16. Method chaining on the previous expression
- 17.
- 18. app.module.ts — Load config
- 19. Importing required dependencies
- 20.
- 21. Decorator that adds metadata or behavior
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29. Exporting for use in other files
- 30.
- 31. any.service.ts — Use config values
- 32. Importing required dependencies
- 33.
- 34. Decorator that adds metadata or behavior
- 35. Exporting for use in other files
- 36.
- 37.
- 38.
- 39. Returning a value
- 40. Closing block
- 41. Closing block
- 42.
- 43. Config validation with Joi
- 44. Importing required dependencies
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52. Method chaining on the previous expression
- 53. Method chaining on the previous expression
- 54.
- 55.
- 56. If DATABASE_URL is missing, app crashes at startup:
- 57. Error: "DATABASE_URL" is required
Spot the bug
// .env committed to Git!
DATABASE_URL=mongodb://admin:password123@production-server:27017/myapp
JWT_SECRET=my-super-secret-keyNeed a hint?
Should .env files with real credentials be in your Git repository?
Show answer
NEVER commit .env files with real credentials to Git! Anyone with repo access can see your passwords. Fix: add .env to .gitignore and use .env.example with empty values instead.
Explain like I'm 5
Think of your app like getting dressed. At home you wear pajamas (development). At school you wear a uniform (production). Same you, different outfits! Environment variables are like outfit lists for each place - you don't sew clothes into your body, you just change what you wear.
Fun fact
In 2016, an Uber engineer accidentally committed AWS credentials to a public GitHub repo. Hackers found them within minutes and accessed data of 57 million users. This is why .env files exist and why you NEVER commit secrets to Git!
Hands-on challenge
Create a .env file with DATABASE_URL, JWT_SECRET, and PORT. Set up ConfigModule with Joi validation that requires DATABASE_URL and JWT_SECRET. Add a .env.example and update .gitignore. What happens when you remove DATABASE_URL?
More resources
- NestJS Configuration (NestJS Official)
- dotenv Documentation (GitHub)
- Environment Variables Best Practices (The Twelve-Factor App)