Back to writing
Glib Rulev
Glib Rulev

Managing Configs and Environment Variables in Node.js Apps

🧭 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 for local environment; switch to secret managers in production and other environments.
  • Always validate and type-check your configuration.
  • Separate runtime configuration from build-time logic.
  • Use official tools in NestJS and Fastify 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 into git), 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 the local 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. Overwriting NODE_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 just development or production modes. Instead, use a dedicated variable (like ENV or APP_ENV) for custom environment names and keep NODE_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 GitAdd .env to .gitignore
Secrets in codeUse environment variables or secret managers
No schema validationUse Joi, Zod, or TypeBox
Overwrite NODE_ENVUse explicit variables (e.g. ENV, APP_ENV)
Logging sensitive configsFilter 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.