Loading Environment Variables in Node.js

A comprehensive guide to managing application configuration

Understanding Environment Variable Loading

Imagine you're setting up a new smartphone. When you first turn it on, it needs to know certain things: your language preference, time zone, wifi settings, and account details. These settings determine how your phone behaves in different situations. Environment variables work similarly for our applications - they're the initial setup that tells our application how to behave in different contexts.

Just as we wouldn't want to hardcode our phone number into every app we create, we don't want to hardcode configuration values into our application code. Instead, we load these values from the environment, allowing our application to adapt to different situations without changing its code.

Manual Environment Variable Loading

Let's start with the most basic way to load environment variables. Think of this like manually adjusting settings on your device. We have several ways to do this:

Command Line Loading

This is like temporarily changing a setting for a single session. Here's how it works:


// Running your application with environment variables
PORT=3000 NODE_ENV=development node app.js

// In your application code
const port = process.env.PORT;
console.log(`Server starting on port ${port}`);

// You can even combine multiple variables
PORT=3000 DB_HOST=localhost DB_PASSWORD=secret node app.js
            

While this method works, it's like having to manually enter your wifi password every time you turn on your phone - it gets tedious quickly. Let's look at more sustainable approaches.

NPM Scripts Method

This approach is like saving a preset configuration. We can define our environment variables in our package.json:


{
    "scripts": {
        "start": "PORT=3000 NODE_ENV=development node app.js",
        "start:prod": "PORT=80 NODE_ENV=production node app.js",
        "start:test": "PORT=3001 NODE_ENV=testing node app.js"
    }
}

// Now we can run our application with different configurations
npm run start        // Development settings
npm run start:prod   // Production settings
npm run start:test   // Testing settings
            

This is better, but still not ideal for managing many variables or sensitive information. Let's explore a more robust solution.

The .env File Approach

Think of a .env file as a configuration profile for your application - like having a settings file that stores all your preferences in one place. Here's how to set it up:

Creating Your .env File


# .env file
PORT=3000
NODE_ENV=development
DB_HOST=localhost
DB_USER=myuser
DB_PASSWORD=mypassword
API_KEY=your_secret_key_here

# Notice the format:
# - No spaces around the equals sign
# - No quotes needed unless your value contains spaces
# - One variable per line
# - Comments start with #
            

Setting Up dotenv

The dotenv package acts like an interpreter between your .env file and your application. Here's how to implement it:


// First, install dotenv
// npm install dotenv

// At the very top of your application (before other imports)
require('dotenv').config();

// Now you can use environment variables anywhere in your application
const express = require('express');
const app = express();

// Environment variables are accessible via process.env
const port = process.env.PORT || 3000;
const dbConfig = {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD
};

app.listen(port, () => {
    console.log(`Server running on port ${port} in ${process.env.NODE_ENV} mode`);
});
            

Environment Variable Best Practices

Just as you wouldn't share your phone's unlock code with everyone, there are important security practices to follow with environment variables:

Protecting Sensitive Information


# .gitignore
.env
.env.local
.env.*
!.env.example

# .env.example - Safe to commit
PORT=3000
DB_HOST=your_database_host
DB_USER=your_database_user
DB_PASSWORD=your_database_password
API_KEY=your_api_key_here
            

Validation and Defaults

Always validate your environment variables and provide sensible defaults where appropriate:


// config/environment.js
const requiredEnvVars = ['DB_HOST', 'DB_USER', 'DB_PASSWORD'];

function validateEnv() {
    const missing = requiredEnvVars.filter(env => !process.env[env]);
    
    if (missing.length > 0) {
        throw new Error(
            `Missing required environment variables: ${missing.join(', ')}`
        );
    }
}

// Configuration with defaults
const config = {
    port: process.env.PORT || 3000,
    nodeEnv: process.env.NODE_ENV || 'development',
    db: {
        host: process.env.DB_HOST,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD,
        // Provide defaults for optional configuration
        port: process.env.DB_PORT || 5432,
        ssl: process.env.DB_SSL === 'true'
    }
};

validateEnv();
module.exports = config;
            

Advanced Environment Variable Patterns

Environment-Specific Configuration Files

Sometimes you need different settings for different environments. Here's how to manage that:


// config/environments/
// ├── development.env
// ├── testing.env
// └── production.env

// config/index.js
const path = require('path');
require('dotenv').config({
    path: path.join(__dirname, 
        `environments/${process.env.NODE_ENV || 'development'}.env`)
});

const config = {
    database: {
        url: process.env.DATABASE_URL,
        options: {
            ssl: process.env.NODE_ENV === 'production',
            logging: process.env.NODE_ENV !== 'production'
        }
    },
    server: {
        port: process.env.PORT || 3000,
        cors: {
            enabled: process.env.ENABLE_CORS === 'true',
            origins: process.env.CORS_ORIGINS?.split(',') || []
        }
    }
};

module.exports = config;
            

Runtime Environment Variable Updates

Sometimes you need to update environment variables during runtime:


// Utility function for safe environment variable updates
const updateEnvironmentVariable = (key, value) => {
    // Update process.env
    process.env[key] = value;
    
    // If using dotenv, you might want to update the .env file too
    const fs = require('fs');
    const envFile = '.env';
    const currentEnv = fs.readFileSync(envFile, 'utf8');
    const updatedEnv = currentEnv.replace(
        new RegExp(`^${key}=.*$`, 'm'),
        `${key}=${value}`
    );
    
    fs.writeFileSync(envFile, updatedEnv);
};

// Example usage
updateEnvironmentVariable('API_VERSION', '2.0');
            

Testing with Environment Variables

When testing applications that use environment variables, we need special considerations:


// test/setup.js
const path = require('path');
require('dotenv').config({
    path: path.join(__dirname, '../.env.test')
});

// test/database.test.js
describe('Database Connection', () => {
    const originalEnv = process.env;

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

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

    it('should connect with test credentials', async () => {
        // Test-specific environment setup
        process.env.DB_HOST = 'test-host';
        process.env.DB_USER = 'test-user';
        
        const db = require('../db');
        const connection = await db.connect();
        expect(connection.isConnected).toBe(true);
    });
});
            

Deployment Considerations

Different deployment platforms handle environment variables differently. Here's how to manage them:

Heroku Deployment


// Setting environment variables in Heroku
heroku config:set NODE_ENV=production
heroku config:set DB_HOST=your-production-host
heroku config:set API_KEY=your-production-api-key

// Reading them in your application remains the same
const apiKey = process.env.API_KEY;
            

Docker Deployment


// 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
    env_file:
      - .env.production
    ports:
      - "3000:3000"