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.
Installation
Section titled “Installation”yarn add @voltage/zod zod reflect-metadataConfiguration classes
Section titled “Configuration classes”Defining a class
Section titled “Defining a class”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).
register()
Section titled “register()”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 {}registerAsync()
Section titled “registerAsync()”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 {}Injecting configuration
Section titled “Injecting configuration”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 }); }}Reusing a schema
Section titled “Reusing a schema”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() }), }),) {}The Input<T> utility type
Section titled “The Input<T> utility type”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!, }),});Request validation
Section titled “Request validation”ParseObjectPipe
Section titled “ParseObjectPipe”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
Section titled “ParseSchemaPipe”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);}Convenience decorators
Section titled “Convenience decorators”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) { ... }Validation errors
Section titled “Validation errors”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 URLThe application will not start until all configuration is valid.