Kurs NestJS · Moduł 10: Walidacja danych
Grupy walidacji - różne reguły dla różnych bram
W tej lekcji6
Ta sama prowincja, dwie różne bramy. Przy wjeździe strażnik sprawdza wszystko: dokumenty, ładunek, pieczęcie. Przy wyjeździe pyta tylko o to, co się zmieniło. Reguły są inne, choć towar ten sam.
Twoje DTO ma dokładnie ten problem. Przy tworzeniu centuriona wymagasz imienia, rangi i kohorty. Przy aktualizacji wymaganie tych samych pól zmusza klienta do przysyłania całości tylko po to, żeby zmienić jeden numer. Ta lekcja pokazuje dwa wyjścia z tej sytuacji - i zbiera cały moduł w całość.
Grupy walidacji
Grupy walidacji służą do stosowania różnych reguł walidacji w różnych kontekstach - na przykład inaczej przy tworzeniu, inaczej przy aktualizacji. Nie do grupowania dekoratorów w osobne pliki, nie do tworzenia grup użytkowników z uprawnieniami i nie do organizowania testów jednostkowych.
Zapisuje się je przy dekoratorach, a aktywuje przy pipie:
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) {}Jedna klasa obsługuje dwa konteksty, bo każdy dekorator wie, w której grupie obowiązuje. Wadą jest czytelność: przy dziesięciu polach i trzech grupach trzeba czytać uważnie, żeby powiedzieć, co dokładnie jest wymagane przy aktualizacji.
Dlatego w praktyce częściej sięga się po drugie rozwiązanie.
Mapped types
Zamiast jednej klasy z grupami - dwie klasy, z których druga powstaje z pierwszej. Służą do tego pomocniki z pakietu @nestjs/mapped-types:
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) tworzy kopię DTO ze wszystkimi polami opcjonalnymi - i to on sprawia, że wszystkie pola stają się opcjonalne. Nie dzieli DTO na mniejsze części, nie usuwa połowy pól i nie łączy dwóch DTO w jeden. Właśnie dlatego jest najczęściej używany do tworzenia UpdateDto: aktualizacja z natury dotyczy podzbioru pól.
Zapis dziedziczenia ma stałą kolejność: class UpdateDto → extends → PartialType(CreateDto) → {}. Ciało klasy zwykle zostaje puste, bo wszystko przychodzi z klasy bazowej wraz z jej dekoratorami.
Pozostała trójka dzieli się rolami. OmitType tworzy DTO bez wybranych pól, a PickType - DTO z tylko wybranymi polami; to jedyna różnica między nimi, a nie szybkość ani przeznaczenie do REST czy GraphQL. Wywołanie OmitType zapisuje się w kolejności: OmitType( → CreateUserDto → , ['password'] → ). IntersectionType łączy dwa DTO w jedno.
Wybieraj według tego, czego jest mniej. Gdy z dwudziestu pól chcesz ukryć jedno - OmitType. Gdy chcesz zostawić dwa - PickType.
Cały system walidacji
Ta lekcja zamyka moduł, więc warto zobaczyć całość. Budowanie systemu walidacji ma pięć etapów, w tej kolejności:
- Instalacja
class-validatoriclass-transformer. - Stworzenie klas DTO z dekoratorami.
- Konfiguracja globalnego
ValidationPipe. - Użycie DTO w kontrolerach z
@Body(). - Dodanie własnych walidatorów (opcjonalnie).
Import dekoratorów zapisuje się w kolejności: import { → IsString, IsNumber, IsNotEmpty → } → from 'class-validator';.
Same warstwy układają się od najniższej: dekoratory na polach DTO (@IsString, @IsNumber) → ValidationPipe, który je uruchamia → kontroler, gdzie DTO trafia do @Body → globalny pipe przez app.useGlobalPipes. Każda wyższa warstwa obejmuje szerszy zakres: dekorator dotyczy pola, pipe - jednego argumentu, kontroler - grupy endpointów, pipe globalny - całej aplikacji.
Gdy walidacja nie przejdzie, NestJS zwraca kod 400 Bad Request - nie 200, nie 401 i nie 500. To odpowiedź z klasy „wina klienta": dane były wadliwe, a serwer zadziałał poprawnie, odrzucając je.
Opcje i dekoratory, o których łatwo zapomnieć
Pipe można podpiąć na pojedynczym endpoincie, a zapis ma stałą kolejność: @UsePipes( → new ValidationPipe( → { whitelist: true }) → ). Dwa domknięcia na końcu to nie pomyłka - jedno zamyka konstruktor pipe'a, drugie sam dekorator.
Opcja transform: true automatycznie konwertuje parametry URL ze stringów na odpowiednie typy. Nie zmienia formatu odpowiedzi na XML, nie przerabia błędów walidacji na logi i na pewno nie wyłącza transformacji class-transformer - robi dokładnie odwrotnie. Bez niej @Query('page') page: number daje napis '2', mimo że typ mówi number.
Przy zagnieżdżonych obiektach potrzebny jest @Type, którego zapis czyta się w kolejności: @Type( → () => → WeaponDto → ). Funkcja strzałkowa w środku nie jest ozdobnikiem - odracza obliczenie klasy, dzięki czemu działa nawet wtedy, gdy dwie klasy odwołują się do siebie nawzajem.
Tworzenie własnego dekoratora walidacji ma cztery kroki: stworzenie klasy z @ValidatorConstraint → implementacja validate() i defaultMessage() → stworzenie funkcji dekoratora z registerDecorator() → użycie @IsMyValidator() w DTO.
Walidacja wśród innych zabezpieczeń
Na koniec spójrzmy szerzej. Walidacja jest jedną z warstw ochrony API, a kolejność linii obrony wygląda tak:
- CORS - kontrola pochodzenia żądań. Rozstrzyga, czy przeglądarka z danego adresu ma w ogóle prawo się odezwać.
- Guards - autoryzacja i uwierzytelnianie. Rozstrzygają, kim jest wołający.
- ValidationPipe - walidacja danych wejściowych. Rozstrzyga, czy przysłane dane mają sens.
- Business logic - logika biznesowa w serwisie. Rozstrzyga resztę.
Ta kolejność ma praktyczny skutek, który warto zapamiętać: żądanie bez uprawnień zostanie odrzucone, zanim ktokolwiek sprawdzi jego zawartość. Dzięki temu klient bez tokenu nie dowie się z komunikatu błędu, których pól wymaga endpoint, do którego nie ma dostępu.
Podsumowanie
Inna brama, inne reguły:
- grupy walidacji stosują różne reguły w różnych kontekstach (create vs update) - nie grupują plików, użytkowników ani testów,
PartialType(CreateDto)tworzy kopię DTO ze wszystkimi polami opcjonalnymi i jest najczęstszym sposobem naUpdateDto,- zapis:
class UpdateDto→extends→PartialType(CreateDto)→{}, OmitTypetworzy DTO bez wybranych pól,PickType- z tylko wybranymi;OmitType(→CreateUserDto→, ['password']→),IntersectionTypełączy dwa DTO w jedno,- budowanie systemu: instalacja
class-validatoriclass-transformer→ klasy DTO z dekoratorami → globalnyValidationPipe→ DTO w kontrolerach z@Body()→ własne walidatory, - import:
import {→IsString, IsNumber, IsNotEmpty→}→from 'class-validator';, - warstwy walidacji od najniższej: dekoratory na polach DTO →
ValidationPipe→ kontroler → globalny pipe, - przy niepowodzeniu walidacji NestJS zwraca
400 Bad Request, @UsePipes(→new ValidationPipe(→{ whitelist: true })→),transform: truekonwertuje parametry URL ze stringów na właściwe typy,@Type(→() =>→WeaponDto→)przy walidacji zagnieżdżonej,- własny walidator w czterech krokach: klasa z
@ValidatorConstraint→validate()idefaultMessage()→ funkcja zregisterDecorator()→ użycie@IsMyValidator(), - linie obrony API: CORS → Guards → ValidationPipe → logika biznesowa.
W następnej lekcji zbierzemy to wszystko w projekt. A na razie zapamiętaj: walidacja nie polega na dopisaniu jak największej liczby dekoratorów. Polega na tym, żeby w każdym kontekście obowiązywały te reguły, które w nim mają sens - i żadne inne.
Kod do tej lekcji: src/validation-groups.ts
1// Grupy walidacji - Różne reguły dla różnych bram
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: Dodaj grupy walidacji do dekoratorów
19export class LegionaryDto {
20 // TODO: Wymagany przy 'create', opcjonalny przy 'update'
21 @IsString()
22 @IsNotEmpty()
23 name: string;
24
25 // TODO: Wymagany przy 'create', opcjonalny przy 'update'
26 @IsEnum(Rank)
27 rank: Rank;
28
29 // TODO: Wymagany przy 'create', opcjonalny przy 'update'
30 @IsNumber()
31 @Min(16)
32 @Max(65)
33 age: number;
34}
35
36// Alternatywa: PartialType z @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: Stwórz UpdateLegionaryDto
53// Użyj wzorca: wszystkie pola z @IsOptional()
54class UpdateLegionaryDto {
55 @IsOptional()
56 @IsString()
57 name?: string;
58
59 // Dodaj pozostałe pola jako opcjonalne
60}
61
62console.log('Grupy walidacji skonfigurowane!');
63console.log('Create: wszystko wymagane');
64console.log('Update: pola opcjonalne');
65Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służą grupy walidacji (validation groups) w class-validator?
2. Który mapped type w NestJS sprawia, że WSZYSTKIE pola DTO stają się opcjonalne?
To 2 z 7 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij UpdateCenturionDto dodając @IsOptional() i odpowiednie walidatory do każdego pola
- Układanie w poziomie
Ułóż elementy definicji UpdateDto z użyciem PartialType:
- Klikanie w kolejności
Ułóż wywołanie OmitType do tworzenia publicznego DTO bez pola 'password':
- Układanie w pionie
Uporządkuj etapy budowania kompletnego systemu walidacji w NestJS:
- Edytor kodu
Uzupełnij dekoratory walidacji i kontroler w pełnym module NestJS
- Układanie w poziomie
Ułóż elementy użycia @UsePipes na endpoincie w prawidłowej kolejności:
- Klikanie w kolejności
Ułóż elementy importu dekoratorów z class-validator:
- Układanie w pionie
Uporządkuj warstwy walidacji od najniższego do najwyższego poziomu:
- Układanie w pionie
Uporządkuj warstwy ochrony API od pierwszej do ostatniej linii obrony:
- Układanie w poziomie
Ułóż elementy dekoratora @Type do zagnieżdżonej walidacji:
- Klikanie w kolejności
Ułóż kroki tworzenia własnego dekoratora walidacji: