Hardcoding database credentials, API keys, and feature flags in your source code is one of the most common — and most dangerous — mistakes in web development. Environment variables separate configuration from code, letting you deploy the same codebase to development, staging, and production with different settings.
The 12-Factor App Principle
The 12-Factor App methodology, created by Heroku's founders, states that configuration should be stored in the environment, never in the codebase. This means:
- Database URLs, API keys, and secrets are never committed to version control
- The same code runs in every environment; only the configuration changes
- Configuration is injected via environment variables at runtime
Using .env Files
# Database DATABASE_URL=postgres://user:password@localhost:5432/myapp_dev DATABASE_POOL_SIZE=10 # API Keys STRIPE_SECRET_KEY=sk_test_abc123 SENDGRID_API_KEY=SG.xxxxx # App Configuration NODE_ENV=development PORT=3000 APP_URL=http://localhost:3000 # Feature Flags ENABLE_NEW_CHECKOUT=true ENABLE_DARK_MODE=false
Reading Environment Variables
// Load .env file in development
import 'dotenv/config';
// Access variables
const dbUrl = process.env.DATABASE_URL;
const port = parseInt(process.env.PORT || '3000', 10);
const isDev = process.env.NODE_ENV === 'development';
// Validate required variables at startup
const required = ['DATABASE_URL', 'STRIPE_SECRET_KEY'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
}
// Native PHP
$dbUrl = getenv('DATABASE_URL');
$debug = getenv('APP_DEBUG') === 'true';
// With vlucas/phpdotenv (Composer)
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$dotenv->required(['DATABASE_URL', 'APP_KEY'])->notEmpty();
// Access via $_ENV
$apiKey = $_ENV['STRIPE_SECRET_KEY'];
Critical Rule: .gitignore
# NEVER commit these .env .env.local .env.production # DO commit this (template without real values) # .env.example
Create a .env.example file with all the required variables but placeholder values. This documents what variables your app needs without exposing secrets.
DATABASE_URL=postgres://user:password@localhost:5432/myapp STRIPE_SECRET_KEY=sk_test_your_key_here NODE_ENV=development PORT=3000
Environment-Specific Configuration
- Development: Debug mode on, verbose logging, local database, test API keys
- Staging: Debug mode off, production-like config, staging API keys
- Production: Debug mode off, minimal logging, production database, real API keys, HTTPS enforced
Secrets Management in Production
For production, don't rely on .env files on the server. Use proper secrets management:
- Cloud provider secrets: AWS Secrets Manager, Google Secret Manager, Azure Key Vault
- HashiCorp Vault: Self-hosted secrets management with rotation and audit logging
- CI/CD secrets: GitHub Actions secrets, GitLab CI variables, Vercel environment variables
- Docker: Docker secrets, or inject via
docker run -eordocker-compose.yml
Common Mistakes
- Committing .env to Git — even once. If you've done this, rotate all secrets immediately.
- Using the same API keys for dev and prod — always use test/sandbox keys in development.
- Not validating env vars at startup — fail fast with a clear error message instead of crashing later with a cryptic error.
- Accessing
process.enveverywhere — centralize config access in a single module.
Try Our Free Developer Tools
Encode and decode secrets, format config files instantly.