Cours NestJS · Module 10: Validation des données

Validateurs personnalisés - Créer tes propres gardes

3 min de lecture
Dans cette leçon5

Les décorateurs intégrés de class-validator couvrent la plupart des cas. Mais que faire si tu as besoin d'un garde spécialisé qui vérifie des règles uniques de ton empire ? Il est temps d'apprendre à créer tes propres gardes de la porte !

@ValidatorConstraint - validateur personnalisé complet

L'approche la plus flexible consiste à créer ta propre classe de validateur :

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}

La classe doit implémenter l'interface ValidatorConstraintInterface avec deux méthodes :

  • validate() - renvoie true si la valeur est valide
  • defaultMessage() - renvoie le message d'erreur

Utilisation avec @Validate()

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

Créer un décorateur - une approche plus propre

Au lieu d'utiliser @Validate(), tu peux créer ton propre décorateur :

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}

Tu peux maintenant l'utiliser comme n'importe quel autre décorateur :

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

Validateurs asynchrones

Parfois, la validation nécessite un accès à la base de données (par exemple, vérifier si un nom est unique). Dans ce cas, tu crées un validateur asynchrone :

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}

Remarque : Les validateurs asynchrones nécessitent une intégration avec le système d'injection de dépendances de NestJS via useContainer.

Validation entre champs

Tu peux créer des validateurs qui vérifient les relations entre les champs :

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}

Avec des validateurs personnalisés, tu as un contrôle total sur la logique de validation. Tes gardes de la porte peuvent désormais vérifier n'importe quelles règles, même les plus complexes !

Code de cette leçon : src/custom-validators.ts
1// Custom Validators - Gardes de porte personnalisés
2import {
3  ValidatorConstraint,
4  ValidatorConstraintInterface,
5  ValidationArguments,
6  registerDecorator,
7  ValidationOptions,
8  Validate,
9} from 'class-validator';
10
11// TODO : Créer le validateur personnalisé IsRomanName
12// Règle : le nom doit commencer par une majuscule,
13// contenir au moins 2 caractères et uniquement des lettres
14
15@ValidatorConstraint({ name: 'isRomanName', async: false })
16export class IsRomanNameConstraint
17  implements ValidatorConstraintInterface
18{
19  validate(value: string, args: ValidationArguments): boolean {
20    // TODO : Implémenter la validation
21    // Vérifier : /^[A-Z][a-zA-Z]{1,}$/.test(value)
22    return true; // Remplacer par l'implémentation correcte
23  }
24
25  defaultMessage(args: ValidationArguments): string {
26    return 'Name must be a valid Roman name!';
27  }
28}
29
30// TODO : Créer le décorateur IsLegionCode
31// Format : LEG-[2-4 lettres majuscules]-[3-4 chiffres]
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 : Implémenter la 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// Utilisation des validateurs personnalisés
52class CreateLegionaryDto {
53  @Validate(IsRomanNameConstraint)
54  name: string;
55
56  @IsLegionCode()
57  legionCode: string;
58}
59
60console.log('Validateurs personnalisés créés !');
61

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelles méthodes une classe marquée avec @ValidatorConstraint doit-elle implémenter ?

  2. 2. Comment applique-t-on à un champ de DTO un validateur personnalisé créé avec @ValidatorConstraint ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Implémente la méthode validate dans IsLatinTextConstraint pour vérifier la regex /^[a-zA-Z .]+$/

  • Tri vertical

    Range les éléments de création d'un décorateur de validation personnalisé dans le bon ordre :

  • Éditeur de code

    Implémente la logique de AgeForRankConstraint : legatus min 35, centurio min 25, miles min 16

  • Éditeur de code

    Implémente le décorateur @IsRomanNumeral en utilisant registerDecorator