NestJS course Β· Module 10: Data Validation

Custom Validators - Creating Your Own Guards

3 min read
In this lesson5

The built-in class-validator decorators cover most cases. But what if you need a specialized guard that checks unique rules of your empire? It's time to learn how to create your own gate guards!

@ValidatorConstraint - full custom validator

The most flexible approach is to create your own validator class:

1import {
2  ValidatorConstraint,
3  ValidatorConstraintInterface,
4  ValidationArguments,
5} from 'class-validator';
6
7@ValidatorConstraint({ name: 'isRomanName', async: false })
8export class IsRomanNameConstraint implements ValidatorConstraintInterface {
9  validate(value: string, args: ValidationArguments): boolean {
10    // Check if the name starts with an uppercase letter
11    // and contains only Latin letters
12    return /^[A-Z][a-z]+$/.test(value);
13  }
14
15  defaultMessage(args: ValidationArguments): string {
16    return 'Name ($value) is not a valid Roman name!';
17  }
18}

The class must implement the ValidatorConstraintInterface with two methods:

  • validate() - returns true if the value is valid
  • defaultMessage() - returns the error message

Usage with @Validate()

1import { Validate } from 'class-validator';
2
3export class CreateLegionaryDto {
4  @Validate(IsRomanNameConstraint)
5  name: string;
6}

Creating a Decorator - a cleaner approach

Instead of using @Validate(), you can create your own decorator:

1import {
2  registerDecorator,
3  ValidationOptions,
4  ValidatorConstraint,
5  ValidatorConstraintInterface,
6} from 'class-validator';
7
8@ValidatorConstraint({ async: false })
9class IsLegionCodeConstraint implements ValidatorConstraintInterface {
10  validate(value: string): boolean {
11    return /^LEG-[IVXLCDM]+-\d{3}$/.test(value);
12  }
13
14  defaultMessage(): string {
15    return 'Legion code must have the format LEG-[Roman numerals]-[3 digits]';
16  }
17}
18
19export function IsLegionCode(validationOptions?: ValidationOptions) {
20  return function (object: object, propertyName: string) {
21    registerDecorator({
22      target: object.constructor,
23      propertyName: propertyName,
24      options: validationOptions,
25      constraints: [],
26      validator: IsLegionCodeConstraint,
27    });
28  };
29}

Now you can use it like any other decorator:

1export class CreateLegionaryDto {
2  @IsLegionCode({ message: 'Invalid legion code!' })
3  legionCode: string;
4}

Asynchronous Validators

Sometimes validation requires database access (e.g., checking if a name is unique). In that case, you create an asynchronous validator:

1@ValidatorConstraint({ name: 'isUniqueRank', async: true })
2export class IsUniqueRankConstraint implements ValidatorConstraintInterface {
3  constructor(private readonly legionService: LegionService) {}
4
5  async validate(rank: string): Promise<boolean> {
6    // Check in the database if the rank is available
7    const existing = await this.legionService.findByRank(rank);
8    return !existing;
9  }
10
11  defaultMessage(): string {
12    return 'This rank is already taken in the legion!';
13  }
14}

Note: Asynchronous validators require integration with the NestJS Dependency Injection system through useContainer.

Cross-Field Validation

You can create validators that check relationships between fields:

1@ValidatorConstraint({ async: false })
2class IsAgeValidForRankConstraint implements ValidatorConstraintInterface {
3  validate(value: any, args: ValidationArguments): boolean {
4    const object = args.object as CreateLegionaryDto;
5    // Legatus must be at least 30 years old
6    if (object.rank === 'legatus' && object.age < 30) {
7      return false;
8    }
9    // Centurio must be at least 25 years old
10    if (object.rank === 'centurio' && object.age < 25) {
11      return false;
12    }
13    return true;
14  }
15
16  defaultMessage(args: ValidationArguments): string {
17    return 'Age does not meet the required rank!';
18  }
19}

With custom validators, you have full control over the validation logic. Your gate guards can now check any rules, even the most complex ones!

Code for this lesson: src/custom-validators.ts
1// Custom Validators - Custom gate guards
2import {
3  ValidatorConstraint,
4  ValidatorConstraintInterface,
5  ValidationArguments,
6  registerDecorator,
7  ValidationOptions,
8  Validate,
9} from 'class-validator';
10
11// TODO: Create custom validator IsRomanName
12// Rule: name must start with a capital letter,
13// contain at least 2 characters and only letters
14
15@ValidatorConstraint({ name: 'isRomanName', async: false })
16export class IsRomanNameConstraint
17  implements ValidatorConstraintInterface
18{
19  validate(value: string, args: ValidationArguments): boolean {
20    // TODO: Implement validation
21    // Check: /^[A-Z][a-zA-Z]{1,}$/.test(value)
22    return true; // Change to correct implementation
23  }
24
25  defaultMessage(args: ValidationArguments): string {
26    return 'Name must be a valid Roman name!';
27  }
28}
29
30// TODO: Create IsLegionCode decorator
31// Format: LEG-[2-4 uppercase letters]-[3-4 digits]
32export function IsLegionCode(options?: ValidationOptions) {
33  return function (object: object, propertyName: string) {
34    registerDecorator({
35      target: object.constructor,
36      propertyName,
37      options,
38      validator: {
39        validate(value: string): boolean {
40          // TODO: Implement regex
41          return /^LEG-[A-Z]{2,4}-\d{3,4}$/.test(value);
42        },
43        defaultMessage(): string {
44          return 'Legion code must have format LEG-XX-000';
45        },
46      },
47    });
48  };
49}
50
51// Using custom validators
52class CreateLegionaryDto {
53  @Validate(IsRomanNameConstraint)
54  name: string;
55
56  @IsLegionCode()
57  legionCode: string;
58}
59
60console.log('Custom validators created!');
61

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What methods must a class marked with @ValidatorConstraint implement?

  2. 2. How do you apply a custom validator created with @ValidatorConstraint to a DTO field?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Implement the validate method in IsLatinTextConstraint to check the regex /^[a-zA-Z .]+$/

  • Vertical ordering

    Arrange the elements for creating a custom validation decorator in the correct order:

  • Code editor

    Implement the AgeForRankConstraint logic: legatus min 35, centurio min 25, miles min 16

  • Code editor

    Implement the @IsRomanNumeral decorator using registerDecorator

Useful articles