Validation
Introduction​
It's best practice to validate the incoming data, therefore you ensure that it complies with the requirements of your application. We provide several built-in pipes to validate incoming values. However, we are only going to focus on the ValidationPipe
here. You can read about the others in Pipes section.
class
vs interface
​
If you're a TypeScript user, in theory you can use interfaces to determine your request body shapes, but since interfaces are removed during compile time, it's not possible to gather information about them. On the other hand, classes are part of the ES6 standard which means they don't get removed during compile time, therefore they exist in the compiled Javascript and we can collect information about them and their properties, which ValidationPipe
heavily relies on.
ValidationPipe​
The ValidationPipe
uses class-validator
to validate and class-transformer
to transform the incoming request body.
Usage​
We first create our CreateUserDTO
DTO (data transfer object) to define the rules we need.
import { IsNotEmpty, IsEmail } from 'class-validator';
export class CreateUserDTO {
@IsEmail()
email: string;
@IsNotEmpty()
fullName: string;
}
And, later we make use of the CreateUserDTO
in conjunction with ValidationPipe
in our route handler.
// pages/api/user.ts
class UserHandler {
@Post()
createUser(@Body(ValidationPipe) body: CreateUserDTO) {
return await DB.createUser(body);
}
}
export default createHandler(UserHandler);
Nested data​
When your application expects a nested JSON object, you can easily define its shape in your DTOs and validate the incoming data against it.
import { createHandler, Body, Post, ValidationPipe } from 'next-api-decorators';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsNumber, MinLength, ValidateNested } from 'class-validator';
class Coordinate {
@IsNotEmpty()
@IsNumber()
lat: number;
@IsNotEmpty()
@IsNumber()
lng: number;
}
class MapMarker {
@IsNotEmpty()
@MinLength(3)
label: string;
@Type(() => Coordinate)
@ValidateNested()
@IsNotEmpty()
coordinates: Coordinate;
}
class LocationHandler {
@Post()
saveLocation(@Body(ValidationPipe) body: MapMarker) {
// Do something with the data.
return `Location "${body.label}" saved.`;
}
}
export default createHandler(LocationHandler);
Configuration​
The options you can pass into ValidationPipe
are inherited from class-validator
with an additional transformerOptions
property, which inherits class-transformer
's plainToClass
options.
🔗 class-transformer
options
ZodValidationPipe​
The ZodValidationPipe
uses zod
to validate and transform the incoming request body.
Usage​
We first create our createUserSchema
(zod schema) to define the rules we need and from that schema we create our DTO.
import { createUserSchema } from "next-api-decorators";
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email();
fullName: z.string().min(1);
});
export class CreateUserDTO extends createZodDto(createUserSchema) {};
And, later we make use of the CreateUserDTO
in conjunction with ZodValidationPipe
in our route handler.
// pages/api/user.ts
class UserHandler {
@Post()
createUser(@Body(ZodValidationPipe) body: CreateUserDTO) {
return await DB.createUser(body);
}
}
export default createHandler(UserHandler);
Nested data​
The equivalent of the class-validator
and class-transformer
would be
import { createHandler, createZodDto, Body, Post, ZodValidationPipe } from 'next-api-decorators';
import { z } from "zod";
const coordinateSchema = z.object({
lat: z.number(),
lng: z.number()
});
const mapMarkerSchema z.object({
label: z.string().min(3),
coordinates: coordinateSchema
});
export class MapMarker extends createZodDto(mapMarkerSchema) {};
class LocationHandler {
@Post()
saveLocation(@Body(ZodValidationPipe) body: MapMarker) {
// Do something with the data.
return `Location "${body.label}" saved.`;
}
}
export default createHandler(LocationHandler);