The Pain of Missing Env Vars
We've all been there: deploying a Node.js application only to have it crash immediately (or worse, subtly misbehave) because a crucial environment variable like DATABASE_URL
was missing, empty, or malformed. Debugging configuration issues after deployment is frustrating and time-consuming. Manually checking process.env
properties everywhere is tedious and error-prone.
What if you could guarantee your application's required environment variables are present and valid before any of your core application logic runs? What if you could get full type safety for your process.env
object?
Introducing schema-env
v2.1
That's exactly why I built schema-env
! It's a lightweight Node.js library that loads your standard .env
files, merges them with process.env
, and validates the result against a schema at application startup. If validation fails, it throws a clear error, preventing your app from starting in an invalid state. If it succeeds, you get back a fully typed configuration object.
I'm excited to announce v2.1.0, which brings a major enhancement alongside a critical bug fix:
✨ Pluggable Validation Adapters: You're no longer limited to Zod! While Zod remains the excellent default, you can now easily integrate Joi, Yup, or even your own custom validation logic using a simple adapter interface.
🐛 Precedence Bug Fix: Fixed a crucial bug ensuring process.env
values now correctly override values loaded from .env
files, matching the documented behavior.
Quick Start with Zod (Default)
Getting started is simple if you're using Zod:
1. Define your schema (src/env.ts
)
import { z } from "zod";
export const envSchema = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url("Must be a valid PostgreSQL URL"),
API_KEY: z.string().min(15, "API Key must be at least 15 characters"),
});
// Infer the type directly!
export type Env = z.infer<typeof envSchema>;
2. Create your .env
file
# .env
DATABASE_URL=postgresql://user:password@host:5432/db
API_KEY=your-super-secret-and-long-api-key
PORT=8080
3. Validate at startup (src/index.ts
)
import { createEnv } from "schema-env";
import { envSchema, type Env } from "./env.js"; // Use .js extension if needed
let env: Env;
try {
// Pass the schema. Type is inferred!
env = createEnv({ schema: envSchema });
console.log("✅ Environment validated successfully!");
} catch (error) {
// createEnv throws on validation failure
// Error details are already logged to console.error
console.error("❌ Fatal: Environment validation failed.");
process.exit(1);
}
// Now use your type-safe 'env' object
console.log(`Running on port: ${env.PORT}`);
// await connectToDb(env.DATABASE_URL);
If DATABASE_URL
is missing or API_KEY
is too short, createEnv
throws, and your app exits cleanly.
New in v2.1: Bring Your Own Validator!
Want to use Joi, Yup, or something else? The new validator
option makes it easy.
Example using Joi:
(See the full runnable example in the repo: examples/custom-adapter-joi/
)
1. Define Joi Schema & TS Type (env.joi.ts
)
import Joi from "joi";
// Define the TS type you expect after validation
export interface JoiEnv {
API_HOST: string;
API_PORT: number;
}
// Define the Joi schema
export const joiEnvSchema = Joi.object<JoiEnv, true>({
API_HOST: Joi.string().hostname().required(),
API_PORT: Joi.number().port().default(8080),
}).options({ abortEarly: false, allowUnknown: true, convert: true });
2. Implement the Adapter (joi-adapter.ts
)
import type { ObjectSchema } from "joi";
// Import types from schema-env
import type { ValidationResult, ValidatorAdapter, StandardizedValidationError } from "schema-env";
export class JoiValidatorAdapter<TResult> implements ValidatorAdapter<TResult> {
constructor(private schema: ObjectSchema<TResult>) {}
validate(data: Record<string, unknown>): ValidationResult<TResult> {
const result = this.schema.validate(data);
if (!result.error) {
return { success: true, data: result.value as TResult };
} else {
// Map Joi errors to the standardized format
const issues: StandardizedValidationError[] = result.error.details.map((d) => ({
path: d.path,
message: d.message,
}));
return { success: false, error: { issues } };
}
}
}
3. Use the Adapter (index.ts
)
import { createEnv } from "schema-env";
import { JoiValidatorAdapter } from "./joi-adapter.js";
import { joiEnvSchema, type JoiEnv } from "./env.joi.js";
const adapter = new JoiValidatorAdapter(joiEnvSchema);
try {
// Use 'validator' option and provide explicit types
const env = createEnv<undefined, JoiEnv>({
validator: adapter,
// dotEnvPath options still work here too!
});
console.log("Validated with Joi:", env.API_HOST, env.API_PORT);
} catch (error) {
console.error("❌ Fatal: Environment validation failed.");
process.exit(1).
}
Other Features
-
Async Secrets: Need to fetch secrets from AWS Secrets Manager, HashiCorp Vault, etc., at startup? Use
createEnvAsync
! It fetches from multiple sources concurrently before validation.
const env = await createEnvAsync({ schema, // or validator secretsSources: [fetchFromAWS, fetchFromVault], });
Flexible
.env
Loading: Handles.env
,.env.${NODE_ENV}
, loading from multiple paths (dotEnvPath: ['.env.shared', '.env.local']
), and optional variable expansion viadotenv-expand
(expandVariables: true
).Clear Precedence:
process.env
> Secrets (async only) >.env.${NODE_ENV}
>dotEnvPath
files > Schema/Adapter Defaults.
A Note on Development
This project was developed as an exploration of human-AI collaboration. I worked closely with an AI assistant (guided by specific instructions, project docs, and requirements) for tasks like generating boilerplate, drafting code/docs/tests, and refactoring. All code, fixes (like the precedence bug!), and final decisions were reviewed, directed, and tested by me (the human!). It's been an interesting process learning how to leverage these tools effectively in a structured project.
Try It Out!
Stop letting configuration errors ruin your deployments. Give schema-env
a try!
- GitHub (Code, Docs, Examples): https://github.com/devvictrix/schema-env
- npm:
npm i schema-env zod
(or justschema-env
if using adapters)
Top comments (0)