NestJS: Using Class Validator to validate array of objects

Updated: January 1, 2024 By: Guest Contributor Post a comment

Introduction

Validating data is crucial in backend development to ensure that incoming data adheres to the expected format and types. In NestJS, the Class Validator package is a powerful tool for handling such validations neatly and declaratively. This tutorial delves into validating arrays of objects using Class Validator, enhancing the robustness of your NestJS applications.

Getting Started

Before diving into the usage of Class Validator to validate arrays of objects, ensure you have a NestJS project set up. If you don’t have one ready, you can create a new project by running:

nest new project-name

Then, install the class-validator and class-transformer packages:

npm install class-validator class-transformer

Defining a DTO with Array of Objects

First, define a Data Transfer Object (DTO) that includes an array of objects. Say we’re dealing with users that have a list of addresses, each address being an object:

import { IsString, IsPostalCode, IsNotEmpty, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

export class CreateAddressDto {
    @IsString()
    @IsNotEmpty()
    street: string;

    @IsPostalCode()
    postalCode: string;
}

export class CreateUserDto {
    @IsString()
    @IsNotEmpty()
    name: string;

    @ValidateNested({ each: true })
    @Type(() => CreateAddressDto)
    addresses: CreateAddressDto[];
}

Here, the @ValidateNested() decorator specifies that each object in the array should be validated against the CreateAddressDto rules. The @Type() decorator from the class-transformer package informs NestJS how to transform the plain objects into instances of CreateAddressDto.

Applying Validation Pipe

To trigger the validation, use NestJS’s ValidationPipe. Set this up globally or at the controller method level. The ValidationPipe will enforce the rules you have set in your DTO.

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

@Controller('users')
export class UsersController {
    @Post()
    @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
    async create(@Body() createUserDto: CreateUserDto) {
        // Your logic here
    }
}

This endpoint will now reject any requests where the user object does not match the CreateUserDto schema, including the array of addresses.

Custom Array Validators

Class Validator also allows you to define custom validation decorators if the existing ones don’t fit your needs. For instance, ensuring that the array has at least one element:

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

export function IsNotEmptyArray(validationOptions?: ValidationOptions) {
    return function (object: Object, propertyName: string) {
        registerDecorator({
            name: 'isNotEmptyArray',
            target: object.constructor,
            propertyName: propertyName,
            constraints: [],
            options: validationOptions,
            validator: {
                validate(value: any, args: ValidationArguments) {
                    return Array.isArray(value) && value.length > 0;
                }
            }
        });
    };
}

// Apply the custom validator on DTO:
export class CreateUserDto {
    // ... other properties

    @IsNotEmptyArray({ message: 'Addresses array cannot be empty.' })
    addresses: CreateAddressDto[];
}

The @IsNotEmptyArray custom decorator ensures that the addresses array isn’t empty.

Handling Nested Validation Issues

Errors thrown by the ValidationPipe are comprehensive, but handling nested object errors can be tricky at times. To structure the error output for better clarity, you can extend the ValidationPipe or use exception filters in NestJS.

What if your requirements are even more complex? For example, you might need to include asynchronous validators, integrate with other libraries, or perform conditional validations based on the rest of the object’s context. Class Validator supports these advanced use cases via custom asynchronous classes and conditional decorators. This flexibility allows you to craft virtually any validation rule you may require.

Final Words

While validating arrays of objects is powerful, be cautious about potential performance impacts if the arrays are overly large. Always consider the trade-off between strict data validation and the computational cost associated with deep validation of large object structures.

In conclusion, using class-validator with NestJS to validate an array of objects is an elegant approach to ensure data integrity. The combination of built-in decorators and the power to create your own provides a flexible validation framework fit for any application. Ultimately, integrating these practices will contribute to cleaner, safer, and more reliable codebases.