Managing .env and Environment Variables in Node.js
THEJORD Team••1 min read
nodejsenvconfigurationsecurity
How to use environment variables in Node.js: dotenv, .env files, security. Best practices.
Introduction to Environment Variables
Environment variables are key-value pairs that configure your application's behavior without hardcoding values in your source code. They're essential for managing secrets, API keys, and environment-specific configuration. This guide covers best practices for using .env files and environment variables in Node.js applications.
Why Use Environment Variables?
Security
- Keep secrets out of source code
- Never commit API keys to Git
- Different credentials per environment
Flexibility
- Same code in dev, staging, production
- Easy configuration changes without deployment
- Container and cloud-native deployment
The .env File
Basic Syntax
# .env file
# Comments start with #
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
API_KEY=sk-abc123def456
PORT=3000
DEBUG=true
NODE_ENV=development
# Strings with spaces need quotes
APP_NAME="My Application"
# Multi-line values
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
File Naming Conventions
.env # Default, loaded in all environments
.env.local # Local overrides (git-ignored)
.env.development # Development-specific
.env.production # Production-specific
.env.test # Test-specific
.env.example # Template for required variables
Using dotenv in Node.js
Installation
npm install dotenv
Basic Usage
// Load at the top of your entry file
import 'dotenv/config';
// or
require('dotenv').config();
// Access variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
Conditional Loading
import dotenv from 'dotenv';
// Load different files based on NODE_ENV
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
dotenv.config({ path: envFile });
// Override with local file
dotenv.config({ path: '.env.local', override: true });
Type Safety with TypeScript
Type Definitions
// types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
PORT: string;
DATABASE_URL: string;
API_KEY: string;
}
}
Validation with Zod
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
});
// Validate and parse
const env = envSchema.parse(process.env);
// Use typed env
console.log(env.PORT); // number
console.log(env.DATABASE_URL); // string
Framework Integration
Next.js
// .env.local (automatically loaded)
DATABASE_URL=postgres://...
// Public variables (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
// Usage in code
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // Client-safe
const dbUrl = process.env.DATABASE_URL; // Server-only
Vite
// .env
VITE_API_URL=https://api.example.com
// Usage (only VITE_ prefixed vars are exposed)
const apiUrl = import.meta.env.VITE_API_URL;
Express
// config.js
import 'dotenv/config';
export const config = {
port: parseInt(process.env.PORT) || 3000,
dbUrl: process.env.DATABASE_URL,
isProduction: process.env.NODE_ENV === 'production',
logLevel: process.env.LOG_LEVEL || 'info',
};
// app.js
import { config } from './config.js';
app.listen(config.port);
Security Best Practices
.gitignore
# Always ignore .env files with secrets
.env
.env.local
.env.*.local
# Keep example file
!.env.example
.env.example Template
# .env.example (commit this to repo)
# Copy to .env and fill in values
DATABASE_URL=postgres://user:password@localhost:5432/dbname
API_KEY=your-api-key-here
PORT=3000
NODE_ENV=development
Secret Rotation
// Support multiple API keys during rotation
const apiKeys = process.env.API_KEYS?.split(',') || [];
function validateApiKey(key) {
return apiKeys.includes(key);
}
Production Deployment
Docker
# Dockerfile - don't include .env
FROM node:20
COPY . .
RUN npm install
CMD ["node", "server.js"]
# docker-compose.yml
services:
app:
build: .
env_file:
- .env.production
environment:
- NODE_ENV=production
Cloud Platforms
# Vercel
vercel env add DATABASE_URL production
# Heroku
heroku config:set DATABASE_URL=postgres://...
# AWS (via Parameter Store or Secrets Manager)
aws ssm put-parameter --name "/app/DATABASE_URL" --value "..." --type SecureString
Common Patterns
Default Values
const port = process.env.PORT || 3000;
const host = process.env.HOST ?? 'localhost';
const debug = process.env.DEBUG === 'true';
Required Variables
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const apiKey = requireEnv('API_KEY');
Parsing Complex Values
# .env
ALLOWED_ORIGINS=http://localhost:3000,https://example.com
FEATURE_FLAGS={"darkMode":true,"betaFeatures":false}
// JavaScript
const origins = process.env.ALLOWED_ORIGINS?.split(',') || [];
const features = JSON.parse(process.env.FEATURE_FLAGS || '{}');
Tools and Resources
For working with configuration:
- JSON Formatter - Format JSON config values
- Base64 Encoder - Encode binary secrets
- Hash Generator - Generate secure tokens
Troubleshooting
Variables Not Loading
// Check if dotenv is loaded first
console.log(process.env.MY_VAR); // undefined
// Solution: import dotenv before other imports
import 'dotenv/config';
import { myModule } from './module.js';
Different Values in Production
// Debug: log which file is loaded
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('Loading from:', `.env.${process.env.NODE_ENV}`);
Conclusion
Environment variables are essential for secure, flexible applications. Key takeaways:
- Never commit secrets to source control
- Use .env files for local development
- Validate environment variables at startup
- Use platform-specific secret management in production
- Keep .env.example updated for team onboarding
For more developer resources, explore our free online tools. For dotenv documentation, see dotenv on GitHub.