javascript-today

Environment Variables

Environment variables allow you to configure your application differently for development, testing, and production without changing code. They’re essential for keeping secrets safe.

What are Environment Variables?

Variables set outside your code that your application can access:

// Access environment variables
console.log(process.env.NODE_ENV);  // "development" or "production"
console.log(process.env.PORT);      // "3000"
console.log(process.env.DATABASE_URL);

Common uses:

  • API keys and secrets
  • Database connection strings
  • Port numbers
  • Environment mode (dev/prod)
  • Feature flags

Using process.env

Built into Node.js:

const express = require('express');
const app = express();

// Use environment variable with fallback
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`Environment: ${NODE_ENV}`);
});

Setting Environment Variables

Command Line (Temporary)

Linux/Mac:

PORT=4000 node server.js
NODE_ENV=production PORT=8080 node server.js

Windows (CMD):

set PORT=4000
node server.js

Windows (PowerShell):

$env:PORT=4000
node server.js

package.json Scripts

{
  "scripts": {
    "start": "node server.js",
    "dev": "NODE_ENV=development nodemon server.js",
    "prod": "NODE_ENV=production node server.js"
  }
}

Cross-platform with cross-env:

npm install cross-env --save-dev
{
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server.js",
    "prod": "cross-env NODE_ENV=production node server.js"
  }
}

Using .env Files

Install dotenv

npm install dotenv

Create .env File

Create .env in your project root:

# .env
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_secret_api_key_here
JWT_SECRET=super_secret_key
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587

Important: Add .env to .gitignore!

# .gitignore
.env
node_modules/

Load .env in Your App

// Load .env file (at the very top of your main file)
require('dotenv').config();

const express = require('express');
const app = express();

// Now use environment variables
const PORT = process.env.PORT || 3000;
const DATABASE_URL = process.env.DATABASE_URL;
const API_KEY = process.env.API_KEY;

console.log('Database:', DATABASE_URL);
console.log('API Key:', API_KEY ? '***' : 'Not set');

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

ES Modules

// server.js (ES modules)
import 'dotenv/config';
import express from 'express';

const app = express();
const PORT = process.env.PORT || 3000;

app.listen(PORT);

Different Environments

Multiple .env Files

Common pattern:

.env              # Local development (gitignored)
.env.example      # Template (committed to git)
.env.test         # Testing environment
.env.production   # Production (never commit!)

.env.example (commit this):

PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_api_key_here
JWT_SECRET=your_jwt_secret

Loading Specific .env Files

// Load different file based on NODE_ENV
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`
});

// Or specify path directly
require('dotenv').config({ path: '.env.production' });

Environment-Specific Configuration

const config = {
  development: {
    port: 3000,
    database: 'mongodb://localhost:27017/dev',
    logLevel: 'debug'
  },
  production: {
    port: process.env.PORT,
    database: process.env.DATABASE_URL,
    logLevel: 'error'
  },
  test: {
    port: 3001,
    database: 'mongodb://localhost:27017/test',
    logLevel: 'silent'
  }
};

const env = process.env.NODE_ENV || 'development';
const currentConfig = config[env];

module.exports = currentConfig;

Practical Examples

Database Connection

require('dotenv').config();

const DATABASE_URL = process.env.DATABASE_URL;

if (!DATABASE_URL) {
  console.error('DATABASE_URL not set!');
  process.exit(1);
}

// Use in connection
connectToDatabase(DATABASE_URL);

API Keys

require('dotenv').config();

const express = require('express');
const app = express();

const API_KEY = process.env.API_KEY;

// Middleware to check API key
function requireApiKey(req, res, next) {
  const key = req.headers['x-api-key'];
  
  if (key === API_KEY) {
    next();
  } else {
    res.status(401).json({ error: 'Invalid API key' });
  }
}

app.get('/api/data', requireApiKey, (req, res) => {
  res.json({ data: 'Secret data' });
});

Email Configuration

require('dotenv').config();

const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: process.env.SMTP_PORT,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASSWORD
  }
});

async function sendEmail(to, subject, text) {
  await transporter.sendMail({
    from: process.env.EMAIL_FROM,
    to,
    subject,
    text
  });
}

Feature Flags

require('dotenv').config();

const ENABLE_ANALYTICS = process.env.ENABLE_ANALYTICS === 'true';
const ENABLE_PAYMENTS = process.env.ENABLE_PAYMENTS === 'true';

app.get('/api/config', (req, res) => {
  res.json({
    features: {
      analytics: ENABLE_ANALYTICS,
      payments: ENABLE_PAYMENTS
    }
  });
});

// Conditional middleware
if (ENABLE_ANALYTICS) {
  app.use(analyticsMiddleware);
}

Configuration Module Pattern

Create a dedicated config file:

config.js:

require('dotenv').config();

function required(name) {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Environment variable ${name} is required`);
  }
  return value;
}

function optional(name, defaultValue) {
  return process.env[name] || defaultValue;
}

module.exports = {
  // Server
  port: optional('PORT', 3000),
  nodeEnv: optional('NODE_ENV', 'development'),
  
  // Database
  databaseUrl: required('DATABASE_URL'),
  
  // Security
  jwtSecret: required('JWT_SECRET'),
  apiKey: required('API_KEY'),
  
  // Email
  smtp: {
    host: required('SMTP_HOST'),
    port: required('SMTP_PORT'),
    user: required('SMTP_USER'),
    password: required('SMTP_PASSWORD')
  },
  
  // Features
  enableAnalytics: optional('ENABLE_ANALYTICS', 'false') === 'true',
  
  // Helper
  isDevelopment: function() {
    return this.nodeEnv === 'development';
  },
  
  isProduction: function() {
    return this.nodeEnv === 'production';
  }
};

server.js:

const config = require('./config');
const express = require('express');
const app = express();

console.log('Environment:', config.nodeEnv);
console.log('Analytics enabled:', config.enableAnalytics);

app.listen(config.port, () => {
  console.log(`Server running on port ${config.port}`);
});

Validation

Validate environment variables at startup:

require('dotenv').config();

const requiredEnvVars = [
  'DATABASE_URL',
  'JWT_SECRET',
  'API_KEY'
];

const missingVars = requiredEnvVars.filter(
  varName => !process.env[varName]
);

if (missingVars.length > 0) {
  console.error('Missing required environment variables:');
  missingVars.forEach(varName => {
    console.error(`  - ${varName}`);
  });
  process.exit(1);
}

// Continue with app...

Type Conversion

Environment variables are always strings:

// Wrong - this is a string "3000", not a number
const PORT = process.env.PORT;

// Correct - convert to number
const PORT = parseInt(process.env.PORT, 10) || 3000;
const PORT = Number(process.env.PORT) || 3000;

// Boolean conversion
const ENABLE_FEATURE = process.env.ENABLE_FEATURE === 'true';

// Array conversion
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [];
// ALLOWED_ORIGINS=http://localhost:3000,https://example.com

Security Best Practices

Never Commit Secrets

# .gitignore
.env
.env.local
.env.*.local
.env.production

Use Different Secrets Per Environment

# Development
JWT_SECRET=dev_secret_change_in_production

# Production
JWT_SECRET=P7$mK9#xL2@nQ4&vB8!jR3^wT6

Minimal Permissions

Only give your app the environment variables it needs:

// Good - specific variables
const { DATABASE_URL, API_KEY } = process.env;

// Avoid - exposing entire process.env
app.get('/debug', (req, res) => {
  res.json(process.env); // ❌ Exposes all secrets!
});

Rotate Secrets Regularly

Change API keys, database passwords, and JWT secrets periodically.

Production Deployment

Hosting Platforms

Most platforms provide environment variable management:

Heroku:

heroku config:set DATABASE_URL=postgresql://...
heroku config:set API_KEY=secret

Vercel/Netlify: Set in dashboard UI or CLI

Docker:

docker run -e DATABASE_URL=mongodb://... myapp

docker-compose.yml:

version: '3'
services:
  app:
    environment:
      - DATABASE_URL=mongodb://mongo:27017/app
      - NODE_ENV=production

Don’t Use .env in Production

Instead, set environment variables through:

  • Platform UI/CLI
  • CI/CD pipeline
  • Container orchestration
  • Secret management services (AWS Secrets Manager, etc.)

Complete Example

.env:

PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=dev_secret_key
CORS_ORIGIN=http://localhost:3000
RATE_LIMIT_MAX=100
LOG_LEVEL=debug

config.js:

require('dotenv').config();

module.exports = {
  port: parseInt(process.env.PORT, 10) || 3000,
  env: process.env.NODE_ENV || 'development',
  database: {
    url: process.env.DATABASE_URL
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: '7d'
  },
  cors: {
    origin: process.env.CORS_ORIGIN
  },
  rateLimit: {
    max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
    windowMs: 15 * 60 * 1000 // 15 minutes
  },
  logging: {
    level: process.env.LOG_LEVEL || 'info'
  }
};

server.js:

const config = require('./config');
const express = require('express');
const app = express();

// Use config throughout app
app.use(express.json());

app.get('/api/status', (req, res) => {
  res.json({
    status: 'ok',
    environment: config.env
  });
});

app.listen(config.port, () => {
  console.log(`Server running on port ${config.port}`);
  console.log(`Environment: ${config.env}`);
});

Best Practices

DO:

  • Use .env files for local development
  • Add .env to .gitignore
  • Commit .env.example as a template
  • Validate required variables at startup
  • Convert string values to correct types
  • Use a config module for organization

DON’T:

  • Commit .env files with secrets
  • Use .env files in production (use platform features)
  • Expose process.env in API responses
  • Hard-code secrets in your code
  • Forget to document required variables

Summary

Task Code
Load .env file require('dotenv').config()
Access variable process.env.VARIABLE_NAME
With fallback process.env.PORT || 3000
Convert to number parseInt(process.env.PORT, 10)
Convert to boolean process.env.VAR === 'true'
Check if exists if (!process.env.VAR) { ... }