🧭 Introduction
In modern Node.js applications, configuration management is more than just setting PORT=3000. As your project scales into multiple environments (local, development, staging, production), mismanaged configurations can lead to downtime, security leaks, or brittle deployments.
Frameworks like NestJS and Fastify are built for scalable systems - but they require proper config handling to unlock their full potential. This guide will help you set up robust, secure, and type-safe config practices using real-world examples and tools that work.
🔑 Key Takeaways
- Avoid hardcoding secrets or environment-specific values in your codebase.
- Use
.envfiles forlocalenvironment; switch to secret managers inproductionand other environments. - Always validate and type-check your configuration.
- Separate runtime configuration from build-time logic.
- Use official tools in
NestJSandFastifyto make your config type-safe and secure.
⚙️ Why Configuration Matters
Hardcoding your secrets in code might work locally - but in production, it’s a security liability.
Managing config externally makes your code:
- Easier to test across environments
- Safer to deploy
- Easier to change without a rebuild
Real-world risks of poor config:
- Exposing secrets in source control
- Breaking services with incorrect variables
- Making debugging across environments harder
📁 Core Concepts
What is a Config?
A config is anything that varies between environments: database URLs, API keys, feature flags, ports, etc.
.env Files
Plaintext files that store key-value pairs:
PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
JWT_SECRET=supersecret
Hierarchy
Structure config with defaults and override by environment:
.env # base
.env.local # developer machine
.env.stage # stage
.env.prod # production
✅ General Best Practices in Node.js
- Use dotenv for local development:
require('dotenv').config();
const port = process.env.PORT || 3000;
- Node.js 20+ built-in support:
node --env-file=.env index.js
- Store secrets and environment-specific values in environment variables, not in code.
- Keep
.envout of version control (don’t commit intogit), always add to.gitignore - Provide
.env.examplewith all required env variables to help new developers to setup environment quickly - Use
.envfor only for thelocalenvironment - Use schema validation to catch missing or malformed configs at startup (Zod, Joi)
- Each environment (
local,dev,test,prod) should have its own config. - Use cloud secret managers (AWS Secrets Manager, Azure Key Vault, etc.) in production for enhanced security.
- Don’t overwrite
NODE_ENVvariable. OverwritingNODE_ENVmixes up application configuration with Node.js runtime optimizations, disables important production features in non-production environments, and causes confusion when you need more than justdevelopmentorproductionmodes. Instead, use a dedicated variable (likeENVorAPP_ENV) for custom environment names and keepNODE_ENVstrictly for controlling runtime optimizations and debugging features
🧪 Configuration in NestJS
NestJS has a built-in @nestjs/config module that simplifies config loading, validation, and access.
Install:
npm install @nestjs/config
Set up with validation (Joi):
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
PORT: Joi.number().default(3000),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
}),
}),
],
})
export class AppModule {}
Usage in services:
import { ConfigService } from '@nestjs/config';
@Injectable()
export class UserService {
constructor(private configService: ConfigService) {}
getJwtSecret(): string {
return this.configService.get<string>('JWT_SECRET');
}
}
🚀 Configuration in Fastify
Fastify recommends env-schema with @sinclair/typebox for schema validation and type-safe config loading.
Install:
npm install env-schema @sinclair/typebox
Define schema & load config:
// config.js
import envSchema from 'env-schema';
import { Type } from '@sinclair/typebox';
const schema = Type.Object({
PORT: Type.String({ default: '3000' }),
DATABASE_URL: Type.String(),
JWT_SECRET: Type.String(),
});
export const config = envSchema({
schema,
dotenv: true, // auto-load .env
});
Inject config into Fastify instance:
// main.js
import Fastify from 'fastify';
import { config } from 'config';
const fastify = Fastify();
fastify.decorate('config', config);
🧠 Advanced Patterns & Tools
- Secret Managers Use AWS Secrets Manager, Azure Key Vault, or GCP Secret Manager to inject secrets securely at runtime.
- CI/CD Integration Inject environment variables via GitHub Actions, GitLab CI, etc.
- Microservices Share only what’s necessary. Use central secret/config stores like Vault or Parameter Store.
🧪 Testing & Mocking
- Test-specific configs
Use
.env.testor pass variables via CI: - Set
NODE_ENV=testfor testing environments
NODE_ENV=test
PORT=9999
- Mocking configs Override process.env in test setup files.
⚠️ Common Pitfalls
| ❌ Problem | ✅ Fix |
|---|---|
.env committed to Git | Add .env to .gitignore |
| Secrets in code | Use environment variables or secret managers |
| No schema validation | Use Joi, Zod, or TypeBox |
| Overwrite NODE_ENV | Use explicit variables (e.g. ENV, APP_ENV) |
| Logging sensitive configs | Filter logs and avoid exposing secrets |
🧩 Conclusion
Good configuration is invisible when it works-and disastrous when it doesn’t. Whether you’re building a monolith or microservice, start with secure and scalable config management.
- Use
.envfor local. - Validate all configs.
- Use secret managers in prod.
- Adopt schema-based, type-safe config loading in NestJS and Fastify.
- Add ENV variable for defining current environment, avoid overwriting NODE_ENV
By following these practices, your application becomes safer, easier to maintain, and ready for production at scale.