Kurs NestJS · Moduł 10: Walidacja danych

Grupy walidacji - różne reguły dla różnych bram

6 min czytania
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:

  1. Instalacja class-validator i class-transformer.
  2. Stworzenie klas DTO z dekoratorami.
  3. Konfiguracja globalnego ValidationPipe.
  4. Użycie DTO w kontrolerach z @Body().
  5. 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:

  1. CORS - kontrola pochodzenia żądań. Rozstrzyga, czy przeglądarka z danego adresu ma w ogóle prawo się odezwać.
  2. Guards - autoryzacja i uwierzytelnianie. Rozstrzygają, kim jest wołający.
  3. ValidationPipe - walidacja danych wejściowych. Rozstrzyga, czy przysłane dane mają sens.
  4. 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 na UpdateDto,
  • zapis: class UpdateDto → extends → PartialType(CreateDto) → {},
  • OmitType tworzy DTO bez wybranych pól, PickType - z tylko wybranymi; OmitType( → CreateUserDto → , ['password'] → ),
  • IntersectionType łączy dwa DTO w jedno,
  • budowanie systemu: instalacja class-validator i class-transformer → klasy DTO z dekoratorami → globalny ValidationPipe → 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: true konwertuje 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() i defaultMessage() → funkcja z registerDecorator() → 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');
65

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służą grupy walidacji (validation groups) w class-validator?

  2. 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:

Przydatne artykuły