Understanding Environment Variables in Express

A developer's guide to configuring applications across different environments

Understanding Environment Variables: The Foundation

Imagine you're a chef who works in multiple kitchens. Each kitchen has its own unique setup - different equipment locations, varying storage temperatures, distinct ingredient suppliers. You wouldn't want to memorize and hardcode all these details for each kitchen. Instead, you'd want a flexible system that adapts to whichever kitchen you're working in. This is exactly what environment variables do for your applications.

Environment variables are like a chef's ability to adapt recipes based on the kitchen they're working in. Just as a chef might need to adjust cooking temperatures or ingredient quantities based on their current kitchen's equipment, your application needs to adapt its configuration based on where it's running.

Understanding Different Environments

Let's extend our kitchen analogy to understand different environments in software development:

Development Environment (Your Home Kitchen)

This is where you experiment and develop new features. Like cooking at home, you can make mistakes here without consequences. You might use test data and simplified configurations.


// Example development environment configuration
const config = {
    port: process.env.PORT || 3000,
    database: process.env.DB_URL || 'mongodb://localhost:27017/dev_db',
    logLevel: process.env.LOG_LEVEL || 'debug',
    apiKeys: {
        thirdPartyService: process.env.API_KEY || 'test_key'
    }
};
            

Testing Environment (Test Kitchen)

This environment mimics production but is isolated for testing. Like a test kitchen where chefs validate new recipes, this is where you verify your application works as expected.


// Testing environment specific configurations
if (process.env.NODE_ENV === 'testing') {
    config.database = 'mongodb://localhost:27017/test_db';
    config.logLevel = 'verbose';
    config.mockExternalServices = true;
}
            

Staging Environment (Restaurant Kitchen During Training)

A mirror of production, but not serving real customers. This is where final validation happens.

Production Environment (Active Restaurant Kitchen)

The real deal - serving actual users. Like a busy restaurant kitchen during service, everything needs to work perfectly here.

Implementing Environment Variables

Let's look at how to properly implement environment variables in your Express application:

Basic Setup with dotenv


// First, install dotenv
// npm install dotenv

// At the very top of your app.js or index.js
require('dotenv').config();

// Create a .env file in your project root
// .env
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_secret_key
NODE_ENV=development

// Create a .env.example file (safe to commit to version control)
// .env.example
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_api_key_here
NODE_ENV=development

// Using environment variables in your application
const express = require('express');
const app = express();

const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;

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

Configuration Management

It's often helpful to centralize your environment variable handling in a configuration file:


// config/index.js
const config = {
    app: {
        port: process.env.PORT || 3000,
        env: process.env.NODE_ENV || 'development'
    },
    db: {
        url: process.env.DATABASE_URL,
        options: {
            useNewUrlParser: true,
            useUnifiedTopology: true
        }
    },
    auth: {
        jwtSecret: process.env.JWT_SECRET,
        expiresIn: process.env.JWT_EXPIRES_IN || '1d'
    },
    email: {
        host: process.env.EMAIL_HOST,
        port: process.env.EMAIL_PORT,
        user: process.env.EMAIL_USER,
        password: process.env.EMAIL_PASSWORD
    }
};

// Validate required environment variables
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
        throw new Error(`Missing required environment variable: ${envVar}`);
    }
}

module.exports = config;
            

Security Best Practices

Handling environment variables securely is crucial. Here are essential practices:


// NEVER commit .env files to version control
// .gitignore
.env
.env.*
!.env.example

// NEVER log sensitive environment variables
// BAD
console.log('Database URL:', process.env.DATABASE_URL);

// GOOD - Log non-sensitive configuration
console.log('Server running in', process.env.NODE_ENV, 'mode');

// Use environment-specific validation
const validateConfig = () => {
    if (process.env.NODE_ENV === 'production') {
        if (!process.env.SSL_CERT_PATH) {
            throw new Error('SSL certificate path required in production');
        }
    }
};
            

Practical Environment Variable Patterns

Feature Flags


// Using environment variables for feature flags
const featureFlags = {
    newUserInterface: process.env.ENABLE_NEW_UI === 'true',
    betaFeatures: process.env.ENABLE_BETA_FEATURES === 'true',
    maintenanceMode: process.env.MAINTENANCE_MODE === 'true'
};

app.use((req, res, next) => {
    // Attach feature flags to request object
    req.features = featureFlags;
    next();
});

app.get('/dashboard', (req, res) => {
    res.render('dashboard', {
        useNewUI: req.features.newUserInterface,
        showBetaFeatures: req.features.betaFeatures
    });
});
            

Environment-Specific Middleware


// Development-only middleware
if (process.env.NODE_ENV === 'development') {
    app.use((req, res, next) => {
        console.log(`${req.method} ${req.path}`);
        next();
    });
}

// Production-only middleware
if (process.env.NODE_ENV === 'production') {
    app.use(helmet());
    app.use(compression());
}
            

Database Configuration


const getDatabaseConfig = () => {
    const base = {
        url: process.env.DATABASE_URL,
        options: {
            useNewUrlParser: true,
            useUnifiedTopology: true
        }
    };

    switch (process.env.NODE_ENV) {
        case 'production':
            return {
                ...base,
                options: {
                    ...base.options,
                    ssl: true,
                    replicaSet: process.env.DB_REPLICA_SET,
                    authSource: 'admin'
                }
            };
        case 'testing':
            return {
                url: 'mongodb://localhost:27017/test_db',
                options: {
                    ...base.options
                }
            };
        default:
            return base;
    }
};
            

Deployment and Environment Variable Management

Different deployment platforms handle environment variables differently:

Heroku Example


// Setting environment variables in Heroku
heroku config:set DATABASE_URL=mongodb://...
heroku config:set NODE_ENV=production
heroku config:set JWT_SECRET=your_secret_here

// Reading in your application
const dbUrl = process.env.DATABASE_URL;
            

Docker Example


// Dockerfile
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]

// docker-compose.yml
version: '3'
services:
  web:
    build: .
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DATABASE_URL=mongodb://db:27017/myapp
    ports:
      - "3000:3000"
            

Testing with Environment Variables

Testing applications that use environment variables requires special consideration:


// test/setup.js
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'mongodb://localhost:27017/test_db';
process.env.JWT_SECRET = 'test_secret';

// test/api.test.js
describe('API Tests', () => {
    const originalEnv = process.env;

    beforeEach(() => {
        jest.resetModules();
        process.env = { ...originalEnv };
    });

    afterEach(() => {
        process.env = originalEnv;
    });

    it('should use test database', () => {
        const config = require('../config');
        expect(config.db.url).toBe('mongodb://localhost:27017/test_db');
    });

    it('should handle missing environment variables', () => {
        delete process.env.DATABASE_URL;
        expect(() => {
            require('../config');
        }).toThrow('Missing required environment variable: DATABASE_URL');
    });
});