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.
- Sample git repo: nest-pipes
- More information: pipes
- Class validator: class-validator
- Zod: Zod
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")
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 fromstring
tonumber
- If you pass a string
abc
, theParseIntPipe
will validate & throw an error back to the client. This means that the pipe validates for us.
- 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
- With a correct phone number
- 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
- Successfully response
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
- With name, it’ll return successful response
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