Stop Deploying Broken Configs: How to Validate .env Files
Environment variables are the standard way to configure applications across environments. But .env files have no schema, no type system, and no validation by default. A missing variable or a typo in a value can take down production. This guide covers how to validate .env files and prevent configuration failures.
The .env problem
A typical .env file is deceptively simple:
DATABASE_URL=postgresql://localhost:5432/myapp
REDIS_URL=redis://localhost:6379
API_KEY=sk-abc123
PORT=3000
NODE_ENV=production
There is no way to tell from this file alone:
- Which variables are required vs optional
- What types are expected (string, number, URL, boolean)
- What values are valid (is
NODE_ENV=stagingallowed?) - Whether the values are in the correct format
When something is missing or wrong, you find out at runtime -- often in production.
Common .env mistakes
Missing variables
The most frequent issue. Someone adds a new feature that requires STRIPE_SECRET_KEY, updates their local .env, but forgets to add it to the staging/production environment. The app boots, everything looks fine, until a user tries to check out.
Type mismatches
# Is this a number or a string?
PORT=3000
# Is this a boolean?
ENABLE_CACHE=true
DEBUG=1
VERBOSE=yes
Without explicit typing, your code might parse "true" as a truthy string or fail when parseInt("3000") returns the expected value but parseInt("three-thousand") returns NaN.
Formatting errors
# Extra spaces around the equals sign (some parsers handle this, some don't)
DATABASE_URL = postgresql://localhost:5432/myapp
# Unquoted value with spaces (breaks many parsers)
APP_NAME=My Cool App
# Trailing whitespace (invisible but causes string comparison failures)
API_KEY=sk-abc123
Wrong environment values
# Meant to use production database, accidentally left staging URL
DATABASE_URL=postgresql://staging-db:5432/myapp
NODE_ENV=production
Using .env.example as a schema
The .env.example file is the closest thing to a schema that most projects have. It documents which variables exist and provides sample values:
# .env.example
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
API_KEY=your-api-key-here
PORT=3000
NODE_ENV=development
ENABLE_CACHE=true
LOG_LEVEL=info
The problem is that .env.example has no enforcement mechanism. It is documentation, not validation. Developers can ignore it, and CI pipelines do not check it by default.
Schema-based validation with Zod
The most robust approach is to validate environment variables at application startup using a schema library:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url().startsWith('postgresql://'),
REDIS_URL: z.string().url().startsWith('redis://'),
API_KEY: z.string().min(1, 'API_KEY is required'),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
ENABLE_CACHE: z.coerce.boolean().default(false),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
type Env = z.infer<typeof envSchema>;
function validateEnv(): Env {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Environment validation failed:');
for (const issue of result.error.issues) {
console.error(` ${issue.path.join('.')}: ${issue.message}`);
}
process.exit(1);
}
return result.data;
}
export const env = validateEnv();
This approach gives you:
- Type safety:
env.PORTis anumber, not astring - Default values: missing optional variables get defaults
- Format validation: URLs are checked for valid format
- Enum constraints:
NODE_ENVcan only be one of three values - Fail-fast: the app exits immediately with clear error messages if validation fails
Validation in CI/CD
Catching configuration errors before deployment is better than catching them in production. Add a validation step to your CI pipeline:
# GitHub Actions example
- name: Validate environment variables
run: |
node -e "
const required = [
'DATABASE_URL',
'REDIS_URL',
'API_KEY',
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('Missing required env vars:', missing.join(', '));
process.exit(1);
}
"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
REDIS_URL: ${{ secrets.REDIS_URL }}
API_KEY: ${{ secrets.API_KEY }}
The @t3-oss/env approach
The T3 stack popularized a pattern that validates environment variables and provides type-safe access in Next.js applications:
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
API_SECRET: process.env.API_SECRET,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
});
This separates server-side and client-side variables, preventing accidental exposure of secrets to the browser.
Dotenv file format rules
For maximum compatibility across parsers, follow these rules:
# Keys: uppercase, underscores, no spaces
DATABASE_URL=value
# Values: quote strings that contain spaces or special characters
APP_NAME="My Cool App"
# Use single quotes for values with dollar signs (prevents variable expansion)
REGEX_PATTERN='^\d{3}-\d{4}$'
# Multi-line values: use double quotes with \n
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIB..."
# Comments: start with #
# This is a comment
DATABASE_URL=postgresql://localhost:5432/myapp # Inline comments may not work in all parsers
# Empty values
OPTIONAL_FEATURE=
Practical checklist
- Create a
.env.examplewith all variables and sample values. Commit it to the repository. - Add startup validation using Zod or a similar library. Fail fast with clear messages.
- Type-coerce values at the validation layer.
PORTshould be a number everywhere in your code. - Separate server and client variables in frontend frameworks. Never expose secrets to the browser.
- Validate in CI before deploying. A simple script checking for required variables prevents outages.
- Document constraints in the schema, not just in comments. A Zod schema with
.url().startsWith('postgresql://')is more useful than# Must be a PostgreSQL URL.
Summary
Environment variables are a runtime configuration layer with no built-in safety net. Adding schema validation at startup, type coercion, and CI checks transforms .env files from a common source of production incidents into a reliable, self-documenting configuration system.
Try our Env Validator to validate and check your .env files for errors instantly -- right in your browser, no upload required.