Appearance
Getting Started
@hardmachinelabs/zod-config is a small TypeScript configuration library built on top of Zod v4.
It solves a common problem in Node.js applications: environment variables arrive as untyped strings, often mixed with many unrelated process.env keys, while the application needs a validated, typed, safe-to-read configuration object.
The package gives you:
- a pure Zod-based validator for environment-like objects;
- typed access to validated values with
ConfigReader; - redaction-safe validation errors for secrets;
- an optional NestJS adapter through
@hardmachinelabs/zod-config/nest; - a secondary global bridge through
@hardmachinelabs/zod-config/global.
The core does not load .env files, does not read process.env, and does not mutate process.env. You decide where the input comes from, then the library validates it.
Step 1 - Install the Package
Install the library and Zod:
bash
pnpm add @hardmachinelabs/zod-config zodIf you use NestJS, also install the Nest peer dependencies in your application:
bash
pnpm add @nestjs/common @nestjs/configWhat you get:
@hardmachinelabs/zod-config/corefor framework-independent validation;@hardmachinelabs/zod-config/nestfor the optional NestJS adapter;zodas the schema engine.
Behind the scenes:
- Zod remains your source of truth for parsing, defaults, coercion, transforms, and unknown-key behavior;
- the root package stays core-only, so installing the core does not force NestJS into projects that do not use it.
Step 2 - Define Your Environment Schema
Create a Zod schema that describes the configuration your app actually needs:
ts
import { z } from 'zod'
const schema = z.object({
PORT: z.coerce.number().int().default(3000),
DATABASE_URL: z.string().min(1),
EMAIL_ENABLED: z.stringbool().default(false),
})What you get:
PORTbecomes anumber, even if the input was the string"3000";DATABASE_URLis required and must be non-empty;EMAIL_ENABLEDbecomes a realboolean, so"false"is not treated as truthy by mistake;- missing values can use Zod defaults.
Behind the scenes:
z.coerce.number()handles numeric string input;z.stringbool()parses common boolean strings such as"true","false","1","0","yes", and"no";- with a normal
z.object, unknown keys are stripped by Zod from the validated result.
Step 3 - Infer the TypeScript Type
Infer the TypeScript type from the schema:
ts
import type { InferEnv } from '@hardmachinelabs/zod-config/core'
type Env = InferEnv<typeof schema>What you get:
- one source of truth: the Zod schema;
- a TypeScript
Envtype that matches the parsed output, not the raw input; - typed config reads later, such as
config.get('PORT')returningnumber.
Behind the scenes:
InferEnv<TSchema>maps to the Zod output type;- defaults, coercions, and transforms are reflected in the resulting TypeScript type.
Step 4 - Create the Validator
Create a validator once, then call it with an environment-like object:
ts
import { createEnvValidator } from '@hardmachinelabs/zod-config/core'
const validateEnv = createEnvValidator({
schema,
sensitiveKeys: ['DATABASE_URL'],
})
const values = validateEnv(process.env)What you get:
valuescontains only the parsed Zod output;- sensitive keys are marked internally for safe error formatting;
- invalid configuration throws
EnvValidationError.
Behind the scenes:
- the core accepts
Record<string, unknown>; - passing
process.envis only a usage choice in your application code; - the library does not read from
process.envby itself; - the library does not write to or mutate
process.env; - raw sensitive values are not stored in validation issues.
Step 5 - Read Values Through ConfigReader
Wrap the validated values with ConfigReader:
ts
import { ConfigReader } from '@hardmachinelabs/zod-config/core'
const config = new ConfigReader<Env>(values)
const port = config.get('PORT')
const emailEnabled = config.get('EMAIL_ENABLED')What you get:
portis typed asnumber;emailEnabledis typed asboolean;- unknown keys fail at compile time:
ts
config.get('UNKNOWN_KEY')Behind the scenes:
ConfigReaderonly reads from the validated object passed to its constructor;- it never reads
process.env; - it deep-freezes the received object by default to avoid accidental mutation.
Built-in Error Handling
The package includes a small set of built-in error helpers so applications can fail clearly without leaking secrets.
Available from @hardmachinelabs/zod-config/core:
EnvValidationErrorfor invalid environment validation;formatValidationError()for redaction-safe validation messages;ConfigReaderErrorfor invalid runtime reads fromConfigReader;redactValue()for safe display of individual values;REDACTED_VALUEfor a stable redaction marker;EnvValidationIssuefor typed validation issue metadata.
This page only introduces them. See Error Handling for the complete list and usage examples.
Complete Core Example
ts
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),
DATABASE_URL: z.string().min(1),
EMAIL_ENABLED: z.stringbool().default(false),
})
type Env = InferEnv<typeof schema>
const validateEnv = createEnvValidator({
schema,
sensitiveKeys: ['DATABASE_URL'],
})
const values = validateEnv(process.env)
const config = new ConfigReader<Env>(values)
const port = config.get('PORT')
const emailEnabled = config.get('EMAIL_ENABLED')Next Steps
- Use the Core guide for framework-independent applications.
- Use the NestJS Adapter guide for NestJS applications.
- Read Error Handling for the complete error helper reference.
- Read Security before logging or displaying validation failures.
- Read Limitations for the intentionally small package scope.