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!
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 validdefaultMessage() — returns the error message1import { Validate } from 'class-validator';
2
3export class CreateLegionaryDto {
4 @Validate(IsRomanNameConstraint)
5 name: string;
6}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}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.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!