Environment variables keep configuration and secrets out of your code — but done wrong, they leak credentials, break in production, or fail silently. This guide covers secure, robust environment variable management in Node.js for 2026.
📋 Table of Contents
- Why Environment Variables?
- Basic Setup with dotenv
- Critical: gitignore Your .env Files
- Validate Environment Variables at Startup
- Never Do These (Common Leaks)
- Different Configs Per Environment
- Production Secrets: Beyond .env Files
- Frontend Environment Variables (Different Rules)
- Frequently Asked Questions
- Conclusion
Why Environment Variables?
- Separate config from code: Same code runs in dev, staging, and production with different settings
- Keep secrets out of source control: API keys and passwords never get committed
- Follow twelve-factor app principles: Config in the environment, not the codebase
Basic Setup with dotenv
npm install dotenv
# .env (NEVER commit this — add to .gitignore)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=your-long-random-secret
API_KEY=sk-1234567890
PORT=3000
NODE_ENV=development
// Load at the very top of your entry file
import 'dotenv/config'; // ESM
// or: require('dotenv').config(); // CommonJS
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
Critical: gitignore Your .env Files
# .gitignore
.env
.env.local
.env.*.local
# ✅ DO commit a template with no real values
# .env.example
DATABASE_URL=
JWT_SECRET=
API_KEY=
PORT=3000
The .env.example documents required variables without exposing real values — new developers copy it to .env and fill in their own.
Validate Environment Variables at Startup
Don’t let your app start with missing or invalid config — fail fast with a clear error:
npm install zod
// config.ts — validate and export typed config
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 chars'),
API_KEY: z.string().startsWith('sk-'),
});
// Validate on startup — throws with clear errors if invalid
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:');
console.error(parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const config = parsed.data; // typed, validated config
// Now use config.DATABASE_URL — TypeScript knows its type
Never Do These (Common Leaks)
// 🐛 Committing .env — the #1 way secrets leak
// Always gitignore it
// 🐛 Logging environment variables
console.log(process.env); // ❌ dumps ALL secrets to logs
console.log(`Key: ${process.env.API_KEY}`); // ❌ leaks to logs
// 🐛 Sending config to the frontend
// Server-side secrets must NEVER reach client-side code
// 🐛 Hardcoding as a "temporary" fallback
const key = process.env.API_KEY || 'sk-realkey123'; // ❌ hardcoded secret
// ✅ Fail if required secrets are missing (via validation above)
Different Configs Per Environment
# .env.development
DATABASE_URL=postgresql://localhost:5432/dev
LOG_LEVEL=debug
# .env.production
DATABASE_URL=postgresql://prod-host:5432/prod
LOG_LEVEL=error
# Load the right file based on NODE_ENV
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV || 'development'}` });
Production Secrets: Beyond .env Files
In production, .env files aren’t ideal for secrets. Use a proper secrets manager:
- Cloud provider secrets: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
- Platform env vars: Set directly in Vercel, Railway, Fly.io, or your CI/CD — not in files
- HashiCorp Vault: For enterprise secret management with rotation
// Example: load a secret from AWS Secrets Manager at startup
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
async function loadSecret(name: string) {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const resp = await client.send(new GetSecretValueCommand({ SecretId: name }));
return JSON.parse(resp.SecretString!);
}
const secrets = await loadSecret('myapp/production');
const dbPassword = secrets.DB_PASSWORD;
Frontend Environment Variables (Different Rules)
// Frontend env vars are PUBLIC — anyone can read them in the browser
// NEVER put secrets in frontend env vars
// Vite — only VITE_ prefixed vars are exposed to the client
VITE_API_URL=https://api.example.com // ✅ public config, fine
VITE_SECRET_KEY=sk-123 // ❌ NEVER — this is public!
// Next.js — NEXT_PUBLIC_ prefix for client-exposed vars
NEXT_PUBLIC_API_URL=https://api.example.com // ✅ public
DATABASE_URL=... // server-only, safe
Frequently Asked Questions
Q: Is dotenv safe for production?
A: For non-sensitive config, it’s fine. For secrets in production, prefer a secrets manager or platform environment variables (set in the hosting dashboard) rather than committing or deploying .env files with secrets.
Q: My .env works locally but variables are undefined in production. Why?
A: Production likely doesn’t load your .env file (it’s gitignored, correctly). Set the variables in your hosting platform’s environment settings, CI/CD secrets, or a secrets manager instead.
Q: How do I share secrets with my team securely?
A: Never via chat/email/commits. Use a shared secrets manager, a password manager with team sharing, or tools like Doppler/1Password that sync env vars securely to authorized developers.
Q: Should I validate environment variables?
A: Yes — validate at startup with a schema (Zod). It catches missing or malformed config immediately with a clear error, instead of mysterious failures deep in your app when a variable is undefined.
Q: Why can’t I use server secrets in frontend code?
A: Frontend code runs in the user’s browser where anyone can inspect it. Any “secret” bundled into frontend code is public. Keep secrets server-side and only expose non-sensitive config (like public API URLs) to the client.
Conclusion
Secure environment variable management in Node.js comes down to a few rules: gitignore your .env files, commit a .env.example template, validate variables at startup with a schema, never log or hardcode secrets, and use a proper secrets manager in production rather than deploying .env files. Remember that frontend env vars are public — keep secrets server-side. Following these practices keeps your credentials safe, catches config errors early, and makes your app work reliably across every environment.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment