Skip to main content

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-validator 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);