0
0
NestJSframework~5 mins

Custom validation decorators in NestJS

Choose your learning style9 modes available
Introduction

Custom validation decorators help you check if data meets your own rules. They make your code cleaner and easier to understand.

You want to check if a username is unique before saving.
You need to validate a password follows special rules.
You want to reuse a specific validation across many places.
You want to keep validation logic separate from your main code.
You want to give clear error messages for your custom rules.
Syntax
NestJS
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ async: false })
class CustomValidator implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    // Your validation logic here
    return true; // or false
  }

  defaultMessage(args: ValidationArguments) {
    return 'Custom error message';
  }
}

export function CustomValidation(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [],
      validator: CustomValidator,
    });
  };
}
Use @ValidatorConstraint to create the validation logic class.
Use registerDecorator inside a function to create the decorator.
Examples
This example creates a decorator @IsEven() that checks if a number is even.
NestJS
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ async: false })
class IsEvenConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    return typeof value === 'number' && value % 2 === 0;
  }

  defaultMessage(args: ValidationArguments) {
    return 'Number must be even';
  }
}

export function IsEven(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsEvenConstraint,
    });
  };
}
This example creates a decorator @IsLongerThan('otherProperty') that checks if one string is longer than another property.
NestJS
export function IsLongerThan(property: string, validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [property],
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];
          return typeof value === 'string' && typeof relatedValue === 'string' && value.length > relatedValue.length;
        },
        defaultMessage(args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          return `${args.property} must be longer than ${relatedPropertyName}`;
        },
      },
    });
  };
}
Sample Program

This program defines a custom decorator @IsPositive to check if a number is positive. It then creates two products: one with a positive price and one with a negative price. It validates both and prints the errors.

NestJS
import { validate } from 'class-validator';

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

@ValidatorConstraint({ async: false })
class IsPositiveConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    return typeof value === 'number' && value > 0;
  }

  defaultMessage(args: ValidationArguments) {
    return 'Value must be a positive number';
  }
}

function IsPositive(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsPositiveConstraint,
    });
  };
}

class Product {
  @IsPositive({ message: 'Price must be positive!' })
  price: number;

  constructor(price: number) {
    this.price = price;
  }
}

async function run() {
  const product1 = new Product(100);
  const product2 = new Product(-5);

  const errors1 = await validate(product1);
  const errors2 = await validate(product2);

  console.log('Product 1 errors:', errors1.length === 0 ? 'No errors' : errors1);
  console.log('Product 2 errors:', errors2.length === 0 ? 'No errors' : errors2.map(e => e.constraints));
}

run();
OutputSuccess
Important Notes

Custom validators can be synchronous or asynchronous by setting async: true in @ValidatorConstraint.

Always provide clear error messages in defaultMessage or via ValidationOptions.

Use constraints to pass extra data to your validator if needed.

Summary

Custom validation decorators let you create your own rules for data checks.

They keep your code clean and reusable.

Use @ValidatorConstraint and registerDecorator to build them.