Managing .env and Environment Variables in Node.js
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.
Frequently Asked Questions
What are environment variables for?
To separate configuration (keys, URLs, secrets) from code, so it changes per environment without touching the source.
Why should you not put secrets in the code?
Because they would end up in the repository and be visible to anyone; use environment variables or a secret manager.