🧭 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
.env
files forlocal
environment; switch to secret managers inproduction
and other environments. - Always validate and type-check your configuration.
- Separate runtime configuration from build-time logic.
- Use official tools in
NestJS
andFastify
to 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
.env
out of version control (don’t commit intogit
), always add to.gitignore
- Provide
.env.example
with all required env variables to help new developers to setup environment quickly - Use
.env
for only for thelocal
environment - 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_ENV
variable. OverwritingNODE_ENV
mixes up application configuration with Node.js runtime optimizations, disables important production features in non-production environments, and causes confusion when you need more than justdevelopment
orproduction
modes. Instead, use a dedicated variable (likeENV
orAPP_ENV
) for custom environment names and keepNODE_ENV
strictly 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.test
or pass variables via CI: - Set
NODE_ENV=test
for 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
.env
for 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.