nestjs-zod
v5.0.1
Published
All NestJS + Zod utilities you need
Readme
Core Library Features
Getting Started
Automatic Setup
nestjs-zod can be automatically setup by running the following command:
npx nestjs-zod-cli /path/to/nestjs/projectThis command runs a codemod that adds the validation pipe, serialization interceptor, http exception filter, and swagger cleanup function
Alternatively, you can follow the manual setup steps below
Manual Setup
Install the package:
npm install nestjs-zod # Note: zod ^3.25.0 || ^4.0.0 is also requiredAdd
ZodValidationPipeto theAppModuleZodValidationPipeis required in order to validate the request body, query, and params+ import { APP_PIPE } from '@nestjs/core'; + import { ZodValidationPipe } from 'nestjs-zod'; @Module({ imports: [], controllers: [AppController], providers: [ + { + provide: APP_PIPE, + useClass: ZodValidationPipe, + }, ] }) export class AppModule {}Add
ZodSerializerInterceptorto theAppModuleZodSerializerInterceptoris required in order to validate the response bodies- import { APP_PIPE } from '@nestjs/core'; + import { APP_PIPE, APP_INTERCEPTOR } from '@nestjs/core'; - import { ZodValidationPipe } from 'nestjs-zod'; + import { ZodValidationPipe, ZodSerializerInterceptor } from 'nestjs-zod'; @Module({ imports: [], controllers: [AppController], providers: [ { provide: APP_PIPE, useClass: ZodValidationPipe, }, + { + provide: APP_INTERCEPTOR, + useClass: ZodSerializerInterceptor, + }, ] }) export class AppModule {}[OPTIONAL] Add an
HttpExceptionFilterAn
HttpExceptionFilteris required in order to add custom handling for zod errors- import { APP_PIPE, APP_INTERCEPTOR } from '@nestjs/core'; + import { APP_PIPE, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core'; import { ZodValidationPipe, ZodSerializerInterceptor } from 'nestjs-zod'; + import { HttpExceptionFilter } from './http-exception.filter'; @Module({ imports: [], controllers: [AppController], providers: [ { provide: APP_PIPE, useClass: ZodValidationPipe, }, { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor, }, { provide: APP_FILTER, useClass: HttpExceptionFilter, } ] }) export class AppModule {} + // http-exception.filter + @Catch(HttpException) + export class HttpExceptionFilter extends BaseExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: HttpException, host: ArgumentsHost) { + if (exception instanceof ZodSerializationException) { + const zodError = exception.getZodError(); + if (zodError instanceof ZodError) { + this.logger.error(`ZodSerializationException: ${zodError.message}`); + } + } + + super.catch(exception, host); + } + }[OPTIONAL] Add
cleanupOpenApiDocImportant: This step is important if using
@nestjs/swaggercleanupOpenApiDocis required if using@nestjs/swaggerto properly post-process the OpenAPI doc- SwaggerModule.setup('api', app, openApiDoc); + SwaggerModule.setup('api', app, cleanupOpenApiDoc(openApiDoc));
Check out the example app for a full example of how to integrate nestjs-zod in your nestjs application
Documentation
- Request Validation
- Response Validation
- OpenAPI (Swagger) support
validate(⚠️ DEPRECATED)ZodGuard(⚠️ DEPRECATED)@nest-zod/z(⚠️ DEPRECATED)
Request Validation
createZodDto (Create a DTO from a Zod schema)
function createZodDto<TSchema extends UnknownSchema>(schema: TSchema): ZodDto<TSchema>;Creates a nestjs DTO from a zod schema. These zod DTOs can be used in place of class-validator / class-transformer DTOs. Zod DTOs are responsible for three things:
- Providing a schema for
ZodValidationPipeto validate incoming client data against - Providing a compile-time typescript type from the Zod schema
- Providing an OpenAPI schema when using
nestjs/swagger
[!NOTE] For this feature to work, please ensure
ZodValidationPipeis setup correctly
Parameters
schema- A zod schema. You can "bring your own zod", including zod v3 schemas, v4 schemas, zod mini schemas, etc. The only requirement is that the schema has a method calledparse
Example
Creating a zod DTO
import { createZodDto } from 'nestjs-zod'
import { z } from 'zod'
const CredentialsSchema = z.object({
username: z.string(),
password: z.string(),
})
// class is required for using DTO as a type
class CredentialsDto extends createZodDto(CredentialsSchema) {}Using a zod DTO
@Controller('auth')
class AuthController {
// with global ZodValidationPipe (recommended)
async signIn(@Body() credentials: CredentialsDto) {}
async signIn(@Param() signInParams: SignInParamsDto) {}
async signIn(@Query() signInQuery: SignInQueryDto) {}
// with route-level ZodValidationPipe
@UsePipes(ZodValidationPipe)
async signIn(@Body() credentials: CredentialsDto) {}
}
// with controller-level ZodValidationPipe
@UsePipes(ZodValidationPipe)
@Controller('auth')
class AuthController {
async signIn(@Body() credentials: CredentialsDto) {}
}ZodValidationPipe (Get nestjs to validate using zod)
ZodValidationPipe is needed to ensure zod DTOs actually validate incoming request data when using @Body(), @Params(), or @Query() parameter decorators
When the data is invalid it throws a ZodValidationException.
Example
Globally (recommended)
import { ZodValidationPipe } from 'nestjs-zod'
import { APP_PIPE } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
],
})
export class AppModule {}Locally
import { ZodValidationPipe } from 'nestjs-zod'
// controller-level
@UsePipes(ZodValidationPipe)
class AuthController {}
class AuthController {
// route-level
@UsePipes(ZodValidationPipe)
async signIn() {}
}createZodValidationPipe (Creating custom validation pipe)
export function createZodValidationPipe({ createValidationException }: ZodValidationPipeOptions = {}): ZodValidationPipeClassCreates a custom zod validation pipe
Example
import { createZodValidationPipe } from 'nestjs-zod'
const MyZodValidationPipe = createZodValidationPipe({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})Parameters
params.createValidationException- A callback that will be called with the zod error when a parsing error occurs. Should return a new instance ofError
ZodValidationException
If the zod request parsing fails, then nestjs-zod will throw a ZodValidationException, which will result in the following HTTP response:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{
"code": "too_small",
"minimum": 8,
"type": "string",
"inclusive": true,
"message": "String must contain at least 8 character(s)",
"path": ["password"]
}
]
}You can customize the exception and HTTP response by either 1) creating a custom validation pipe using createZodValidationPipe or 2) handling ZodValidationException inside an exception filter
Here is an example exception filter:
@Catch(ZodValidationException)
export class ZodValidationExceptionFilter implements ExceptionFilter {
catch(exception: ZodValidationException) {
exception.getZodError() // -> ZodError
}
}Response Validation
ZodSerializerDto (Set zod DTO to serialize responses with)
function ZodSerializerDto(dto: ZodDto<UnknownSchema> | UnknownSchema | [ZodDto<UnknownSchema>] | [UnknownSchema])Parses / serializes the return value of a controller method using the provided zod schema. This is especially useful to prevent accidental data leaks.
[!NOTE] Instead of
ZodSerializerDto, consider usingZodResponse, which has some improvements overZodSerializerDto
[!NOTE] For this feature to work, please ensure
ZodSerializerInterceptoris setup correctly
Parameters
options.dto- A ZodDto (or zod schema) to serialize the response with. If passed with array syntax ([MyDto]) then it will parse as an array. Note that the array syntax does not work withzod/mini, because it requires the schema have an.array()method
Example
const UserSchema = z.object({ username: string() })
class UserDto extends createZodDto(UserSchema) {}
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@ZodSerializerDto(UserDto)
getUser(id: number) {
return this.userService.findOne(id)
}
}In the above example, if the userService.findOne method returns password, the password property will be stripped out thanks to the @ZodSerializerDto decorator.
Also note that arrays can be serialized using [] syntax like this:
class BookDto extends createZodDto(z.object({ title: string() })) {}
@Controller('books')
export class BooksController {
constructor() {}
@ZodSerializerDto([BookDto])
getBooks() {
return [{ title: 'The Martian' }, { title: 'Hail Marry' }];
}
}Or by using an array DTO:
class BookListDto extends createZodDto(z.array(z.object({ title: string() }))) {}
@Controller('books')
export class BooksController {
constructor() {}
@ZodSerializerDto(BookListDto)
getBooks() {
return [{ title: 'The Martian' }, { title: 'Hail Marry' }];
}
}ZodSerializerInterceptor (Get nestjs to serialize responses with zod)
To ensure ZodSerializerDto works correctly, ZodSerializerInterceptor needs to be added to the AppModule
[!NOTE] Also see
ZodSerializationExceptionfor information about customizing the serialization error handling
Example
This should be done in the AppModule like so:
@Module({
...
providers: [
...,
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
],
})
export class AppModule {}ZodResponse (Sync run-time, compile-time, and docs-time schemas)
function ZodResponse<TSchema extends UnknownSchema>({ status, description, type }: { status?: number, description?: string, type: ZodDto<TSchema> & { io: "input" } }): (target: object, propertyKey?: string | symbol, descriptor?: Pick<TypedPropertyDescriptor<(...args: any[]) => input<TSchema>|Promise<input<TSchema>>>, 'value'>) => void
function ZodResponse<TSchema extends RequiredBy<UnknownSchema, 'array'>>({ status, description, type }: { status?: number, description?: string, type: [ZodDto<TSchema> & { io: "input" }] }): (target: object, propertyKey?: string | symbol, descriptor?: Pick<TypedPropertyDescriptor<(...args: any[]) => Array<input<TSchema>>|Promise<Array<input<TSchema>>>>, 'value'>) => voidConsolidation of multiple decorators that allows setting the run-time, compile-time, and docs-time schema all at once
[!NOTE] For this feature to work, please ensure
ZodSerializerInterceptorandcleanupOpenApiDocare setup correctly
Parameters
params.status- Optionally sets the "happy-path"statusof the response. If provided, sets the status code using@HttpCodefromnestjs/commonand using@ApiResponsefromnestjs/swaggerparams.description- Optionally sets a description of the response using@ApiResponseparams.type- Sets the run-time (via@ZodSerializerDto), compile-time (via TypeScript), and docs-time (via@ApiResponse) response type.
Example
You may find yourself duplicating type information:
@ZodSerializer(BookDto)
@ApiOkResponse({
status: 200,
type: BookDto
})
getBook(): BookDto {
...
}Here, BookDto is repeated 3 times:
- To set the DTO to use to serialize
- To set the DTO to use for the OpenAPI documentation
- To set the return type for the function
If these 3 spots get out of sync, this may cause bugs. If you want to remove this duplication, you can consolidate using ZodResponse:
- @ZodSerializer(BookDto)
- @ApiOkResponse({
- status: 200,
- type: BookDto.Output
- })
- getBook(): BookDto {
+ @ZodResponse({ type: BookDto })
+ getBook()
...
}@ZodResponse will set all these things. It will set the DTO to use to serialize, it will set the DTO to use for the OpenAPI documentation, and it will throw a compile-time typescript error if the method does not return data that matches the zod input schema
This is pretty powerful, because it ensures the run-time, compile-time, and docs-time representations of your response are all in sync. For this reason, it's recommended to use @ZodResponse instead of repeating the DTO three times.
ZodSerializationException
If the zod response serialization fails, then nestjs-zod will throw a ZodSerializationException, which will result in the following HTTP response:
{
"message": "Internal Server Error",
"statusCode": 500,
}You can customize the exception and HTTP response handling ZodSerializationException inside an exception filter
See the example app here for more information.
OpenAPI (Swagger) support
[!NOTE] For additional documentation, follow Nest.js' Swagger Module Guide, or you can see the example application here
If you have @nestjs/swagger setup, documentation will automatically be generated for:
- Request bodies, if you use
@Body() body: MyDto - Response bodies, if you use
@ApiOkResponse({ type: MyDto.Output })(or@ZodResponse({ type: MyDto })) - Query params, if you use
@Query() query: MyQueryParamsDto
To generate the OpenAPI document, nestjs-zod uses z.toJSONSchema for zod v4 schemas. It's recommended to review the zod documentation itself for more information about how the OpenAPI document is generated
For zod v3 schemas, nestjs-zod uses a custom-built (deprecated) function called zodV3ToOpenAPI that generates the OpenAPI document by inspecting the zod schema directly.
However, please ensure cleanupOpenApiDoc is setup correctly as detailed below
cleanupOpenApiDoc (Ensure proper OpenAPI output)
function cleanupOpenApiDoc(doc: OpenAPIObject, options?: { version?: '3.1' | '3.0' | 'auto' }): OpenAPIObjectCleans up the generated OpenAPI doc by applying some post-processing
[!NOTE] There used to be a function called
patchNestJsSwagger. This function has been replaced bycleanupOpenApiDoc
Parameters
doc- The OpenAPI doc generated bySwaggerModule.createDocumentoptions.version- The OpenAPI version to use while cleaning up the document.auto(default) - Uses the version specified in the OpenAPI document (The version in the OpenAPI can be changed by using thesetOpenAPIVersionmethod on the swagger document builder).3.1- Nullable fields will useanyOfand{ type: 'null' }3.0- Nullable fields will usenullable: true
Example
To complete the swagger integration/setup, cleanupOpenApiDoc needs to be called with the generated open api doc, like so:
const openApiDoc = SwaggerModule.createDocument(app,
new DocumentBuilder()
.setTitle('Example API')
.setDescription('Example API description')
.setVersion('1.0')
.build(),
);
- SwaggerModule.setup('api', app, openApiDoc);
+ SwaggerModule.setup('api', app, cleanupOpenApiDoc(openApiDoc));Output schemas
Note that z.toJSONSchema can generate two versions of any zod schema: "input" or "output". This is what the zod documentation says about this:
Some schema types have different input and output types, e.g. ZodPipe, ZodDefault, and coerced primitives.
Note that by default, when generating OpenAPI documentation, nestjs-zod uses the "input" version of a schema, except for @ZodResponse which always generates the "output" version of a schema. If you want to explicitly use the "output" version of a schema when generating OpenAPI documentation, you can use the .Output property of a zod DTO. For example, this makes sense when using @ApiResponse:
@ApiResponse({
type: MyDto.Output
})However, it's recommended to use @ZodResponse over @ApiResponse, which automatically handles this for you:
@ZodResponse({
type: MyDto // <-- No need to do `.Output` here
})Reusable schemas
You can also externalize and reuse schemas across multiple DTOs. If you add .meta({ id: "MySchema" }) to any zod schema, then that schema will be added directly to components.schemas in the OpenAPI documentation. For example, this code:
const Author = z.object({ name: z.string() }).meta({ id: "Author" })
class BookDto extends createZodDto(z.object({ title: z.string(), author: Author })) { }
class BlogPostDto extends createZodDto(z.object({ title: z.string(), author: Author })) { }Will result in this OpenAPI document:
{
"components": {
"schemas": {
"Author": {
// ...
},
"BookDto": {
"type": "object",
"properties": {
"author": {
"$ref": "#/components/schemas/Author"
},
"required": ["author"]
}
},
"BlogPostDto": {
"type": "object",
"properties": {
"author": {
"$ref": "#/components/schemas/Author"
},
"required": ["author"]
}
}
}
},
// ...
}zodV3ToOpenAPI (DEPRECATED)
[!CAUTION]
zodV3ToOpenAPIis deprecated and will not be supported soon, since zod v4 adds built-in support for generating OpenAPI schemas from zod schemas. See MIGRATION.md for more information.
You can convert any Zod schema to an OpenAPI JSON object:
import { zodToOpenAPI } from 'nestjs-zod'
import { z } from 'zod'
const SignUpSchema = z.object({
username: z.string().min(8).max(20),
password: z.string().min(8).max(20),
sex: z
.enum(['male', 'female', 'nonbinary'])
.describe('We respect your gender choice'),
social: z.record(z.string().url())
})
const openapi = zodV3ToOpenAPI(SignUpSchema)The output will be the following:
{
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 8,
"maxLength": 20
},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 20
},
"sex": {
"description": "We respect your gender choice",
"type": "string",
"enum": ["male", "female", "nonbinary"]
},
"social": {
"type": "object",
"additionalProperties": {
"type": "string",
"format": "uri"
}
},
"birthDate": {
"type": "string",
"format": "date-time"
}
},
"required": ["username", "password", "sex", "social", "birthDate"]
}validate (DEPRECATED)
[!CAUTION]
validateis deprecated and will not be supported soon. It is recommended to use.parsedirectly. See MIGRATION.md for more information.
If you don't like ZodGuard and ZodValidationPipe, you can use validate function:
import { validate } from 'nestjs-zod'
validate(wrongThing, UserDto, (zodError) => new MyException(zodError)) // throws MyException
const validatedUser = validate(
user,
UserDto,
(zodError) => new MyException(zodError)
) // returns typed value when succeedZodGuard (DEPRECATED)
[!CAUTION] Guard-related functions are deprecated and will not be supported soon. It is recommended to use guards for authorization, not validation. See MIGRATION.md for more information.
[!CAUTION]
ZodGuardis deprecated and will not be supported soon. It is recommended to use guards for authorization, not validation. See MIGRATION.md for more information.
Sometimes, we need to validate user input before specific Guards. We can't use Validation Pipe since NestJS Pipes are always executed after Guards.
The solution is ZodGuard. It works just like ZodValidationPipe, except for that is doesn't transform the input.
It has 2 syntax forms:
@UseGuards(new ZodGuard('body', CredentialsSchema))@UseZodGuard('body', CredentialsSchema)
Parameters:
- The source -
'body' | 'query' | 'params' - Zod Schema or DTO (just like
ZodValidationPipe)
When the data is invalid - it throws ZodValidationException.
import { ZodGuard } from 'nestjs-zod'
// controller-level
@UseZodGuard('body', CredentialsSchema)
@UseZodGuard('params', CredentialsDto)
class MyController {}
class MyController {
// route-level
@UseZodGuard('query', CredentialsSchema)
@UseZodGuard('body', CredentialsDto)
async signIn() {}
}createZodGuard (Creating custom guard)
[!CAUTION]
createZodGuardis deprecated and will not be supported soon. It is recommended to use guards for authorization, not validation. See MIGRATION.md for more information.
import { createZodGuard } from 'nestjs-zod'
const MyZodGuard = createZodGuard({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})@nest-zod/z (DEPRECATED)
[!CAUTION]
@nest-zod/zis no longer supported and has no impact on the OpenAPI generation. It is recommended to usezoddirectly. See MIGRATION.md for more information.
@nest-zod/z provides a special version of Zod. It helps you to validate the user input more accurately by using our custom schemas and methods.
ZodDateString
[!CAUTION]
@nest-zod/zis no longer supported and has no impact on the OpenAPI generation. It is recommended to usezoddirectly. See MIGRATION.md for more information.
In HTTP, we always accept Dates as strings. But default Zod only has validations for full date-time strings. ZodDateString was created to address this issue.
// 1. Expect user input to be a "string" type
// 2. Expect user input to be a valid date (by using new Date)
z.dateString()
// Cast to Date instance
// (use it on end of the chain, but before "describe")
z.dateString().cast()
// Expect string in "full-date" format from RFC3339
z.dateString().format('date')
// [default format]
// Expect string in "date-time" format from RFC3339
z.dateString().format('date-time')
// Expect date to be the past
z.dateString().past()
// Expect date to be the future
z.dateString().future()
// Expect year to be greater or equal to 2000
z.dateString().minYear(2000)
// Expect year to be less or equal to 2025
z.dateString().maxYear(2025)
// Expect day to be a week day
z.dateString().weekDay()
// Expect year to be a weekend
z.dateString().weekend()Valid date format examples:
2022-05-15
Valid date-time format examples:
2022-05-02:08:33Z2022-05-02:08:33.000Z2022-05-02:08:33+00:002022-05-02:08:33-00:002022-05-02:08:33.000+00:00
Errors:
invalid_date_string- invalid dateinvalid_date_string_format- wrong formatPayload:
expected-'date' | 'date-time'
invalid_date_string_direction- not past/futurePayload:
expected-'past' | 'future'
invalid_date_string_day- not weekDay/weekendPayload:
expected-'weekDay' | 'weekend'
too_smallwithtype === 'date_string_year'too_bigwithtype === 'date_string_year'
ZodPassword
[!CAUTION]
@nest-zod/zis no longer supported and has no impact on the OpenAPI generation. It is recommended to usezoddirectly. See MIGRATION.md for more information.
ZodPassword is a string-like type, just like the ZodDateString. As you might have guessed, it's intended to help you with password schemas definition.
Also, ZodPassword has a more accurate OpenAPI conversion, comparing to regular .string(): it has password format and generated RegExp string for pattern.
// Expect user input to be a "string" type
z.password()
// Expect password length to be greater or equal to 8
z.password().min(8)
// Expect password length to be less or equal to 100
z.password().max(100)
// Expect password to have at least one digit
z.password().atLeastOne('digit')
// Expect password to have at least one lowercase letter
z.password().atLeastOne('lowercase')
// Expect password to have at least one uppercase letter
z.password().atLeastOne('uppercase')
// Expect password to have at least one special symbol
z.password().atLeastOne('special')Errors:
invalid_password_no_digitinvalid_password_no_lowercaseinvalid_password_no_uppercaseinvalid_password_no_specialtoo_smallwithtype === 'password'too_bigwithtype === 'password'
Credits
This library was originally created by risen228 and now maintained by BenLorantfy
