NestJS: How to Validate Nested Objects

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

Introduction

Validating incoming data is crucial for any backend application. In NestJS, nested object validation can ensure the integrity and structure of the data your app handles. This tutorial will explore how to apply validation rules seamlessly in your NestJS projects.

Setting Up Validation

Before diving into nested object validation, it’s imperative to set up the foundational validation package. NestJS leans on the class-validator package to enable decorators for validation rules. To begin, install the required packages with:

npm install class-validator class-transformer

Once installed, you’ll also need to integrate them into your module using ValidationPipe. Include the pipe globally in your main application file:

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

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

Basic Validation with DTO Classes

Data Transfer Object (DTO) classes are a primary way to define and validate the structure of the data in a NestJS app. For a simple object with a single level of properties, use class-validator decorators to describe the constraints:

import { IsString, IsInt, Min, Max } from 'class-validator';

class CreateUserDto {
  @IsString()
  readonly name: string;

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

This DTO ensures that name is a string, and age is an integer between 0 and 100.

Validating Nested Objects

Let’s consider a case with nested objects:

import { IsString, IsInt, Min, Max, ValidateNested, IsOptional, Type } from 'class-validator';
class AddressDto {
  @IsString()
  readonly street: string;

  @IsInt()
  readonly zipCode: number;
}

class CreateUserDto {
  @IsString()
  readonly name: string;

  @IsInt()
  @Min(0)
  @Max(100)
  readonly age: number;

  @Type(() => AddressDto)
  @ValidateNested()
  @IsOptional()
  readonly address?: AddressDto;
}

In the above example, Type and ValidateNested decorators are used to handle nested object validation. The Type decorator indicates which class to transform the nested object into, while ValidateNested enforces the validation rules defined inside the nested DTO class.

Advanced Validation Techniques

For more complex scenarios, you can employ custom validators or use conditional validation using IsNotEmpty, IsOptional, and arrow functions to apply validation criteria flexibly.

Custom Validators

Here’s an example of creating a custom validator:

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'customTextLength', async: false })
class CustomTextLengthValidator implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments) {
    return text.length > 10 && text.length < 20; // custom validation criteria
  }
  defaultMessage(args: ValidationArguments) {
    return 'Text length is wrong';
  }
}

Decorate the property in your DTO with the custom validator:

class CreateUserDto {
  //... other properties

  @Validate(CustomTextLengthValidator)
  readonly bio: string;
}

Conditional Validation

Use conditions to apply validation rules only when certain conditions are met:

import { IsString, ValidateIf } from 'class-validator';
class CreateUserDto {
  //... other properties

  @IsString()
  @ValidateIf(o => o.bio !== '')
  readonly nickname: string;
}

This ensures that nickname must meet the IsString validation rule only if bio is not empty.

Groups and Nested Validation

Groups allow you to control which validation rules apply at different times. They are handy for cases when specific fields must be validated only in certain contexts, like during creation or update operations.

Define groups in your DTO:

import { IsString, IsInt, Min, Max, ValidateNested, GroupSequenceProvider, GroupSequence } from 'class-validator';

@GroupSequence(['CreateUserDto', 'FullCheck'])
class CreateUserDto {
  //... other properties
  @IsString({ groups: ['FullCheck'] })
  readonly detailedBio: string;
}

Then specify groups when using the validation pipe:

//... setup code

app.useGlobalPipes(new ValidationPipe({ groups: ['FullCheck'] }));

Custom Transformers

Apart from using built-in decorators from class-transformer, you can also define custom transformers to preprocess your data before validation occurs.

Conclusion

Validating nested objects in NestJS using class-validator ensures structured and predictable data throughout your application. By utilizing the validation decorators and techniques we’ve covered, you can create robust and secure NestJS applications tailored to your data requirements. Embrace these features to keep your application’s input solid and your server operations reliable.