Environment Variables
Define, validate, and maintain environment variables with clear separation by environment and risk level.
When to Use
- You are introducing or changing configuration via environment variables.
- You need a secure method to handle secrets and required config.
- You want to prevent runtime failures caused by missing or invalid values.
Workflow
- Enumerate required variables and classify each as secret or non-secret.
- Define source of truth per environment (local/staging/production).
- Validate naming consistency and default value policy.
- Add startup validation for required variables and format checks.
- Document rotation and rollback steps for secret values.
Validation Patterns
Use a schema-based validator at startup so failures surface immediately, not deep in request handling. With Zod:
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "staging", "production"]),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.coerce.number().min(1).max(65535).default(3000),
});
const env = envSchema.parse(process.env);
With Joi:
const Joi = require("joi");
const envSchema = Joi.object({
NODE_ENV: Joi.string().valid("development", "staging", "production").required(),
DATABASE_URL: Joi.string().uri().required(),
API_KEY: Joi.string().min(32).required(),
PORT: Joi.number().port().default(3000),
}).options({ stripUnknown: true });
const { value: env, error } = envSchema.validate(process.env);
if (error) throw new Error(`Invalid env: ${error.message}`);
Treat .env.example as the contract: list every variable with a placeholder and short description. Never commit .env. Use dotenv hierarchy: .env (base) → .env.local (local overrides, gitignored) → .env.<environment> for staging/production. Load in that order so higher-priority files override lower ones.
Common Pitfalls
- Missing required vars discovered at runtime: A request hits a code path that needs
REDIS_URL; the app never validated it at startup. Fix: validate all required vars in one place before the app accepts traffic. - Inconsistent naming across services: One service uses
DB_HOST, anotherdatabase_host, anotherDATABASE_HOST. Fix: adopt a single convention (e.g., SCREAMING_SNAKE_CASE, prefixed by domain) and enforce via lint or schema. - Committing
.envfiles: Secrets end up in git history. Fix: add.env,.env.local,.env.*.localto.gitignore; use.env.examplewith placeholders only. - Default values masking misconfig: Defaulting
DATABASE_URLtolocalhosthides the fact production never received the real URL. Fix: no defaults for required production vars; fail fast if absent.
Output Format
## Variable Inventory
- <VAR_NAME>: <purpose> (<secret|non-secret>) [required|optional]
## Environment Mapping
- Local: <source>
- Staging: <source>
- Production: <source>
## Validation Rules
- [ ] Required variables are present
- [ ] Value format checks defined
- [ ] No secrets committed to repo
## Schema Snippet
- Validator: <zod|joi|other>
- Required vars: <list>
- Format checks: <URL, enum, port, etc.>
## .env.example Contract
- <VAR_NAME>=<placeholder> # <description>
## Common Pitfalls Addressed
- [ ] Startup validation blocks traffic until env valid
- [ ] Naming convention documented and consistent
- [ ] .env* in .gitignore, .env.example committed
- [ ] No silent defaults for required production vars
## Operational Notes
- Rotation: <how to rotate>
- Rollback: <fallback behavior>
Constraints
- Never expose secret values in outputs.
- Prefer explicit validation failures over silent defaults for required variables.
- Keep environment naming and variable conventions consistent across services.
Scan to join WeChat group