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"