Difficulty: Beginner
Every real application needs configuration that varies between environments: database URLs, API keys, port numbers, feature flags, and secret tokens. Hardcoding these values in your source code is a security risk (anyone with access to the code sees your secrets) and makes it impossible to run the same code in development, staging, and production without modification. Environment variables solve this problem.
Environment variables are key-value pairs that exist outside your application code, set by the operating system or the deployment platform. In Node.js, you access them through the process.env object. For example, process.env.PORT reads the PORT environment variable. If the variable is not set, it returns undefined, so you should always provide sensible defaults: const port = process.env.PORT || 3000.
The dotenv package is the standard tool for loading environment variables from a .env file during development. You create a .env file in your project root with KEY=VALUE pairs, call require('dotenv').config() at the very beginning of your application (before any other code that reads process.env), and the variables become available on process.env. The .env file must be added to .gitignore to prevent secrets from being committed to version control.
The NODE_ENV variable is a convention used by Express and many Node.js packages to determine the runtime environment. When NODE_ENV is set to 'production', Express disables verbose error messages, enables view template caching, and optimizes performance. In development, you get detailed error stacks and more helpful debugging output. Always set NODE_ENV=production in your production environment.
A robust configuration pattern is to create a centralized config module that reads all environment variables, validates them, and exports a structured configuration object. This gives you one place to manage defaults, one place to add validation (e.g., ensure required variables are set), and a clean API for the rest of your application. If a required variable like DATABASE_URL is missing, the application should fail immediately on startup with a clear error message, not silently break at runtime.
// --- .env file ---
// PORT=3000
// DATABASE_URL=postgresql://user:password@localhost:5432/mydb
// JWT_SECRET=my-super-secret-key-123
// NODE_ENV=development
// --- server.js ---
require('dotenv').config(); // Load .env file first!
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const DB_URL = process.env.DATABASE_URL;
const SECRET = process.env.JWT_SECRET;
console.log('Environment:', process.env.NODE_ENV);
console.log('Port:', PORT);
console.log('DB configured:', !!DB_URL);
app.get('/', (req, res) => {
res.json({ environment: process.env.NODE_ENV });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
dotenv.config() reads the .env file and populates process.env. It must be called before any code that reads environment variables. The .env file should never be committed to git - add it to .gitignore.
// --- config/index.js ---
require('dotenv').config();
const requiredVars = ['DATABASE_URL', 'JWT_SECRET'];
// Validate required variables exist
for (const varName of requiredVars) {
if (!process.env[varName]) {
console.error(`Missing required env variable: ${varName}`);
process.exit(1);
}
}
module.exports = {
port: parseInt(process.env.PORT) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
isProduction: process.env.NODE_ENV === 'production',
db: {
url: process.env.DATABASE_URL
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
},
cors: {
origin: process.env.CORS_ORIGIN || 'http://localhost:5173'
}
};
// --- server.js ---
const config = require('./config');
const express = require('express');
const app = express();
console.log(`Starting in ${config.nodeEnv} mode`);
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
A centralized config module validates all required variables at startup and exports a clean, structured object. If DATABASE_URL or JWT_SECRET is missing, the app exits immediately with a clear message instead of crashing later with a confusing error.
require('dotenv').config();
const express = require('express');
const app = express();
const isDev = process.env.NODE_ENV !== 'production';
// Development-only middleware
if (isDev) {
const morgan = require('morgan');
app.use(morgan('dev'));
console.log('Morgan logger enabled (development only)');
}
// Error handler shows stack traces only in development
app.use((err, req, res, next) => {
const status = err.status || 500;
const response = {
error: err.message || 'Internal Server Error'
};
// Include stack trace only in development
if (isDev) {
response.stack = err.stack;
}
res.status(status).json(response);
});
// CORS configuration based on environment
const cors = require('cors');
app.use(cors({
origin: isDev
? 'http://localhost:5173'
: process.env.CORS_ORIGIN
}));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV || 'development'} mode on port ${PORT}`);
});
Use NODE_ENV to conditionally enable features. Verbose logging and stack traces are helpful in development but should be hidden in production for security. CORS origins typically differ between environments.
dotenv, process.env, NODE_ENV, Configuration Patterns, Secrets Management, Environment Variables