Validating a .env means different things at different stages: syntax now, required keys at boot, types at CI. The five approaches below go from "one-line check" to "full type-safe schema."
1. Browser-based validator (fastest, zero install)
Paste the file into the online env validator — it runs client-side, flags missing values, duplicates, invalid identifiers, and unterminated quotes in < 50ms.
When to use: debugging a broken deploy, checking a teammate's .env.local, reviewing a PR.
When to skip: you need this in CI or at app startup — the browser isn't available there.
2. A shell one-liner (good for CI)
The bare-minimum "does every key have a value" check:
# fail if any non-comment line has empty value
awk -F= '!/^#/ && NF>1 && $2==""' .env | \
{ if read -r line; then echo "empty: $line"; exit 1; fi; }Useful when you can't install anything in the CI runner.
3. Missing-keys check against a schema list
Most production breakage isn't bad syntax — it's a required key that's missing. The missing env detector takes two inputs:
- Your
.env - A list of required keys (or your
.env.example)
It tells you exactly which required keys are missing or empty, and which keys exist but aren't on the required list (often dead code).
For automation, do the same check in CI with a two-line script:
# CI check — fail if any key in .env.example is missing in .env
comm -23 \
<(grep -E '^[A-Z_]+=' .env.example | cut -d= -f1 | sort) \
<(grep -E '^[A-Z_]+=' .env | cut -d= -f1 | sort) | \
{ if read -r line; then echo "missing: $line"; exit 1; fi; }4. Type-safe validation at boot (production-grade)
The gold standard: validate at app startup, fail fast if any key is missing or wrong type.
Node.js / TypeScript with Zod
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
export const env = envSchema.parse(process.env);
// TypeScript now knows env.PORT is a number, env.DATABASE_URL is a URL, etc.Next.js with t3-env
// env.ts
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
},
client: {
NEXT_PUBLIC_SITE_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
},
});t3-env splits client and server schemas, so you can't accidentally ship a server-only secret to the browser.
Python with Pydantic BaseSettings
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
jwt_secret: str
log_level: str = "info"
class Config:
env_file = ".env"
settings = Settings()
# Raises on missing fields, coerces types automaticallyGo with envconfig
import "github.com/kelseyhightower/envconfig"
type Config struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
Port int `envconfig:"PORT" default:"8080"`
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
}
var config Config
envconfig.MustProcess("", &config)5. CI-time validation (belt + suspenders)
Even with runtime schema validation, catch mistakes earlier — at PR time. A GitHub Action that boots the app briefly to trigger env validation:
# .github/workflows/env-check.yml
name: env check
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: |
cp .env.example .env
# fill in fake but well-typed values
echo 'DATABASE_URL=postgres://fake:pass@localhost/test' >> .env
echo 'JWT_SECRET=$(openssl rand -hex 32)' >> .env
node -e "require('./dist/env.js')" # triggers zod parseThis catches every missing or bad-typed key on every PR, not just the ones the developer thought to test.
Which approach should you pick?
| Scenario | Pick |
|---|---|
| One-off PR review | Browser validator |
| Solo side project | Zod at boot |
| Team Next.js app | t3-env |
| Python service | Pydantic BaseSettings |
| Go service | envconfig |
| Regulated shop | Zod at boot + CI-time validation |
The two checks every team should run
- Syntax: browser validator on every PR that touches
.env.example. - Required keys: missing env detector or the
commone-liner in CI.
Bonus: if you also run the leak checker before every push, the same pipeline catches syntax + missing + leaked-secret issues in one go.