Skip to content

Zod

@voltage/zod integrates Zod into NestJS in two places: module configuration and HTTP request validation. Configuration classes are created with ZodObject(), validated at registration time, and injectable like any other provider. For request data, a set of pipes and parameter decorators validate route params, query strings, and request bodies.

Terminal window
yarn add @voltage/zod zod reflect-metadata

ZodObject() takes a Zod schema and returns a base class. Extend it to create a typed, injectable configuration class:

import { ZodObject } from '@voltage/zod';
import { z } from 'zod';
export class RedisConfiguration extends ZodObject(
z.object({
url: z.url(),
}),
) {}

The class has two static methods: register() for synchronous registration and registerAsync() for async (e.g. when the data comes from another provider).

Pass the raw data directly. Validation runs immediately — if the data is invalid, the application throws at startup:

import { Module } from '@nestjs/common';
import { RedisConfiguration } from './redis.configuration';
@Module({
imports: [
RedisConfiguration.register({ url: process.env.REDIS_URL }),
],
exports: [RedisConfiguration],
})
export class RedisModule {}

Use registerAsync() when the configuration data comes from another provider or must be resolved asynchronously:

import { Module } from '@nestjs/common';
import { RedisConfiguration } from './redis.configuration';
import { AppConfiguration } from '../configuration/app.configuration';
@Module({
imports: [
RedisConfiguration.registerAsync({
imports: [AppConfigurationModule],
inject: [AppConfiguration],
useFactory: (config: AppConfiguration) => ({
url: config.redis.url,
}),
}),
],
exports: [RedisConfiguration],
})
export class RedisModule {}

Inject the configuration class like any other provider. The injected value is the validated instance — access properties directly:

@Injectable()
export class RedisService {
constructor(private readonly config: RedisConfiguration) {}
connect() {
return createClient({ url: this.config.url });
}
}

The schema static property exposes the underlying Zod schema, which is useful when composing a larger configuration from smaller ones:

export class AppConfiguration extends ZodObject(
z.object({
redis: RedisConfiguration.schema.pick({ url: true }),
app: z.object({ url: z.url() }),
}),
) {}

Input<T> extracts the schema’s input type from a configuration class. Use it when you need to type the raw data before validation — for example, when writing a useFactory that accepts another configuration class:

import { Input } from '@voltage/zod';
RedisConfiguration.registerAsync({
useFactory: (): Input<RedisConfiguration> => ({
url: process.env.REDIS_URL!,
}),
});

ParseObjectPipe validates request data against a class built with ZodObject(). Pass it directly to a NestJS parameter decorator:

import { ParseObjectPipe } from '@voltage/zod';
export class AutocompleteQuery extends ZodObject(
z.object({
filter: z.string().min(0).max(255),
}),
) {}
@Get('companies')
async companies(@Query(ParseObjectPipe) query: AutocompleteQuery) {
return this.service.autocomplete(query.filter);
}

The pipe uses NestJS reflection to read the parameter type and extract its schema. The validated data is instantiated as a class instance, so instanceof checks and any methods you add to the class work as expected.

ParseSchemaPipe accepts a Zod schema inline. Use it when you don’t need a full class:

import { ParseSchemaPipe } from '@voltage/zod';
import { z } from 'zod';
@Get(':id')
async find(@Param('id', new ParseSchemaPipe(z.coerce.number().int())) id: number) {
return this.service.find(id);
}

ParseBody, ParseQuery, and ParseParam are shorthand for combining a parameter decorator with ParseSchemaPipe:

import { ParseBody, ParseQuery, ParseParam } from '@voltage/zod';
@Post()
async create(@ParseBody(CreateOrderSchema) body: CreateOrder) { ... }
@Get()
async list(@ParseQuery(FilterSchema) filter: Filter) { ... }
@Get(':id')
async find(@ParseParam(IdSchema) id: number) { ... }

When validation fails in a pipe, a BadRequestException is thrown with the Zod error message. NestJS’s default exception filter returns a 400 response.

When validation fails during module registration (i.e. in register() or registerAsync()), a ZodObjectParseException is thrown. The error message lists each invalid field and the reason:

ZodObjectParseException: url: Invalid URL

The application will not start until all configuration is valid.