NestJS course Β· Module 10: Data Validation
Custom Validators - Creating Your Own Guards
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()- returnstrueif the value is validdefaultMessage()- 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!');
61Spotted 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. What methods must a class marked with @ValidatorConstraint implement?
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