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