| id |
env-variables-schema-validation |
| title |
Environment Variables with Schema Validation |
| category |
environment-config |
| skillLevel |
beginner |
| tags |
environment |
config |
schema |
validation |
dotenv |
|
| lessonOrder |
20 |
| rule |
| description |
Environment Variables with Schema Validation. |
|
| summary |
Environment variables power your application—database URLs, API keys, ports. But they're just strings. You load them with `process.env.DATABASE_URL`, hoping it exists and is valid. No type safety, no... |
Environment variables power your application—database URLs, API keys, ports. But they're just strings. You load them with process.env.DATABASE_URL, hoping it exists and is valid. No type safety, no compile-time checks. One missing variable crashes production. You need a single source of truth for environment configuration with validation and type safety.
import { Schema, Effect } from "effect"
// 1. Define environment schema
const EnvironmentSchema = Schema.Struct({
DATABASE_URL: Schema.String.pipe(
Schema.annotations({ description: 'PostgreSQL connection string' }),
),
API_KEY: Schema.String.pipe(
Schema.minLength(32),
Schema.annotations({
description: 'API authentication key (min 32 chars)',
}),
),
PORT: Schema.String.pipe(
Schema.parseNumber,
Schema.int(),
Schema.between(1024, 65535),
Schema.annotations({ description: 'Server port (1024-65535)' }),
),
LOG_LEVEL: Schema.Literal('debug', 'info', 'warn', 'error').pipe(
Schema.annotations({ description: 'Logging level' }),
),
NODE_ENV: Schema.Literal('development', 'staging', 'production').pipe(
Schema.annotations({ description: 'Deployment environment' }),
),
})
type Environment = typeof EnvironmentSchema.Type
// 2. Create validator
const validateEnv = Schema.decodeUnknown(EnvironmentSchema)
// 3. Load and validate environment
const loadEnvironment = Effect.fn(function* () {
const validated = yield* validateEnv(process.env)
console.log(`✅ Environment loaded: NODE_ENV=${validated.NODE_ENV}`)
return validated
})
// 4. Create service to provide environment
export class EnvironmentService extends Context.Tag('@app/EnvironmentService')<
EnvironmentService,
Environment & {
isDev: () => boolean
isStaging: () => boolean
isProd: () => boolean
}
>() {
static layer = Layer.effect(
this,
Effect.gen(function* () {
const env = yield* loadEnvironment()
return {
...env,
isDev: () => env.NODE_ENV === 'development',
isStaging: () => env.NODE_ENV === 'staging',
isProd: () => env.NODE_ENV === 'production',
}
}),
)
}
// Usage
const program = Effect.gen(function* () {
const envService = yield* EnvironmentService
console.log(`Database: ${envService.DATABASE_URL}`)
console.log(`Port: ${envService.PORT}`)
console.log(`Log level: ${envService.LOG_LEVEL}`)
console.log(`Is production: ${envService.isProd()}`)
return envService.PORT
})
// Run with environment layer
Effect.runPromise(program.pipe(Effect.provide(EnvironmentService.layer)))
.then((port) => console.log(`Server starting on port ${port}`))
.catch((error) => console.error(`Failed to start: ${error.message}`))
| Concept |
Explanation |
| Schema definition |
Single source of truth for all env vars |
| Type safety |
TypeScript knows exact env var types at compile time |
| Validation rules |
Enforce constraints (min length, numeric ranges, allowed values) |
| Fail early |
Validation errors happen on startup, not runtime |
| Service pattern |
Environment accessible throughout app via dependency injection |
| Immutable config |
Environment locked after validation, prevents accidental changes |
| Helper methods |
isDev(), isProd() encapsulate environment checks |
- Application startup with required configuration
- Multi-environment deployments (dev, staging, production)
- Third-party API integrations requiring keys
- Database connection strings with validation
- Server configuration (port, timeouts, limits)
- Any scenario where invalid env vars should crash fast