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.
Top comments (0)