Managing Multiple Environments in Node.js Development, Staging, and Production
Learn how to manage different environments (development, staging, production) in your Node.js application with environment variables, .env files, and configuration management.
Managing Multiple Environments in Node.js
Every Node.js application goes through multiple environments: development, staging, and production. Each needs different configuration.
The Three Environments
| Environment | Purpose | Database | Secrets | URL |
|---|---|---|---|---|
| Development | Local coding | Local MongoDB | Dev secrets | localhost:3000 |
| Staging | Pre-prod testing | Staging DB | Staging secrets | staging.yourdomain.com |
| Production | Live app | Production DB | Prod secrets | api.yourdomain.com |
Using .env Files per Environment
.env.development # Local development .env.staging # Staging server .env.production # Production server .env.example # Template (committed to Git)
// Load the right .env based on NODE_ENV const env = process.env.NODE_ENV || "development"; require("dotenv").config({ path: `.env.${env}` }); console.log(`Running in ${env} mode`); console.log(`Database: ${process.env.MONGODB_URI}`);
.env.development
NODE_ENV=development PORT=3000 MONGODB_URI=mongodb://localhost:27017/devtinder_dev JWT_SECRET=dev_secret_not_for_production CLIENT_URL=http://localhost:5173 LOG_LEVEL=debug
.env.staging
NODE_ENV=staging PORT=3000 MONGODB_URI=mongodb+srv://staging-user:[email protected]/devtinder_staging JWT_SECRET=staging_secret_different_from_dev CLIENT_URL=https://staging.yourdomain.com LOG_LEVEL=info
.env.production
NODE_ENV=production PORT=3000 MONGODB_URI=mongodb+srv://prod-user:[email protected]/devtinder_prod JWT_SECRET=production_secret_long_random_string CLIENT_URL=https://app.yourdomain.com LOG_LEVEL=warn
Using a Configuration Module
Instead of accessing process.env directly everywhere, centralize config:
// src/config/index.js require("dotenv").config(); const config = { env: process.env.NODE_ENV || "development", port: parseInt(process.env.PORT) || 3000, mongoUri: process.env.MONGODB_URI, jwtSecret: process.env.JWT_SECRET, jwtExpiresIn: process.env.JWT_EXPIRES_IN || "7d", clientUrl: process.env.CLIENT_URL, logLevel: process.env.LOG_LEVEL || "info", isDev: process.env.NODE_ENV === "development", isProd: process.env.NODE_ENV === "production", isStaging: process.env.NODE_ENV === "staging" }; // Validate const required = ["mongoUri", "jwtSecret"]; const missing = required.filter(key => !config[key]); if (missing.length > 0) { throw new Error(`Missing config: ${missing.join(", ")}`); } module.exports = config;
Usage:
const config = require("./config"); app.listen(config.port, () => { console.log(`Server running on port ${config.port} in ${config.env} mode`); }); if (config.isDev) { console.log("Development mode verbose logging enabled"); }
Environment-Specific Behavior
// Different cookie settings per environment const cookieOptions = { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000, ...(config.isProd && { secure: true, sameSite: "none" }), ...(config.isDev && { sameSite: "lax" }) }; // Different logging levels if (config.isDev) { app.use(morgan("dev")); // Verbose } else { app.use(morgan("combined")); // Standard } // Different CORS origins const corsOrigins = { development: "http://localhost:5173", staging: "https://staging.yourdomain.com", production: "https://app.yourdomain.com" }; app.use(cors({ origin: corsOrigins[config.env], credentials: true }));
Setting NODE_ENV
Local development:
# In .env.development NODE_ENV=development # Or in package.json scripts "scripts": { "dev": "NODE_ENV=development nodemon src/server.js", "start": "NODE_ENV=production node src/server.js" }
On EC2:
# In .env.production NODE_ENV=production # Or in PM2 ecosystem file module.exports = { apps: [{ name: "devtinder-api", script: "src/server.js", env: { NODE_ENV: "production" } }] };
The Takeaway
Managing multiple environments involves: using separate .env files per environment (.env.development, .env.staging, .env.production), loading the right file based on NODE_ENV, centralizing config in a config module with validation, using environment-specific behavior (cookie settings, CORS, logging), and setting NODE_ENV in package.json scripts or PM2 ecosystem files. Always use different secrets per environment.
Use separate .env files per environment (.env.development, .env.staging, .env.production), load the right one based on NODE_ENV with dotenv config path, centralize all config in a config module, and use environment-specific behavior for cookies, CORS, and logging. Always use different secrets per environment.
Development is for local coding (local DB, debug logging, dev secrets). Staging is for pre-production testing (staging DB, info logging, staging secrets, mirrors production). Production is the live app (production DB, warn logging, strong production secrets, real users).
Create a src/config/index.js module that loads dotenv, defines a config object with all variables (with defaults and type casting), validates required variables, and exports the config. Use config.isDev, config.isProd, config.isStaging for environment-specific behavior.
In .env files (NODE_ENV=development or production), in package.json scripts ("dev": "NODE_ENV=development nodemon src/server.js"), or in PM2 ecosystem files (env: { NODE_ENV: "production" }). Always set it many Express and npm packages behave differently based on NODE_ENV.
Never. Use different JWT secrets, database credentials, and API keys for each environment. If a development secret leaks, your production app stays secure. Use weak/random secrets for development and strong, rotated secrets for production.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

