✨ A seamless validation solution for your NestJS application ✨
by @benlorantfy
✨ Create nestjs DTOs from zod schemas
✨ Validate / parse request body, query params, and url params using zod
✨ Serialize response bodies using zod
✨ Automatically generate OpenAPI documentation using zod
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
-
Install the package:
npm install nestjs-zod # Note: zod ^3.25.0 || ^4.0.0 is also required -
Add
ZodValidationPipeto theAppModuleShow me how
ZodValidationPipeis 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 theAppModuleShow me how
ZodSerializerInterceptoris 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
HttpExceptionFilterShow me how
An
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/swaggerShow me how
cleanupOpenApiDocis 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
- Request Validation
- Response Validation
- OpenAPI (Swagger) support
validate(⚠️ DEPRECATED)ZodGuard(⚠️ DEPRECATED)@nest-zod/z(⚠️ DEPRECATED)
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 ZodValidationPipe is setup correctly
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
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) {}@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 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.
import { ZodValidationPipe } from 'nestjs-zod'
import { APP_PIPE } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
],
})
export class AppModule {}import { ZodValidationPipe } from 'nestjs-zod'
// controller-level
@UsePipes(ZodValidationPipe)
class AuthController {}
class AuthController {
// route-level
@UsePipes(ZodValidationPipe)
async signIn() {}
}export function createZodValidationPipe({ createValidationException }: ZodValidationPipeOptions = {}): ZodValidationPipeClassCreates a custom zod validation pipe
import { createZodValidationPipe } from 'nestjs-zod'
const MyZodValidationPipe = createZodValidationPipe({
// provide custom validation exception factory
createValidationException: (error: ZodError) =>
new BadRequestException('Ooops'),
})params.createValidationException- A callback that will be called with the zod error when a parsing error occurs. Should return a new instance ofError
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
}
}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 using ZodResponse, which has some improvements over ZodSerializerDto
Note
For this feature to work, please ensure ZodSerializerInterceptor is setup correctly
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
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' }];
}
}To ensure ZodSerializerDto works correctly, ZodSerializerInterceptor needs to be added to the AppModule
Note
Also see ZodSerializationException for information about customizing the serialization error handling
This should be done in the AppModule like so:
@Module({
...
providers: [
...,
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
],
})
export class AppModule {}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 ZodSerializerInterceptor and cleanupOpenApiDoc are setup correctly
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.
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.
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.
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
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 by cleanupOpenApiDoc
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
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));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
})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:
Caution
zodV3ToOpenAPI is 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.
Show documentation for deprecated APIs
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"]
}Caution
validate is deprecated and will not be supported soon. It is recommended to use .parse directly. See MIGRATION.md for more information.
Show documentation for deprecated APIs
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 succeedCaution
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.
Show documentation for deprecated APIs
[!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() {}
}[!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'),
})Caution
@nest-zod/z is no longer supported and has no impact on the OpenAPI generation. It is recommended to use zod directly. See MIGRATION.md for more information.
Show documentation for deprecated package
@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.
[!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 date -
invalid_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'
[!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'
This library was originally created by risen228 and now maintained by BenLorantfy
{ "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"] } } } }, // ... }