Environment Variables and Configuration
What Are Environment Variables?
Environment variables are key-value pairs set outside your application code that control how it behaves. They are a standard mechanism for:
- Secrets — database passwords, API (Application Programming Interface) keys, JWT (JSON Web Token) secrets
- Configuration — server port, database name, feature flags
- Environment-specific values — different URLs for development, staging, and production
The key principle: code that changes between environments should live in environment variables, not in your source code.
Why Hard-Coding Secrets Is Dangerous
Consider this code:
// NEVER do this
mongoose.connect('mongodb+srv://admin:SuperSecret123@cluster.mongodb.net/mydb');
If this code is pushed to GitHub (even a private repository), anyone with access can see your credentials. Password rotation requires changing code. Running the same code in multiple environments is impossible.
The .env File and dotenv
The dotenv package loads a .env file into process.env when your app starts. This is the standard approach for local development.
npm install dotenv
Create a .env file in your project root:
PORT=5000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/mydb
JWT_SECRET=my_local_jwt_secret_change_in_production
CLOUDINARY_CLOUD_NAME=mycloud
CLOUDINARY_API_KEY=123456789
CLOUDINARY_API_SECRET=abc123xyz
Load it at the very top of your entry file, before any other code:
// server.js or app.js -- FIRST LINE
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGODB_URI);
const app = express();
const PORT = process.env.PORT || 5000;
app.listen(PORT);
Critical: Never Commit .env
Add .env to your .gitignore file immediately:
# .gitignore
node_modules/
.env
.env.local
.env.production
dist/
Instead, provide a .env.example file with all the keys but no real values:
# .env.example -- safe to commit
PORT=5000
NODE_ENV=development
MONGODB_URI=
JWT_SECRET=
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
New team members copy this file, fill in real values, and are up and running quickly.
Development vs Staging vs Production
Most professional projects have three environments:
| Environment | Purpose | Who uses it |
|---|---|---|
| Development | Local coding and experimentation | Individual developer |
| Staging | Testing in a production-like setting | QA team, product team |
| Production | Real users | Customers |
Each environment has its own .env values. The production database must never be touched by development code.
A common pattern is to use separate .env files and load the right one based on NODE_ENV:
const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env';
require('dotenv').config({ path: envFile });
Centralising Config
Instead of scattering process.env calls throughout your codebase, create a single config.js file that validates and exports all configuration:
// config/index.js
require('dotenv').config();
const required = ['MONGODB_URI', 'JWT_SECRET'];
required.forEach((key) => {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
});
module.exports = {
port: parseInt(process.env.PORT) || 5000,
nodeEnv: process.env.NODE_ENV || 'development',
mongoUri: process.env.MONGODB_URI,
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
cloudinary: {
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
apiKey: process.env.CLOUDINARY_API_KEY,
apiSecret: process.env.CLOUDINARY_API_SECRET,
},
};
Import and use:
const config = require('./config');
mongoose.connect(config.mongoUri);
app.listen(config.port);
This pattern:
- Validates required variables on startup (fast fail)
- Provides a single source of truth for all config
- Makes testing easier (you can mock the config module)
Secrets Management in Production
On Heroku: use heroku config:set.
On AWS (Amazon Web Services): use Secrets Manager or Parameter Store.
On Vercel: use the environment variables dashboard.
On your own server: use systemd environment files or Docker secrets.
Never store production secrets in git, even in private repositories.
Practice Exercise
- Add
dotenvto your Express project and create a.envfile with PORT, MONGODB_URI, and JWT_SECRET. - Add
.envto.gitignore. - Create a
.env.examplewith all keys but empty values. - Create a
config/index.jsthat validates required variables and exports a config object. - Replace all direct
process.envcalls in your routes with imports fromconfig.
Try it yourself
Key Takeaways
- Environment variables keep secrets and environment-specific configuration out of your source code.
- Use the dotenv package locally to load a .env file into process.env at startup.
- Always add .env to .gitignore and provide a .env.example with keys but no values for teammates.
- A centralised config/index.js validates required variables on startup and exports a typed config object.
- In production, set environment variables through the platform (Heroku config:set, Vercel dashboard, AWS Secrets Manager) — never commit them.
Quick Quiz
1.Why should you never commit a .env file to git?
2.What is the purpose of a .env.example file?
3.What does the centralised config/index.js pattern (validating process.env on startup) achieve?
4.Which of the following is a correct call to require('dotenv').config()?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx