Jordach MakayaOperations leader · Backend & systems engineeringCasablanca · FR / EN
All tools
Labs · Developer toolOpen source projectTypeScriptZodNestJS

Validated config. Typed from the start.

process.env gives you strings. Your application needs real types, validation, defaults, and safe failures.

Package
@hardmachinelabs/zod-config
Reading
7 min
Status
Published on npm
Requires
Node.js ≥ 18 · Zod v4
Raw input
process.env.PORT = "3000"
process.env.DATABASE_URL = "..."
zod-configruntime validation · type inference · fail-fast
Typed output
config.get('PORT'): number (3000)
config.get('DATABASE_URL'): string
From loose process.env strings to validated immutable configurationfail fast on startup

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 zod

For NestJS applications, add the optional adapter peer dependencies:

pnpm add @hardmachinelabs/zod-config zod @nestjs/common @nestjs/config

Defining 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"); // number

NestJS 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() and redactValue() mask sensitive data.
  • Immutable application state: ConfigReader deep-freezes the validated configuration by default.
  • No ambient logging: the library never prints environment variables or secrets to standard output automatically.