The problem
In Node.js applications, reading environment variables directly via process.env causes silent runtime errors when required values are missing or malformed. Booleans become traps (Boolean("false") === true), and error logs can accidentally leak production secrets.
// Fragile ambient read pattern
const port = Number(process.env.PORT ?? 3000);
const dbUrl = process.env.DATABASE_URL; // string | undefined!Installation
For core usage in any TypeScript or Node.js project:
pnpm add @hardmachinelabs/zod-config zodFor NestJS applications, add the optional adapter peer dependencies:
pnpm add @hardmachinelabs/zod-config zod @nestjs/common @nestjs/configDefining the schema & API
With zod-config, validate environment variables at startup, infer the parsed TypeScript type, and read typed values through ConfigReader:
import { z } from "zod";
import {
ConfigReader,
createEnvValidator,
type InferEnv,
} from "@hardmachinelabs/zod-config/core";
const schema = z.object({
PORT: z.coerce.number().int().default(3000),
EMAIL_ENABLED: z.stringbool().default(false),
DATABASE_URL: z.string().min(1),
});
type Env = InferEnv<typeof schema>;
const validateEnv = createEnvValidator({
schema,
sensitiveKeys: ["DATABASE_URL"],
});
const values = validateEnv(process.env);
export const config = new ConfigReader<Env>(values);
const port = config.get("PORT"); // numberNestJS integration
The optional Nest adapter integrates into the @nestjs/config lifecycle, capturing the validated Zod output once into a typed injectable service:
import { Injectable } from "@nestjs/common";
import { z } from "zod";
import type { InferEnv } from "@hardmachinelabs/zod-config/core";
import {
createZodConfigModule,
ZodConfigService,
} from "@hardmachinelabs/zod-config/nest";
const schema = z.object({
PORT: z.coerce.number().int().default(3000),
DATABASE_URL: z.string().min(1),
EMAIL_ENABLED: z.stringbool().default(false),
});
type Env = InferEnv<typeof schema>;
export const AppConfigModule = createZodConfigModule({
schema,
envFilePath: [".env"],
ignoreEnvFile: process.env.NODE_ENV === "production",
sensitiveKeys: ["DATABASE_URL"],
});
@Injectable()
export class SomeService {
constructor(private readonly config: ZodConfigService<Env>) {}
getPort(): number {
return this.config.get("PORT");
}
}Security model & error formatting
Configuration errors must be actionable during boot without leaking sensitive values:
- sensitiveKeys: marks schema keys that hold credentials and must be protected.
- EnvValidationError: issue messages store issue code, path, and sensitivity, but never include raw secret values.
- Safe formatting:
formatValidationError()andredactValue()mask sensitive data. - Immutable application state:
ConfigReaderdeep-freezes the validated configuration by default. - No ambient logging: the library never prints environment variables or secrets to standard output automatically.