Environment Variable Validation
Core Rule
NEVER use process.env.X directly. Validate at startup with Zod.
// ❌ WRONG — no types, fails at runtime
const apiUrl = process.env.API_URL // possibly undefined
const port = parseInt(process.env.PORT || '') // NaN if PORT="abc"
// ✅ CORRECT — validate once, typed access everywhere
import { env } from "@/lib/env"
const apiUrl = env.API_URL // string — guaranteed at startup
const port = env.PORT // number — parsed and validated
Basic Pattern
// lib/env.ts
import { z } from "zod"
const schema = z.object({
DATABASE_URL: z.string().url(),
ANTHROPIC_API_KEY: z.string().startsWith("sk-ant-"),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
NEXT_PUBLIC_APP_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
})
export const env = schema.parse(process.env)
schema.parse(process.env) runs once at module load — crashes immediately if any variable is missing or wrong type.
t3-env (Next.js)
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
ANTHROPIC_API_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
})
// Server code: env.DATABASE_URL ✅
// Client code: env.NEXT_PUBLIC_APP_URL ✅
// Client code: env.DATABASE_URL ❌ TypeScript error!
Validation Patterns
ANTHROPIC_API_KEY: z.string().startsWith("sk-ant-")
GITHUB_TOKEN: z.string().startsWith("ghp_")
FEATURE_FLAG: z.string().transform(v => v === "true").pipe(z.boolean())
PORT: z.coerce.number().int().positive().default(3000)
NODE_ENV: z.enum(["development", "test", "production"])
.env File Structure
# .env.example — COMMIT THIS (no real values)
DATABASE_URL=postgresql://localhost:5432/mydb
ANTHROPIC_API_KEY=sk-ant-...
# .env.local — NEVER COMMIT
DATABASE_URL=<real-value>
ANTHROPIC_API_KEY=<real-value>
.gitignore must include: .env, .env.local, .env.*.local, *.env
CI Validation
node -e "require('./src/lib/env')"
Add to CI pipeline to catch missing secrets before deployment.