DTOs and Validation with class-validator in NestJS

In modern web development, validating incoming data is crucial for ensuring that your application behaves as expected and maintains data integrity. DTOs (Data Transfer Objects) are used to define the structure of incoming data, and validation ensures that the data is in the right format. In this article, we’ll dive into how to work with DTOs and validation using the class-validator library in NestJS.

Table of Contents

  1. Introduction
  2. What are DTOs (Data Transfer Objects)?
  3. Setting Up Validation with class-validator
  4. Creating a DTO Class
  5. Common class-validator Decorators
  6. Using DTOs and Validation in Controllers
  7. Custom Validation Messages
  8. Global vs Local Validation
  9. Transforming Data with class-transformer
  10. Creating Custom Validation Decorators
  11. Error Handling in Validation
  12. Best Practices for Validation

Introduction

Data validation is a crucial aspect of building secure and reliable applications. In NestJS, validation of incoming data is achieved using DTOs (Data Transfer Objects) and the class-validator package. DTOs define the shape of data coming into your application, while validation ensures that the data adheres to expected formats, preventing issues like incorrect data types or malformed input.

In this article, we’ll explore how to create DTOs, apply validation rules, and handle validation errors using class-validator in a NestJS application.

What are DTOs (Data Transfer Objects)?

A DTO (Data Transfer Object) is a design pattern used to define the structure of data transferred between different parts of an application. DTOs are typically used to define the shape of incoming requests (like POST or PUT) or outgoing responses.

In NestJS, DTOs are usually implemented as classes that contain properties representing the data fields. They can also include validation rules to ensure the data is valid.

For example, you might have a CreateUserDto for creating new users, with properties like firstName, lastName, email, and age.

Setting Up Validation with class-validator

Before you can use class-validator, you need to install it in your NestJS project.

Install Dependencies

To install the necessary dependencies, run the following command:

bashCopyEditnpm install class-validator class-transformer
  • class-validator provides decorators to define validation rules.
  • class-transformer helps in transforming the plain JavaScript objects into class instances.

Enable Global Validation

To ensure that all incoming requests are validated automatically, you need to enable the ValidationPipe globally in the application. This can be done in your main.ts file.

typescriptCopyEditimport { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}

bootstrap();

The ValidationPipe will automatically validate all incoming data based on the DTOs used in the request bodies.

Creating a DTO Class

Here’s an example of a simple DTO that validates the data for creating a user:

typescriptCopyEditimport { IsString, IsInt, IsEmail, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsOptional()
  firstName: string;

  @IsString()
  lastName: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(18)
  @Max(100)
  age: number;
}

In this example:

  • @IsString() ensures that firstName and lastName are strings.
  • @IsEmail() validates that email is a valid email address.
  • @IsInt(), @Min(), and @Max() are used to ensure that age is an integer between 18 and 100.
  • @IsOptional() makes firstName optional.

Common class-validator Decorators

Here are some commonly used decorators provided by class-validator:

  • @IsString(): Ensures the field is a string.
  • @IsInt(): Ensures the field is an integer.
  • @Min(value: number): Ensures the field is greater than or equal to the specified value.
  • @Max(value: number): Ensures the field is less than or equal to the specified value.
  • @IsEmail(): Validates that the field is a valid email address.
  • @IsOptional(): Marks a field as optional.
  • @IsNotEmpty(): Ensures the field is not empty.

Using DTOs and Validation in Controllers

Once you’ve defined your DTOs, you can use them in your controllers to validate incoming requests. Here’s how to use the CreateUserDto in a controller:

typescriptCopyEditimport { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UserController {
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return `User ${createUserDto.firstName} created successfully!`;
  }
}

In this example, the createUserDto is automatically validated against the CreateUserDto class, and any validation errors will result in a 400 Bad Request response.

Custom Validation Messages

You can provide custom validation error messages using the message option. This helps make the error messages more user-friendly.

typescriptCopyEditexport class CreateUserDto {
  @IsString({ message: 'First name must be a string' })
  @IsOptional()
  firstName: string;

  @IsString({ message: 'Last name must be a string' })
  lastName: string;

  @IsEmail({}, { message: 'Email must be a valid email address' })
  email: string;

  @IsInt({ message: 'Age must be an integer' })
  @Min(18, { message: 'Age must be at least 18' })
  @Max(100, { message: 'Age cannot exceed 100' })
  age: number;
}

Global vs Local Validation

You can apply validation globally to all routes, or locally to specific routes. Here’s how to apply the ValidationPipe globally, which we already discussed:

Global Validation (App-wide)

typescriptCopyEditapp.useGlobalPipes(new ValidationPipe());

Local Validation (Per Route)

Alternatively, you can apply the ValidationPipe to individual routes using the @UsePipes() decorator:

typescriptCopyEditimport { UsePipes, Body, Post, Controller } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';

@Controller('users')
export class UserController {
  @Post()
  @UsePipes(new ValidationPipe())
  create(@Body() createUserDto: CreateUserDto) {
    return `User ${createUserDto.firstName} created successfully!`;
  }
}

Transforming Data with class-transformer

NestJS integrates with class-transformer to convert plain JavaScript objects into class instances. This allows you to use instance methods in your DTOs.

To apply transformation, you can use the plainToClass() function from class-transformer. Here’s an example of how to apply it manually:

typescriptCopyEditimport { plainToClass } from 'class-transformer';
import { CreateUserDto } from './dto/create-user.dto';

const userData = {
  firstName: 'John',
  lastName: 'Doe',
  email: '[email protected]',
  age: 25,
};

const createUserDto = plainToClass(CreateUserDto, userData);

This is particularly useful when you need to apply transformation explicitly before validation.

Creating Custom Validation Decorators

In some cases, you may need to create custom validation rules. Here’s an example of a custom validator for a “strong password”:

typescriptCopyEditimport { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';

export function IsStrongPassword(validationOptions?: ValidationOptions) {
  return (object: Object, propertyName: string) => {
    registerDecorator({
      name: 'isStrongPassword',
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
          return typeof value === 'string' && passwordRegex.test(value);
        },
        defaultMessage(args: ValidationArguments) {
          return 'Password must be at least 8 characters long and contain both letters and numbers';
        },
      },
    });
  };
}

You can then use this custom decorator in your DTOs:

typescriptCopyEditexport class CreateUserDto {
  @IsStrongPassword({ message: 'Password must be strong' })
  password: string;
}

Error Handling in Validation

When validation fails, NestJS will automatically return a 400 Bad Request response with a detailed error message that highlights which fields failed validation.

For example, if the CreateUserDto is invalid, you might get a response like this:

jsonCopyEdit{
  "statusCode": 400,
  "message": [
    "First name must be a string",
    "Email must be a valid email address"
  ],
  "error": "Bad Request"
}

Best Practices for Validation

  • Use Global Validation: Apply the ValidationPipe globally for consistency.
  • Use DTOs for All Incoming Requests: Always define DTOs for each type of incoming request to ensure data consistency.
  • Provide Custom Error Messages: Use meaningful error messages to make debugging easier for clients.
  • Use class-transformer for Complex Transformations: For advanced cases, use class-transformer to convert plain objects to class instances.
  • Handle Sensitive Data with Caution: Be careful about logging sensitive information that may appear in validation errors.

By implementing DTOs and validation with class-validator, you can ensure that your NestJS application is both secure and robust. Proper validation ensures that only valid data enters your system, reducing the risk of bugs and vulnerabilities.