Getting Started
Install
Install nest-zod and its required peer dependencies:
sh
npm install nest-zod zod @nestjs/common rxjssh
yarn add nest-zod zod @nestjs/common rxjssh
pnpm add nest-zod zod @nestjs/common rxjssh
bun add nest-zod zod @nestjs/common rxjssh
deno add npm:nest-zod npm:zod npm:@nestjs/common npm:rxjsIf you also want generated Swagger metadata:
sh
npm install @nestjs/swaggersh
yarn add @nestjs/swaggersh
pnpm add @nestjs/swaggersh
bun add @nestjs/swaggersh
deno add npm:@nestjs/swaggerRecommended First Controller
If your app already uses Swagger, start with nest-zod/swagger:
ts
// items.schemas.ts
import { z } from 'zod';
export const createItemSchema = z.object({
title: z.string().trim().min(1),
});
export const listItemsQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
});
export const itemResponseSchema = z.object({
id: z.uuid(),
title: z.string(),
});
export type CreateItemDto = z.infer<typeof createItemSchema>;
export type ListItemsQueryDto = z.infer<typeof listItemsQuerySchema>;
export type ItemResponseDto = z.infer<typeof itemResponseSchema>;ts
// items.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { z } from 'zod';
import { ZBody, ZParam, ZQuery, ZSerialize } from 'nest-zod/swagger';
import type {
CreateItemDto,
ItemResponseDto,
ListItemsQueryDto,
} from './items.schemas';
import {
createItemSchema,
itemResponseSchema,
listItemsQuerySchema,
} from './items.schemas';
@Controller('items')
export class ItemsController {
@Post()
@ZSerialize(itemResponseSchema)
create(@ZBody(createItemSchema) body: CreateItemDto): ItemResponseDto {
return {
id: '550e8400-e29b-41d4-a716-446655440000',
title: body.title,
};
}
@Get(':id')
@ZSerialize(itemResponseSchema)
get(@ZParam('id', z.uuid()) id: string): ItemResponseDto {
return {
id,
title: 'Widget',
};
}
@Get()
list(@ZQuery(listItemsQuerySchema) query: ListItemsQueryDto) {
return {
page: query.page,
items: [],
};
}
}No Global Registration Needed
The decorators wire themselves up directly, so you do not need to register a global pipe or interceptor for this to work.
What Happens at Runtime
- the
POSTbody is parsed with Zod beforecreate()runs - the
GET :idpath param is validated as a UUID - the
pagequery string is coerced from a string into a positive integer - the response is passed through the schema before Nest sends it
When you use nest-zod/swagger:
ZQuery(schema)documents one query parameter per object propertyZQuery('name', schema)documents a named query parameter, including object-shaped valuesZSerializedocuments the route's effective success status instead of always forcing200
If You Do Not Use Swagger
Change only the import:
ts
import { ZBody, ZParam, ZQuery, ZSerialize } from 'nest-zod';Everything else stays the same. See Runtime Only for that version.