Express.js Environment Variables
Environment variables store configuration values outside your code. Database passwords, API keys, port numbers, and secret tokens all belong in environment variables rather than hardcoded in your source files. This keeps sensitive data off version control, makes your app run differently in development versus production, and follows security best practices used by professional teams everywhere.
Why Hardcoding Is Dangerous
Imagine writing your house key number directly on the front door. Anyone who sees the door gets the key. Hardcoding a database password in your code does the same thing — anyone who reads your code on GitHub sees your credentials.
BAD — password visible in code:
───────────────────────────────
mongoose.connect('mongodb+srv://admin:MySecret123@cluster.mongodb.net/app');
// Anyone who clones this repo sees "MySecret123"
GOOD — password stored in environment:
───────────────────────────────────────
mongoose.connect(process.env.MONGODB_URI);
// Code shows nothing sensitive
How Environment Variables Work in Node.js
Node.js exposes all environment variables through the global process.env object. Every key-value pair your operating system or deployment platform defines shows up there.
Operating System / Deployment Platform
│
│ sets PORT=3000
│ NODE_ENV=production
│ MONGODB_URI=mongodb+srv://...
│
▼
process.env
│
┌───────┴────────────────────────────┐
│ process.env.PORT = '3000' │
│ process.env.NODE_ENV = 'prod...'│
│ process.env.MONGODB_URI = 'mongo.'│
└────────────────────────────────────┘
│
▼
Your Express app reads them
The .env File and dotenv Package
During development, you set environment variables in a .env file. The dotenv package reads this file and loads the variables into process.env automatically when your app starts.
Install dotenv:
npm install dotenv
Create a .env file in your project root:
PORT=3000 NODE_ENV=development MONGODB_URI=mongodb+srv://myuser:mypassword@cluster.mongodb.net/myapp JWT_SECRET=a_very_long_random_string_nobody_can_guess ALLOWED_ORIGIN=http://localhost:5173
Load dotenv at the very top of app.js, before anything else:
require('dotenv').config(); // Must be the first line
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const PORT = process.env.PORT || 3000;
mongoose.connect(process.env.MONGODB_URI);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Always Add .env to .gitignore
The .env file contains secrets. It must never be committed to version control. Add it to .gitignore immediately when you create it:
# .gitignore node_modules/ .env
Instead, provide a .env.example file with the same keys but empty or placeholder values. Teammates copy this file and fill in their own values:
# .env.example — safe to commit, no real secrets PORT=3000 NODE_ENV=development MONGODB_URI= JWT_SECRET= ALLOWED_ORIGIN=
Common Environment Variables in Express Apps
┌───────────────────────────────────────────────────────────────┐ │ Standard Environment Variables │ ├─────────────────────┬─────────────────────────────────────────┤ │ PORT │ Port the server listens on │ │ NODE_ENV │ 'development', 'production', 'test' │ │ MONGODB_URI │ Database connection string │ │ JWT_SECRET │ Secret key for signing tokens │ │ SESSION_SECRET │ Secret key for sessions │ │ SMTP_HOST │ Email service host │ │ SMTP_USER │ Email service username │ │ SMTP_PASS │ Email service password │ │ ALLOWED_ORIGIN │ Frontend URL for CORS │ │ API_KEY │ Third-party service API key │ └─────────────────────┴─────────────────────────────────────────┘
Using NODE_ENV to Change Behavior
The NODE_ENV variable signals which environment your app runs in. Use it to switch between development and production behaviors:
const isDev = process.env.NODE_ENV === 'development';
// Show detailed error stack only in development
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({
success: false,
message: err.message,
stack: isDev ? err.stack : undefined // Hide stack in production
});
});
// Log every request only in development
if (isDev) {
const morgan = require('morgan');
app.use(morgan('dev'));
}
Default Values with the OR Operator
Always provide fallback values when an environment variable might be missing. Use the || operator:
const PORT = process.env.PORT || 3000; const DB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp'; const JWT_EXPIRY = process.env.JWT_EXPIRY || '7d';
This prevents crashes if someone forgets to set a variable. Provide sensible defaults for non-sensitive values; throw an error for required secrets that have no safe default.
Validate Required Variables at Startup
Check that critical variables exist before the server starts. Fail immediately with a clear message rather than crashing later with a confusing error:
// config/validateEnv.js
const required = ['MONGODB_URI', 'JWT_SECRET', 'PORT'];
const validateEnv = () => {
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('ERROR: Missing required environment variables:');
missing.forEach(key => console.error(` - ${key}`));
process.exit(1); // Stop the server before it starts badly
}
console.log('Environment variables loaded successfully');
};
module.exports = validateEnv;
// app.js
require('dotenv').config();
const validateEnv = require('./config/validateEnv');
validateEnv(); // Run before anything else
const express = require('express');
// ... rest of app
Centralized Config Object
Create a single config file that reads all environment variables in one place. The rest of your application imports from this file rather than reading process.env directly everywhere:
// config/config.js
module.exports = {
port: parseInt(process.env.PORT) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
mongoUri: process.env.MONGODB_URI,
jwtSecret: process.env.JWT_SECRET,
jwtExpiry: process.env.JWT_EXPIRY || '7d',
allowedOrigin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173',
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production'
};
// Use anywhere in your app
const config = require('./config/config');
mongoose.connect(config.mongoUri);
app.listen(config.port);
if (config.isDevelopment) {
app.use(morgan('dev'));
}
Environment Variables in Production (Cloud Platforms)
On cloud platforms, you never upload a .env file. Instead, each platform provides its own way to set environment variables through a dashboard or CLI:
┌───────────────────────────────────────────────────────────────┐ │ Setting Env Variables on Cloud Platforms │ ├──────────────────┬────────────────────────────────────────────┤ │ Heroku │ heroku config:set MONGODB_URI=... │ │ Railway │ Settings → Variables → Add Variable │ │ Render │ Environment → Add Environment Variable │ │ Vercel │ Project Settings → Environment Variables │ │ AWS EC2 │ Set via system environment or .env on disk │ │ Docker │ ENV in Dockerfile or --env-file flag │ └──────────────────┴────────────────────────────────────────────┘
Summary
Environment variables store configuration and secrets outside your source code. Use a .env file during development with the dotenv package to load values into process.env. Always add .env to .gitignore and provide a .env.example for teammates. Provide default values with the || operator for non-critical variables and validate required variables at startup before the server begins accepting requests. Centralize all environment variable access in a single config module to keep the rest of your code clean and testable. On cloud platforms, configure environment variables through the platform's dashboard or CLI instead of uploading a .env file.
