How to use class-validator in Nextjs

How to use class-validator in Nextjs

D
dongAuthor
6 min read

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 that reflect-metadata can 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:

  1. plainToInstance(dtoClass, payload): Converts a plain object to an instance of the DTO class, enabling the decorators.
  2. 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: plainToInstance ensures 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 Nextjs | devdong