| name | environment-config |
| description | Centralized environment variable management with validation. Fail fast at startup if config is invalid. Supports multi-environment setups (dev/staging/prod) with type-safe access. |
| license | MIT |
| compatibility | TypeScript/JavaScript, Python |
| metadata | {"category":"foundations","time":"2h","source":"drift-masterguide"} |
Environment Configuration
Centralized, validated environment variables that fail fast at startup.
When to Use This Skill
- Starting a new project that needs env var management
- Environment variables scattered across codebase
- Missing vars causing runtime crashes
- Need different configs for dev/staging/prod
- Want type-safe access to configuration
Core Concepts
- Centralized config - Single source of truth for all env vars
- Fail fast - Validate at startup, not when first accessed
- Type safety - Full TypeScript/Python typing for all config values
- Environment separation - Clear distinction between dev/staging/prod
File Structure
project/
├── .env # Local development (gitignored)
├── .env.example # Template (committed)
├── .env.production # Production overrides (gitignored or in CI)
├── .env.local # Local overrides (gitignored)
└── src/
└── lib/
└── env.ts # Validation and exports
TypeScript Implementation
With Zod Validation
import { z } from 'zod';
const serverSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
DATABASE_URL: z.string().url(),
API_URL: z.string().url().default('http://localhost:8787'),
REDIS_URL: z.string().url().optional(),
ENABLE_ANALYTICS: z.coerce.boolean().default(false),
ENABLE_RATE_LIMITING: z.coerce.boolean().default(true),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
const clientSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_APP_URL: z.string().url().optional(),
});
const serverEnv = serverSchema.safeParse(process.env);
const clientEnv = clientSchema.safeParse(process.env);
if (!serverEnv.success) {
console.error('❌ Invalid server environment variables:');
console.error(JSON.stringify(serverEnv.error.flatten().fieldErrors, null, 2));
throw new Error('Invalid server environment configuration');
}
if (!clientEnv.success) {
console.error('❌ Invalid client environment variables:');
console.error(JSON.stringify(clientEnv.error.flatten().fieldErrors, null, 2));
throw new Error('Invalid client environment configuration');
}
export const env = serverEnv.data;
export const publicEnv = clientEnv.data;
export type Env = z.infer<typeof serverSchema>;
export type PublicEnv = z.infer<typeof clientSchema>;
Without Dependencies (Lightweight)
function requireEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
function optionalEnv(key: string, defaultValue: string): string {
return process.env[key] || defaultValue;
}
function boolEnv(key: string, defaultValue: boolean): boolean {
const value = process.env[key];
if (value === undefined) return defaultValue;
return value === 'true' || value === '1';
}
function intEnv(key: string, defaultValue: number): number {
const value = process.env[key];
if (value === undefined) return defaultValue;
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
throw new Error(`Environment variable ${key} must be a number`);
}
return parsed;
}
export const env = {
DATABASE_URL: requireEnv('DATABASE_URL'),
JWT_SECRET: requireEnv('JWT_SECRET'),
API_URL: optionalEnv('API_URL', 'http://localhost:8787'),
LOG_LEVEL: optionalEnv('LOG_LEVEL', 'info'),
PORT: intEnv('PORT', 3000),
ENABLE_ANALYTICS: boolEnv('ENABLE_ANALYTICS', false),
ENABLE_RATE_LIMITING: boolEnv('ENABLE_RATE_LIMITING', true),
isDev: process.env.NODE_ENV === 'development',
isProd: process.env.NODE_ENV === 'production',
isTest: process.env.NODE_ENV === 'test',
} as const;
Object.keys(env);
Python Implementation
With Pydantic
from pydantic_settings import BaseSettings
from pydantic import Field, field_validator
from typing import Literal
from functools import lru_cache
class Settings(BaseSettings):
"""Application settings with validation."""
environment: Literal["development", "production", "test"] = "development"
debug: bool = False
database_url: str = Field(..., description="PostgreSQL connection string")
api_url: str = "http://localhost:8787"
redis_url: str | None = None
enable_analytics: bool = False
enable_rate_limiting: bool = True
jwt_secret: str = Field(..., min_length=32)
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
@field_validator("database_url")
@classmethod
def validate_database_url(cls, v: str) -> str:
if not v.startswith(("postgresql://", "postgres://")):
raise ValueError("database_url must be a PostgreSQL connection string")
return v
@property
def is_dev(self) -> bool:
return self.environment == "development"
@property
def is_prod(self) -> bool:
return self.environment == "production"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
@lru_cache
def get_settings() -> Settings:
"""Cached settings instance. Call once at startup."""
return Settings()
settings = get_settings()
Without Dependencies
import os
from dataclasses import dataclass
from typing import Optional
class ConfigError(Exception):
"""Raised when configuration is invalid."""
pass
def require_env(key: str) -> str:
"""Get required environment variable or raise."""
value = os.getenv(key)
if not value:
raise ConfigError(f"Missing required environment variable: {key}")
return value
def optional_env(key: str, default: str) -> str:
"""Get optional environment variable with default."""
return os.getenv(key, default)
def bool_env(key: str, default: bool) -> bool:
"""Get boolean environment variable."""
value = os.getenv(key)
if value is None:
return default
return value.lower() in ("true", "1", "yes")
def int_env(key: str, default: int) -> int:
"""Get integer environment variable."""
value = os.getenv(key)
if value is None:
return default
try:
return int(value)
except ValueError:
raise ConfigError(f"Environment variable {key} must be an integer")
@dataclass(frozen=True)
class Settings:
"""Immutable application settings."""
database_url: str
jwt_secret: str
api_url: str
log_level: str
port: int
enable_analytics: bool
enable_rate_limiting: bool
is_dev: bool
is_prod: bool
is_test: bool
def load_settings() -> Settings:
"""Load and validate settings from environment."""
return Settings(
database_url=require_env("DATABASE_URL"),
jwt_secret=require_env("JWT_SECRET"),
api_url=optional_env("API_URL", "http://localhost:8787"),
log_level=optional_env("LOG_LEVEL", "INFO"),
port=int_env("PORT", 8000),
enable_analytics=bool_env("ENABLE_ANALYTICS", False),
enable_rate_limiting=bool_env("ENABLE_RATE_LIMITING", True),
is_dev=os.getenv("ENVIRONMENT", "development") == "development",
is_prod=os.getenv("ENVIRONMENT") == "production",
is_test=os.getenv("ENVIRONMENT") == "test",
)
settings = load_settings()
Usage Examples
TypeScript
import { env } from '@/lib/env';
export async function GET() {
if (env.ENABLE_ANALYTICS) {
await trackEvent('api_call');
}
const response = await fetch(`${env.API_URL}/data`);
return Response.json(await response.json());
}
import { publicEnv } from '@/lib/env';
const apiClient = createClient(publicEnv.NEXT_PUBLIC_API_URL);
Python
from config.settings import settings
@app.get("/health")
async def health():
return {
"status": "healthy",
"environment": settings.environment,
"debug": settings.debug,
}
from config.settings import settings
async def process_data():
if settings.enable_analytics:
await track_event("data_processed")
.env.example Template
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
JWT_SECRET=your-secret-key-at-least-32-characters-long
API_URL=http://localhost:8787
REDIS_URL=redis://localhost:6379
ENABLE_ANALYTICS=false
ENABLE_RATE_LIMITING=true
NODE_ENV=development
LOG_LEVEL=debug
PORT=3000
.gitignore
# Environment files
.env
.env.local
.env.*.local
.env.production
.env.staging
# Keep the example
!.env.example
Docker Integration
# Build args for client-side vars (baked into bundle)
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
# Runtime vars set via docker-compose or k8s
services:
app:
build: .
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
env_file:
- .env.production
Best Practices
- Validate at startup - Never let invalid config reach runtime
- Use typed access - No raw
process.env calls in business logic
- Separate client/server - Client vars need special prefixes
- Default sensibly - Dev-friendly defaults, prod requires explicit config
- Document everything -
.env.example is your config documentation
Common Mistakes
- Committing
.env files with secrets
- Using
process.env directly throughout codebase
- Not validating at startup (crashes at 3am instead)
- Exposing server secrets to client bundle
- Missing
.env.example (new devs can't onboard)
Related Skills