Chapter 4.4☕ 18 min read

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.

01What is .env? — Swiggy Kitchen Ke Secret Recipes

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 VariableWhy Keep Secret?
Database URIMONGO_URIGives access to your database
JWT SecretJWT_SECRETAllows forging fake authentication tokens
SMTP PasswordEMAIL_PASSWORDLets anyone send emails as you
API KeysSTRIPE_KEYCharges to your account
Port NumberPORTNot 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!

02dotenv Package — .env File Ko Load Karna

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.env is 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
03.gitignore — .env Ko GitHub Par Jaane Se Rokna

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:

  1. Create .env.example with placeholder values (commit this to Git)
  2. Add .env to .gitignore (never commit)
  3. Each developer copies .env.example → .env and fills their own values
  4. 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 you
04Config Management — Organizing All Config in One Place

Instead 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
05Multiple Environments — Development vs Production 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:

PlatformHow to Set Env Vars
RailwayDashboard → Variables
RenderDashboard → Environment → Environment Variables
AWS Elastic BeanstalkConfiguration → Software → Environment Properties
VercelProject Settings → Environment Variables
HerokuSettings → 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
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase