Search code examples
typescriptnestjszod

How to create custom validation pipe for NestJs that will use Zod as validator


I want to build a custom validator for NestJs (using v10) that will allow doing following

  1. Use Zod schema to validate
  2. Allows Creation of Dto from Zod Schema
  3. Works with File Upload (Nest's built-in FileInterceptor, UploadedFile & ParseFilePipe) etc

Following is current implementation

Validation Pipe

// zod-validation.pipe.ts (bound globally from app module)
import { Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodDtoClass } from '../utils/zod-dto.interface';

@Injectable()
export class ZodValidationPipe {
  transform(value: Record<string, unknown>, metadata: ArgumentMetadata) {
    const schemaClass: typeof ZodDtoClass = metadata.metatype! as unknown as typeof ZodDtoClass;

    if (!schemaClass.schema) return;

    const result = schemaClass.schema.safeParse(value);
    if (!result.success) {
      throw new BadRequestException(result.error.flatten());
    }

    return { data: result.data };
  }
}

Dto

// create-feed.dto.ts
import { z } from 'zod';
import { ZodDtoClass } from '../../utils/zod-dto.interface';

const schema = z
  .object({
    title: z.string().min(5).max(35).nullish(),
    body: z.string().max(140).nullish(),
  })
  .refine(
    (d) => {
      return !!(d.title || d.body);
    },
    {
      path: ['title'],
      message: "Title, Body and attachment can't be empty at the same time",
    },
  );

export class CreateFeedDto extends ZodDtoClass<typeof schema> {
  static override schema = schema;
}

Controller method

  @UseInterceptors(FileInterceptor('attachment'))
  @Post('/create')
  create(
    @Body() createFeedDto: CreateFeedDto,
    @UploadedFile(
      new ParseFilePipe({
        validators: [
          new MaxFileSizeValidator({ maxSize: 5000000 }),
          new FileTypeValidator({ fileType: '(jpg|png)$' }),
        ],
      }),
    )
    attachment: Express.Multer.File,
  ) {
    console.log({
      createFeedDto: createFeedDto,
      attachment,
    });

    return this.feedsService.create(createFeedDto);
  }

After request from postman with all fields (with jpg file). Following is the result I'm getting

{
  createFeedDto: { data: { title: 'hello', body: 'This is body text' } },
  attachment: undefined
}

FYI: I've already used all of the packages available that can do a similar thing & I found all of them have different problems and don't fulfill my requirements. So I want to create a new one


Solution

  • In your implementation of the pipe, in the if(!shcemaClass.schema) you should use return value to ensure that the original value is still returned rather than an undefined object. You really should read the docs on pipe and understand what my pipe is doing before saying it doesn't work