Standardize and Validate Incoming Data Using Pipes in NestJS



This content originally appeared on DEV Community and was authored by Thien Pham

If you want to validate & transform the incoming Data before they go into the routes, then you can use Pipes.

Transform & Validate Incoming Data

1. Firstly, Create CatService

export type Cat = {
  id: number;
  name: string;
};

@Injectable()
export class CatService {
  cats: Cat[] = [
    { id: 1, name: 'Cat 1' },
    { id: 2, name: 'Cat 2' },
    { id: 3, name: 'Cat 3' },
  ];

  findOne(id: number): Cat {
    return this.cats.find(cat => cat.id === id);
  }
}

2. Creates CatController

@Controller('cat')
export class CatController {
  constructor(private readonly catService: CatService) {}

  @Get(':id')
  getCat(@Param('id') id: number): Cat {
    return this.catService.findOne(id);
  }
}

3. First call, the response will be empty because the id is a string ("1")

Image description

4. Utilize ParseIntPipe for Transformation and Validation

 @Get(':id')
 getCat(@Param('id', ParseIntPipe) id: number): Cat {
   return this.catService.findOne(id);
 }
  • The response is correct now. This means that when we use ParseIntPipe, it will transform the param from string to number

Image description

  • If you pass a string abc, the ParseIntPipe will validate & throw an error back to the client. This means that the pipe validates for us.

Image description

  • You can also use Pipe for a @Query().
  @Get(':id')
  getCat(@Query('id', ParseIntPipe) id: number): Cat {
    return this.catService.findOne(id);
  }
  • There are many built-in Pipes:
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe

Custom Pipes

1. Update CatService to add a phone for cats =))

export type Cat = {
  id: number;
  name: string;
  phone?: string;
};

@Injectable()
export class CatService {
  cats: Cat[] = [
    { id: 1, name: 'Cat 1' },
    { id: 2, name: 'Cat 2' },
    { id: 3, name: 'Cat 3' },
  ];

  findOne(id: number): Cat {
    return this.cats.find(cat => cat.id === id);
  }

  addPhone(id: number, phone: string): Cat {
    const cat = this.findOne(id);
    cat.phone = phone;
    return cat;
  }
}

2. Create a custom pipe PhoneValidatePipe

import { ArgumentMetadata, PipeTransform } from '@nestjs/common';

export class PhoneValidatePipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (!value) {
      throw new Error('Phone number is required');
    }
    if (value.length !== 10) {
      throw new Error('Phone number must be 10 digits');
    }
    return '+' + value; // Transform
  }
}

3. Add a new method to CatController

 @Post(':id/phone')
  addPhone(
    @Param('id', ParseIntPipe) id: number,
    @Body('phone', new PhoneValidatePipe()) phone: string,
  ): Cat {
    return this.catService.addPhone(id, phone);
  }

4. Check the response

  • With a wrong phone number

Image description

  • With a correct phone number

Image description

  • So, the Pipes is helping use to validate & transform the incoming data.

Validate a Schema

If you want to validate an object, there are two solutions:

  • Use class-validator
  • Use Zod

Use class-validator

$ npm i --save class-validator class-transformer

1. Create a Dto

import { IsNotEmpty, IsOptional, IsString } from 'class-validator';

export class CreateCatDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsOptional()
  phone?: string;
}

2. Update CatController & CatService to add a new Cat

  • CatService
  create(cat: CreateCatDto): Cat {
    const newCat = {
      id: this.cats.length + 1,
      ...cat,
    };
    this.cats.push(newCat);
    return newCat;
  }
  • CatController
  @Post()
  async createCat(@Body() cat: CreateCatDto) {
    return this.catService.create(cat);
  }

3. Create RequestValidationPipe & extends it from PipeTransform like the PhoneValidatePipe above to validate and transform data

import { ArgumentMetadata, PipeTransform } from '@nestjs/common';
import { validateOrReject, ValidationError } from 'class-validator';
import { plainToInstance } from 'class-transformer';

export class RequestValidationPipe implements PipeTransform {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    try {
      // validateOrReject & plainToInstance to validate the incoming data
      await validateOrReject(plainToInstance(metatype, value));
    } catch (e) {
      // Format the message response to clients
      if (!(e instanceof Array)) throw e;
      const errors = e.map(errorItem => {
        if (!(errorItem instanceof ValidationError)) throw e;
        return errorItem;
      });
      const message = errors
        .map(error => Object.values(error.constraints))
        .join(', ');
      throw new Error(message);
    }
    return value;
  }
}

4. Use the new custom pipes in the controller

  @Post()
  @UsePipes(new RequestValidationPipe())
  async createCat(@Body() cat: CreateCatDto) {
    return this.catService.create(cat);
  }

4. Result

  • It’ll return error if there is missing fields

Image description

  • Successfully response

Image description

Use Zod

npm install --save zod

1. Create Zod Dto

export const createCatSchema = z
  .object({
    name: z.string(),
    phone: z.string().optional(),
  })
  .required();

export type ZodCreateCatDto = z.infer<typeof createCatSchema>;

2. Create ZodValidationPipe & still extends from PipeTransform

import {
  ArgumentMetadata,
  BadRequestException,
  PipeTransform,
} from '@nestjs/common';
import { ZodSchema } from 'zod';

export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    try {
      return this.schema.parse(value);
    } catch (error) {
      throw new BadRequestException('Validation failed');
    }
  }
}

3. Update CatController

 @Post()
  @UsePipes(new ZodValidationPipe(createCatSchema))
  async createCat(@Body() cat: CreateCatDto) {
    return this.catService.create(cat);
  }

4. Result

  • Without name, it’ll return the bad request message

Image description

  • With name, it’ll return successful response

Image description

Summary

Both class-validator and Zod enhance data validation in NestJS by providing robust, flexible, and type-safe mechanisms, ultimately leading to cleaner code and better error management. Choosing between them often depends on specific project requirements and developer preferences.


This content originally appeared on DEV Community and was authored by Thien Pham