Environment Variables — .env Secrets Management
Environment variables are like your Swiggy kitchen's secret recipes — you keep them hidden from everyone, especially from your Git repository.
Environment variables (env vars) are key-value pairs stored outside your code. They contain sensitive information like database passwords, API keys, JWT secrets, and SMTP credentials.
Why do we need them? Imagine your Swiggy kitchen's secret biryani recipe is written on the wall of your restaurant — everyone can see it! That's what happens when you put secrets in your code. Environment variables keep those secrets hidden.
What should go in .env?
| Secret | .env Variable | Why Keep Secret? |
|---|---|---|
| Database URI | MONGO_URI | Gives access to your database |
| JWT Secret | JWT_SECRET | Allows forging fake authentication tokens |
| SMTP Password | EMAIL_PASSWORD | Lets anyone send emails as you |
| API Keys | STRIPE_KEY | Charges to your account |
| Port Number | PORT | Not a secret, but environment-specific |
What is NOT a secret:
- App name, version — can be in package.json
- Feature flags — can have defaults
- Development-only config — can have fallbacks
The golden rule: If it can get your account hacked, database leaked, or auth bypassed — it belongs in .env. Never in your source code!
dotenv is a zero-dependency package that loads environment variables from a .env file into process.env. Think of it as the keymaster who unlocks your secret recipe book when your app starts.
npm install dotenv
Create a .env file:
# .env — DevInHyderabad Swiggy Backend Configuration
# 🔐 Yeh secrets hain! Kabhi GitHub par mat daalna!
# Server
PORT=3000
NODE_ENV=development
# MongoDB
MONGO_URI=mongodb://localhost:27017/dev-in-hyderabad
# JWT Authentication
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRES_IN=7d
# Email (Gmail SMTP)
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-16-char-app-password
# App URL (for email links)
APP_URL=http://localhost:3000
# File Upload
UPLOAD_DIR=uploads
MAX_FILE_SIZE=5242880
Loading .env in your app — TWO ways:
// METHOD 1: Import at the very top (recommended in ES Modules)
// server.js — Top of the file!
import 'dotenv/config';
// Now process.env has all your .env variables!
const PORT = process.env.PORT || 3000;
const MONGO_URI = process.env.MONGO_URI;
console.log('Server starting on port ' + PORT);
console.log('Environment: ' + process.env.NODE_ENV);
// METHOD 2: Call config() explicitly (CommonJS style)
// Works in both CommonJS and ES Modules
import dotenv from 'dotenv';
dotenv.config(); // Load .env file
// Access variables
console.log(process.env.JWT_SECRET); // "your-super-secret-jwt-key..."
Critical — dotenv must be loaded BEFORE you access any env variable!
// ❌ BAD: dotenv loaded AFTER using env vars
import mongoose from 'mongoose';
const dbUrl = process.env.MONGO_URI; // undefined!
// dotenv abhi load nahi hua!
import 'dotenv/config'; // Too late! Already accessed MONGO_URI!
// ✅ GOOD: dotenv loaded FIRST
import 'dotenv/config'; // Pehle load karo!
import mongoose from 'mongoose';
const dbUrl = process.env.MONGO_URI; // Works! 🎉
How process.env works:
process.envis a global object in Node.js — accessible everywhere- dotenv reads the .env file, parses it, and sets
process.env[KEY] = value - Values are always strings — convert numbers/booleans manually
- Existing env vars (set in terminal) are NOT overwritten by .env
Never. Commit. Your. .env. File. To. GitHub.
If your .env file goes to GitHub, anyone can read your database password, JWT secret, and API keys. Automated bots scan GitHub constantly for exposed secrets.
The .gitignore file:
# .gitignore — Never commit these
node_modules/
.env # 🔐 Secrets! Never commit!
.env.local
.env.production
uploads/ # User-uploaded files (optional)
dist/ # Build output
But what about other developers on my team? They need environment variables too! Create a .env.example file with placeholder values:
# .env.example — Copy this to .env and fill in your values!
# This file IS committed to GitHub (no secrets here)
# Server
PORT=3000
NODE_ENV=development
# MongoDB — Change this to your local MongoDB URL
MONGO_URI=mongodb://localhost:27017/myapp
# JWT — Generate a random secret
JWT_SECRET=your-secret-here
# Email — Get App Password from Google
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
How to share env vars with your team:
- Create
.env.examplewith placeholder values (commit this to Git) - Add
.envto.gitignore(never commit) - Each developer copies
.env.example → .envand fills their own values - In production, set env vars through your hosting dashboard (Railway, Vercel, AWS)
WHAT IF YOU ACCIDENTALLY COMMIT .ENV?
# 1. REMOVE it from Git tracking (but file stays on disk)
git rm --cached .env
# 2. Add to .gitignore immediately
echo '.env' >> .gitignore
# 3. CHANGE ALL YOUR PASSWORDS AND SECRETS!
# Assume they are compromised. Because they are.
# 4. For secrets already pushed to GitHub, rotate them NOW
# GitHub also has secret scanning that may alert youInstead of accessing process.env everywhere in your code, create a centralized config file. This makes your code cleaner, gives you a single place to manage defaults and validation, and makes testing easier.
// config/index.js — Centralized Configuration
import 'dotenv/config';
const config = {
// Server
port: parseInt(process.env.PORT) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
isProduction: process.env.NODE_ENV === 'production',
isDevelopment: process.env.NODE_ENV === 'development',
// MongoDB
mongo: {
uri: process.env.MONGO_URI || 'mongodb://localhost:27017/dev-in-hyderabad',
options: {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000
}
},
// JWT
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
get secretWarning() {
if (!this.secret || this.secret === 'your-secret-here') {
console.warn('⚠️ WARNING: JWT_SECRET is not set! Using INSECURE default!');
console.warn(' Set JWT_SECRET in your .env file!');
}
return this.secret;
}
},
// Email
email: {
user: process.env.EMAIL_USER,
password: process.env.EMAIL_PASSWORD,
host: process.env.EMAIL_HOST || 'smtp.gmail.com',
port: parseInt(process.env.EMAIL_PORT) || 587,
from: process.env.EMAIL_FROM || 'noreply@devinhyderabad.com'
},
// File Upload
upload: {
dir: process.env.UPLOAD_DIR || 'uploads',
maxFileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024,
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
},
// App
app: {
url: process.env.APP_URL || 'http://localhost:3000',
name: 'DevInHyderabad Swiggy',
version: '1.0.0'
}
};
// Validate required config on startup
const validateConfig = () => {
const required = [
{ key: 'MONGO_URI', value: config.mongo.uri, desc: 'Database URL' },
{ key: 'JWT_SECRET', value: config.jwt.secret, desc: 'JWT signing secret' },
];
let missing = false;
for (const { key, value, desc } of required) {
if (!value || value === 'your-secret-here') {
console.error('❌ MISSING REQUIRED ENV: ' + key + ' (' + desc + ')');
console.error(' Add it to your .env file!');
missing = true;
}
}
if (missing) {
console.error('\n⚠️ App may not work correctly without required env vars!');
}
};
validateConfig();
export default config;
Using config in your code:
// Instead of: process.env.MONGO_URI everywhere
// Use config!
import config from './config/index.js';
// Connect to MongoDB
await mongoose.connect(config.mongo.uri, config.mongo.options);
// Start server
app.listen(config.port, () => {
console.log('Server running in ' + config.nodeEnv + ' mode on port ' + config.port);
});
// Check environment
if (config.isProduction) {
console.log('🔥 Production mode! Running at ' + config.app.url);
} else {
console.log('🧪 Development mode at http://localhost:' + config.port);
}
Benefits of centralized config:
- Single source of truth — All config in one place
- Type conversion — parseInt(), JSON.parse() once, not everywhere
- Validation — Check required vars on startup, fail early
- Defaults — Sensible fallbacks for development
- Documentation — One file shows ALL available config
Different environments need different configurations. Development uses local MongoDB, production uses MongoDB Atlas. Development shows debug logs, production doesn't.
Multiple .env files pattern:
# .env.development — Development settings
PORT=3000
MONGO_URI=mongodb://localhost:27017/dev-in-hyderabad
NODE_ENV=development
LOG_LEVEL=debug
# .env.production — Production settings
PORT=8080
MONGO_URI=mongodb+srv://user:pass@cluster.mongodb.net/app
NODE_ENV=production
LOG_LEVEL=info
Loading environment-specific .env:
// config/index.js — Load environment-specific .env
import dotenv from 'dotenv';
import path from 'path';
// Determine which .env file to load
const envFile = process.env.NODE_ENV === 'production'
? '.env.production'
: '.env.development';
// Load environment-specific file
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
// Also load .env (it overrides if present)
dotenv.config(); // .env takes precedence
const config = {
port: parseInt(process.env.PORT) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
// ...rest of config
};
Package.json scripts for different environments:
{
"scripts": {
"start": "node server.js",
"dev": "NODE_ENV=development nodemon server.js",
"prod": "NODE_ENV=production node server.js"
}
}
Production best practices:
- Never use .env files in production! Set env vars through your platform's dashboard (Railway, Render, AWS, Vercel, etc.)
- Use secrets management services like HashiCorp Vault, AWS Secrets Manager, or Doppler for production secrets
- Rotate secrets regularly — Change JWT_SECRET and API keys every 90 days
- Use different secrets for each environment — Dev secrets != Prod secrets
Setting env vars on popular platforms:
| Platform | How to Set Env Vars |
|---|---|
| Railway | Dashboard → Variables |
| Render | Dashboard → Environment → Environment Variables |
| AWS Elastic Beanstalk | Configuration → Software → Environment Properties |
| Vercel | Project Settings → Environment Variables |
| Heroku | Settings → Config Vars |
Key Takeaways
- ✅ Environment variables keep secrets OUT of your code — DB URIs, JWT secrets, API keys go in .env
- ✅ dotenv loads .env into process.env — import at the VERY TOP of your entry file
- ✅ NEVER commit .env to Git — add it to .gitignore. Create .env.example instead for teammates.
- ✅ Centralize config in config/index.js — type conversions, validation, and defaults in one place
- ✅ Use environment-specific .env files: .env.development, .env.production
- ✅ In production, set env vars through hosting platform dashboard, not .env files
- ✅ Always validate required env vars on startup — fail fast if secrets are missing
- ✅ process.env values are always strings — use parseInt() for numbers, JSON.parse() for objects
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login