DEV Community

Cover image for Standardize and Validate Incoming Data Using Pipes in NestJS
Thien Pham
Thien Pham

Posted on

Standardize and Validate Incoming Data Using Pipes in NestJS

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
 }
Enter fullscreen mode Exit fullscreen mode
  • 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);
  }
Enter fullscreen mode Exit fullscreen mode
  • There are many built-in Pipes:
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

1. Create a Dto

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

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

  @IsOptional()
  phone?: string;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode
  • CatController
  @Post()
  async createCat(@Body() cat: CreateCatDto) {
    return this.catService.create(cat);
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Use the new custom pipes in the controller

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

4. Result

  • It'll return error if there is missing fields

Image description

  • Successfully response

Image description

Use Zod

npm install --save zod
Enter fullscreen mode Exit fullscreen mode

1. Create Zod Dto

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

export type ZodCreateCatDto = z.infer<typeof createCatSchema>;
Enter fullscreen mode Exit fullscreen mode

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');
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

3. Update CatController

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

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.

Top comments (0)