How to use class-validator in Nextjs
When developing with Next.js, how do you validate the data that comes through API routes or forms? Unlike frameworks like Express or NestJS, Next.js doesn’t provide a built-in validation layer. This means developers have to write their own code to ensure data integrity.
In this post, I’ll show you how to integrate the familiar class-validator library—popular among NestJS developers—into a Next.js project. Step by step, we’ll explore how you can write cleaner, more maintainable, and reliable validation logic using decorator-based validation.
From my personal experience, introducing class-validator significantly improved both the stability of the product and the ease of development. While Next.js isn’t the most convenient tool for building APIs, a well-designed architecture can still make it a capable API server framework.
This article is based on Next.js version 15. Please keep that in mind. 😉
Why Use class-validator in Next.js?
When handling API request bodies in Next.js, manually checking whether each field is of the correct type or missing can quickly become tedious.
// Basic validation logic
if (!body.email || typeof body.email !== 'string') {
// Handle error...
}
if (!body.password || body.password.length < 6) {
// Handle error...
}
This kind of code becomes messy, and every time you add a new field, you have to update your validation logic accordingly.
By using class-validator, you can avoid that mess. With a DTO (Data Transfer Object) class and decorators, you can declare validation rules in a declarative and elegant way, making your code more readable and intuitive.
Setting Up class-validator
Let’s dive into how to set up class-validator in your Next.js project.
1. Install Required Packages
First, install the necessary packages for validation.
npm install class-validator class-transformer reflect-metadata
class-validator: The decorator-based validation library.class-transformer: Converts plain JavaScript objects into class instances. This step is essential for decorators to work properly.reflect-metadata: Helps decorators read type information at runtime.
You’ll also need Babel plugins to support decorators properly.
npm install --save-dev @babel/core babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators babel-plugin-parameter-decorator
2. Configure tsconfig.json
Edit your tsconfig.json to allow decorators and emit metadata, so TypeScript handles them correctly.
// tsconfig.json
{
"compilerOptions": {// ...existing settings"experimentalDecorators": true,"emitDecoratorMetadata": true
}
}
experimentalDecorators: Enables the use of decorators in TypeScript.emitDecoratorMetadata: Generates metadata thatreflect-metadatacan use at runtime.
3. Configure .babelrc
Create a .babelrc file in your project root and add the necessary plugins for decorators.
// .babelrc
{
"presets": ["next/babel"],
"plugins": ["babel-plugin-transform-typescript-metadata",["@babel/plugin-proposal-decorators", { "legacy": true }],"babel-plugin-parameter-decorator",[ "@babel/plugin-transform-runtime", { "regenerator": true }]
]
}
4. Import reflect-metadata Globally
reflect-metadata should be imported once at the entry point of your app. Ideally, do this at the top of a common utility file like src/lib/validation.ts.
// At the top of src/lib/validation.ts or app/api/.../route.ts
import "reflect-metadata";
// ...other code
Defining DTOs (Data Transfer Objects)
Now, let’s create a DTO class to define our validation rules. Create a file at src/dto/create-user.dto.ts:
// src/dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsOptional, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: 'Name is required.' })
name: string;
@IsEmail({}, { message: 'Invalid email format.' })
email: string;
@MinLength(6, { message: 'Password must be at least 6 characters long.' })
password: string;
@IsNotEmpty({ message: 'School is required.' })
school: string;
@IsNotEmpty({ message: 'Phone number is required.' })
phoneNumber: string;
@IsOptional()
introduce?: string;
}
Decorators like @IsEmail() and @MinLength(6) allow you to express validation rules simply. Adding custom messages makes your API responses more user-friendly.
Creating a Validation Utility
Since Next.js lacks built-in support like NestJS’s ValidationPipe, you need to create your own validation utility function. Add this function to src/lib/validation.ts:
// src/lib/validation.ts
import 'reflect-metadata';
import { validate, ValidationError } from 'class-validator';
import { plainToInstance } from 'class-transformer';
export async function validateDto<T extends object>(
dtoClass: new () => T,
payload: unknown,
): Promise<{ instance: T; errors: string[] }> {
const instance = plainToInstance(dtoClass, payload);
const errors: ValidationError[] = await validate(instance);
if (errors.length > 0) {const errorMessages = errors .map((err) => Object.values(err.constraints || {})) .flat();return { instance, errors: errorMessages };
}
return { instance, errors: [] };
}
This function does two main things:
plainToInstance(dtoClass, payload): Converts a plain object to an instance of the DTO class, enabling the decorators.validate(instance): Validates the DTO instance and returns any errors found.
Using DTO in an API Route
Let’s use the DTO and validation utility in an API route. Here’s an example app/api/users/route.ts:
// app/api/users/route.ts
import { NextResponse } from 'next/server';
import { CreateUserDto } from '@/dto/create-user.dto';
import { validateDto } from '@/lib/validation';
export async function POST(req: Request) {
try {const body = await req.json();const { instance, errors } = await validateDto(CreateUserDto, body);
if (errors.length > 0) { // Validation failed return NextResponse.json( { message: 'Validation failed', errors }, { status: 400 }, );}
// Validation passed → proceed to DB or business logic// …
return NextResponse.json({ message: '✅ Validation passed', data: instance,});
} catch (err: any) {// Handle parse errors or other exceptionsreturn NextResponse.json( { message: 'An unexpected error occurred' }, { status: 500 },);
}
}
The POST request parses the body and passes it to validateDto. If there are errors, it returns a 400 response. If validation passes, the instance can safely be used for further logic. Since instance is typed as CreateUserDto, type safety is guaranteed.
Adding a Controller
You can further organize your logic by separating route and controller:
class UserController {
async createUser(payload: CreateUserDto) {await guardAdmin();await validateDto(CreateUserDto, payload);
return userService.createUserByAdmin(payload);
}
}
This allows for better separation of concerns. Similarly, you could create your own Service and Repository layers.
For a Better Development Experience
Using class-validator in Next.js does more than just reduce validation boilerplate:
- Improved readability and maintainability: Validation rules are clearly declared in the DTOs.
- Type safety:
plainToInstanceensures that your objects are typed properly, reducing bugs. - Declarative programming: You focus on what to validate, not how to do it.
While initial setup requires some effort, the added stability and developer convenience make class-validator a worthy investment. Try using it in your Next.js projects for more robust, type-safe applications.
References
- How to Use class-validator in a Next.js App
- And of course, my own code 😉