Cours NestJS · Module 10: Validation des données
Les groupes de validation - des règles différentes pour des portes différentes
Dans cette leçon6
La même province, deux portes différentes. À l'entrée, le garde contrôle tout : papiers, cargaison, sceaux. À la sortie, il ne demande que ce qui a changé. Les règles diffèrent, quoique la marchandise soit la même.
Ton DTO connaît exactement ce problème. Créer un centurion exige un nom, un rang et une cohorte. Mettre à jour, si l'on exige les mêmes champs, contraint le client à tout renvoyer pour changer un seul numéro. Cette leçon montre deux issues - et rassemble tout le module.
Les groupes de validation
Les groupes de validation servent à appliquer des règles de validation différentes selon le contexte - un jeu à la création, un autre à la mise à jour, par exemple. Non à regrouper les décorateurs dans des fichiers séparés, non à créer des groupes d'utilisateurs avec des permissions, non à organiser les tests unitaires.
Ils s'inscrivent sur les décorateurs et s'activent sur le pipe :
1export class CenturionDto {
2 @IsNotEmpty({ groups: ['create'] })
3 @IsOptional({ groups: ['update'] })
4 @IsString({ groups: ['create', 'update'] })
5 name: string;
6
7 @IsNumber({ groups: ['create', 'update'] })
8 cohortId: number;
9}
10
11@Post()
12@UsePipes(new ValidationPipe({ groups: ['create'] }))
13create(@Body() dto: CenturionDto) {}Une seule classe dessert deux contextes, car chaque décorateur sait dans quel groupe il vaut. L'inconvénient est la lisibilité : avec dix champs et trois groupes, il faut lire attentivement pour dire ce qui est exactement requis à la mise à jour.
C'est pourquoi, en pratique, on recourt plus souvent à la seconde solution.
Les mapped types
Au lieu d'une classe à groupes - deux classes, la seconde dérivée de la première. Les assistants du paquet @nestjs/mapped-types s'en chargent :
1export class CreateCenturionDto {
2 @IsString()
3 @IsNotEmpty()
4 name: string;
5
6 @IsNumber()
7 cohortId: number;
8
9 @IsString()
10 password: string;
11}
12
13class UpdateDto extends PartialType(CreateDto) {}
14
15const PublicUserDto = OmitType(CreateUserDto, ['password']);
16const NameOnlyDto = PickType(CreateCenturionDto, ['name']);
17const FullDto = IntersectionType(CreateCenturionDto, MetadataDto);PartialType(CreateDto) crée une copie du DTO avec tous les champs optionnels - c'est lui qui rend tous les champs optionnels. Il ne divise pas le DTO en parties plus petites, ne supprime pas la moitié des champs et ne fusionne pas deux DTO en un seul. Voilà précisément pourquoi il est le plus couramment employé pour créer un UpdateDto : une mise à jour porte par nature sur un sous-ensemble de champs.
L'héritage s'écrit dans un ordre fixe : class UpdateDto → extends → PartialType(CreateDto) → {}. Le corps de la classe reste d'ordinaire vide, car tout vient de la classe de base avec ses décorateurs.
Les trois autres se partagent les rôles. OmitType crée un DTO sans les champs sélectionnés, et PickType un DTO avec uniquement les champs sélectionnés ; c'est là leur seule différence, non la vitesse ni une destination REST contre GraphQL. Un appel à OmitType s'écrit dans l'ordre : OmitType( → CreateUserDto → , ['password'] → ). IntersectionType fusionne deux DTO en un seul.
Choisis selon ce qui est le moins nombreux. Pour masquer un champ sur vingt - OmitType. Pour n'en garder que deux - PickType.
Tout le système de validation
Cette leçon clôt le module : il vaut donc la peine d'en voir l'ensemble. Construire un système de validation compte cinq étapes, dans cet ordre :
- Installer
class-validatoretclass-transformer. - Créer les classes DTO avec des décorateurs.
- Configurer le
ValidationPipeglobal. - Utiliser les DTO dans les contrôleurs avec
@Body(). - Ajouter des validateurs personnalisés (optionnel).
Les décorateurs s'importent dans l'ordre : import { → IsString, IsNumber, IsNotEmpty → } → from 'class-validator';.
Les couches elles-mêmes vont de la plus basse : décorateurs sur les champs du DTO (@IsString, @IsNumber) → ValidationPipe, qui les exécute → le contrôleur, où le DTO parvient à @Body → le pipe global par app.useGlobalPipes. Chaque couche supérieure couvre une portée plus large : un décorateur concerne un champ, un pipe un argument, un contrôleur un groupe d'endpoints, un pipe global toute l'application.
Lorsque la validation échoue, NestJS renvoie 400 Bad Request - non 200, non 401, non 500. C'est une réponse de la classe « faute du client » : les données étaient fautives et le serveur a bien agi en les rejetant.
Options et décorateurs qu'on oublie aisément
Un pipe peut se brancher sur un endpoint unique, et l'écriture suit un ordre fixe : @UsePipes( → new ValidationPipe( → { whitelist: true }) → ). Les deux fermetures finales ne sont pas une erreur - l'une referme le constructeur du pipe, l'autre le décorateur lui-même.
L'option transform: true convertit automatiquement les paramètres d'URL de chaînes vers les types appropriés. Elle ne change pas le format de la réponse en XML, ne transforme pas les erreurs de validation en logs, et ne désactive assurément pas la transformation de class-transformer - elle fait exactement l'inverse. Sans elle, @Query('page') page: number te donne la chaîne '2', quoique le type dise number.
Les objets imbriqués réclament @Type, dont l'écriture se lit dans l'ordre : @Type( → () => → WeaponDto → ). La fonction fléchée qu'elle contient n'est pas un ornement - elle diffère l'évaluation de la classe, ce qui la fait fonctionner même quand deux classes se renvoient l'une à l'autre.
Créer un décorateur de validation personnalisé compte quatre étapes : créer une classe avec @ValidatorConstraint → implémenter validate() et defaultMessage() → créer une fonction de décorateur avec registerDecorator() → utiliser @IsMyValidator() dans un DTO.
La validation parmi les autres défenses
Terminons par une vue plus large. La validation est l'une des couches de protection d'une API, et les lignes de défense se succèdent ainsi :
- CORS - contrôle de l'origine des requêtes. Il décide si un navigateur d'une adresse donnée peut seulement prendre la parole.
- Guards - autorisation et authentification. Ils décident qui est l'appelant.
- ValidationPipe - validation des données d'entrée. Il décide si les données envoyées ont un sens.
- Business logic - la logique métier du service. Elle décide du reste.
Cet ordre a une conséquence pratique à retenir : une requête sans permissions est rejetée avant que quiconque n'inspecte son contenu. Un client sans token ne peut donc apprendre d'un message d'erreur quels champs exige un endpoint auquel il n'a aucun accès.
Récapitulatif
Une autre porte, d'autres règles :
- les groupes de validation appliquent des règles différentes selon le contexte (create vs update) - ils ne groupent ni fichiers, ni utilisateurs, ni tests,
PartialType(CreateDto)crée une copie du DTO avec tous les champs optionnels et constitue la voie la plus courante vers unUpdateDto,- l'écriture :
class UpdateDto→extends→PartialType(CreateDto)→{}, OmitTypecrée un DTO sans les champs sélectionnés,PickTypeun DTO avec uniquement ceux-ci ;OmitType(→CreateUserDto→, ['password']→),IntersectionTypefusionne deux DTO en un seul,- construction du système : installer
class-validatoretclass-transformer→ classes DTO avec décorateurs →ValidationPipeglobal → DTO dans les contrôleurs avec@Body()→ validateurs personnalisés, - l'import :
import {→IsString, IsNumber, IsNotEmpty→}→from 'class-validator';, - couches de validation, de la plus basse : décorateurs sur les champs du DTO →
ValidationPipe→ le contrôleur → le pipe global, - en cas d'échec de la validation, NestJS renvoie
400 Bad Request, @UsePipes(→new ValidationPipe(→{ whitelist: true })→),transform: trueconvertit les paramètres d'URL de chaînes vers les bons types,@Type(→() =>→WeaponDto→)pour la validation imbriquée,- un validateur personnalisé en quatre étapes : une classe avec
@ValidatorConstraint→validate()etdefaultMessage()→ une fonction avecregisterDecorator()→ l'emploi d'@IsMyValidator(), - les lignes de défense de l'API : CORS → Guards → ValidationPipe → logique métier.
Dans la prochaine leçon, nous rassemblerons tout cela en un projet. En attendant, retiens ceci : valider ne consiste pas à ajouter le plus de décorateurs possible. Cela consiste à faire valoir, dans chaque contexte, les règles qui y ont un sens - et aucune autre.
Code de cette leçon : src/validation-groups.ts
1// Groupes de validation - Des règles différentes pour des portes différentes
2import {
3 IsString,
4 IsNotEmpty,
5 IsNumber,
6 Min,
7 Max,
8 IsOptional,
9 IsEnum,
10} from 'class-validator';
11
12enum Rank {
13 MILES = 'miles',
14 CENTURIO = 'centurio',
15 LEGATUS = 'legatus',
16}
17
18// TODO : Ajouter des groupes de validation aux décorateurs
19export class LegionaryDto {
20 // TODO : Requis pour 'create', optionnel pour 'update'
21 @IsString()
22 @IsNotEmpty()
23 name: string;
24
25 // TODO : Requis pour 'create', optionnel pour 'update'
26 @IsEnum(Rank)
27 rank: Rank;
28
29 // TODO : Requis pour 'create', optionnel pour 'update'
30 @IsNumber()
31 @Min(16)
32 @Max(65)
33 age: number;
34}
35
36// Alternative : PartialType de @nestjs/mapped-types
37// import { PartialType } from '@nestjs/mapped-types';
38
39class CreateLegionaryDto {
40 @IsString()
41 @IsNotEmpty()
42 name: string;
43
44 @IsEnum(Rank)
45 rank: Rank;
46
47 @IsNumber()
48 @Min(16)
49 age: number;
50}
51
52// TODO : Créer UpdateLegionaryDto
53// Utiliser le motif : tous les champs avec @IsOptional()
54class UpdateLegionaryDto {
55 @IsOptional()
56 @IsString()
57 name?: string;
58
59 // Ajouter les champs restants comme optionnels
60}
61
62console.log('Groupes de validation configurés !');
63console.log('Create : tout est requis');
64console.log('Update : champs optionnels');
65Tu 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. À quoi servent les groupes de validation dans class-validator ?
2. Quel mapped type de NestJS rend TOUS les champs d'un DTO optionnels ?
Voici 2 des 7 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète UpdateCenturionDto en ajoutant @IsOptional() et les validateurs appropriés à chaque champ
- Tri horizontal
Range les éléments de la définition de UpdateDto en utilisant PartialType :
- Clique dans l'ordre
Range l'appel OmitType pour créer un DTO public sans le champ « password » :
- Tri vertical
Range les étapes de construction d'un système de validation complet dans NestJS :
- Éditeur de code
Complète les décorateurs de validation et le contrôleur dans un module NestJS complet
- Tri horizontal
Range les éléments de l'utilisation de @UsePipes sur un endpoint dans le bon ordre :
- Clique dans l'ordre
Range les éléments de l'import des décorateurs depuis class-validator :
- Tri vertical
Range les couches de validation du plus bas au plus haut niveau :
- Tri vertical
Range les couches de protection de l'API de la première à la dernière ligne de défense :
- Tri horizontal
Range les éléments du décorateur @Type pour la validation imbriquée :
- Clique dans l'ordre
Range les étapes de création d'un décorateur de validation personnalisé :