Kurs NestJS · Moduł 10: Walidacja danych

ValidationPipe - automatyczna stacja kontrolna

4 min czytania
W tej lekcji5

Dekoratory z poprzednich lekcji - @IsString, @IsNumber, @Min - niczego same nie sprawdzają. To opis wymagań przypięty do pól, tak jak wykaz przewozowy przypięty do skrzyni. Ktoś musi ten wykaz wziąć do ręki i porównać z zawartością.

Główną rolą ValidationPipe jest automatyczne uruchamianie walidacji dekoratorów class-validator. Nie szyfruje danych w żądaniach HTTP, nie kompresuje odpowiedzi serwera i nie generuje dokumentacji Swagger. Robi jedno: bierze przychodzący obiekt, odczytuje dekoratory z klasy DTO i sprawdza, czy dane im odpowiadają.

Rejestracja globalna

Pipe rejestruje się raz, przy starcie aplikacji:

1async function bootstrap() {
2  const app = await NestFactory.create(AppModule);
3
4  app.useGlobalPipes(
5    new ValidationPipe({
6      whitelist: true,
7      forbidNonWhitelisted: true,
8      transform: true,
9    }),
10  );
11
12  await app.listen(3000);
13}

Zapis czyta się w czterech częściach: app to obiekt aplikacji, .useGlobalPipes( otwiera rejestrację, new ValidationPipe({ ... }) tworzy pipe z opcjami, a ) domyka wywołanie.

Zwróć uwagę na liczbę mnogą w nazwie metody: useGlobalPipes przyjmuje wiele pipe'ów po przecinku. W praktyce prawie zawsze podajesz jeden - ValidationPipe - bo pozostałe, jak ParseIntPipe, mają sens tylko przy konkretnych parametrach.

Cztery tryby, od najłagodniejszego

Konfiguracja pipe'a to nie zbiór niezależnych przełączników, tylko drabina restrykcyjności. Warto ją znać w całości, bo wybór trybu jest decyzją o tym, jak twoje API traktuje nieznane dane.

  1. Bez ValidationPipe - brak walidacji. Do handlera trafia dowolny obiekt JSON. Adnotacje typów w kodzie są wtedy życzeniem, nie gwarancją.
  2. ValidationPipe() - podstawowa walidacja. Reguły z dekoratorów działają, ale pola spoza DTO przechodzą nietknięte.
  3. whitelist: true - usuwanie nieznanych pól.
  4. whitelist + forbidNonWhitelisted - odrzucanie żądań.

Dwa ostatnie tryby dzieli jedna rzecz i warto ją zapamiętać dokładnie. Opcja whitelist: true automatycznie usuwa z przychodzących danych pola bez dekoratorów walidacji. Nie dodaje białego tła do odpowiedzi, nie tworzy listy dozwolonych adresów IP i nie wyłącza walidacji na wybranych endpointach - po prostu wycina to, czego nie opisałeś w DTO.

Różnica wobec forbidNonWhitelisted: true jest taka, że whitelist usuwa nieznane pola po cichu, a forbidNonWhitelisted zwraca błąd 400. Nie są to opcje identyczne, forbidNonWhitelisted niczego nie wyłącza i nie dopisuje pól do logów serwera.

1// DTO opisuje tylko: name, cohortId
2// Klient przysyła: { name: 'Marcus', cohortId: 3, isAdmin: true }
3
4// ValidationPipe()                     -> serwis dostaje isAdmin: true
5// whitelist: true                      -> serwis dostaje { name, cohortId }
6// whitelist + forbidNonWhitelisted     -> 400 Bad Request

Pole isAdmin w tym przykładzie nie jest przypadkowe. Bez whitelist trafia do serwisu, a jeśli gdzieś dalej ktoś zrobi Object.assign(user, dto), klient właśnie nadał sobie uprawnienia. To najstarszy sposób na przejęcie konta w aplikacjach, które ufają wejściu.

Który tryb wybrać? forbidNonWhitelisted jest surowszy, a przez to uczciwszy: klient dowiaduje się, że przysłał coś nieoczekiwanego, zamiast myśleć, że zapisałeś jego pole. Samo whitelist bywa wygodniejsze przy publicznym API, w którym starsze wersje klientów przysyłają pola już wycofane.

transform

Ostatnia opcja z zestawu produkcyjnego dotyczy typów. transform: true sprawia, że do handlera trafia instancja klasy DTO, a nie zwykły obiekt - i przy okazji konwertuje typy proste. Bez niej @Param('id') id: number daje napis '7', choć typ mówi number, bo z adresu URL wszystko przychodzi jako tekst.

To także warunek działania dekoratorów @Type i @Transform z class-transformer: bez transform: true pozostają one adnotacjami bez skutku.

Na poziomie pojedynczego endpointu

Gdy jeden endpoint potrzebuje innych reguł niż reszta, pipe podpina się lokalnie przez @UsePipes(new ValidationPipe({ ... })) nad metodą. Pipe lokalny ma pierwszeństwo przed globalnym.

Sięgaj po to rzadko. Konfiguracja walidacji rozproszona po kontrolerach szybko przestaje być czytelna, a odpowiedź na pytanie „czy ten endpoint odrzuca nadmiarowe pola?" zaczyna wymagać czytania trzech plików.

Podsumowanie

Wykaz przewozowy sam niczego nie sprawdzi:

  • główną rolą ValidationPipe jest automatyczne uruchamianie walidacji dekoratorów class-validator - nie szyfrowanie, nie kompresja, nie generowanie Swaggera,
  • rejestracja globalna: app → .useGlobalPipes( → new ValidationPipe({ ... }) → ),
  • tryby od najmniej restrykcyjnego: brak ValidationPipe → ValidationPipe() → whitelist: true → whitelist + forbidNonWhitelisted,
  • whitelist: true usuwa z przychodzących danych pola bez dekoratorów walidacji - nie dotyczy adresów IP ani wyłączania walidacji,
  • whitelist usuwa nieznane pola po cichu, forbidNonWhitelisted zwraca błąd 400,
  • nieusunięte pole w rodzaju isAdmin to najprostsza droga do nadania sobie uprawnień,
  • transform: true daje instancję DTO i konwertuje typy proste; bez niej @Type i @Transform nie działają,
  • @UsePipes nad metodą nadpisuje konfigurację globalną - używaj oszczędnie.

W następnej lekcji zajmiemy się class-transformer - tym, co dzieje się z danymi w drugą stronę, przy wychodzeniu z API. A na razie zapamiętaj: dekoratory opisują, pipe wykonuje. Bez niego masz w kodzie bardzo dokładną dokumentację reguł, których nikt nie egzekwuje.

Kod do tej lekcji: src/validation-pipe.ts
1// ValidationPipe - Konfiguracja stacji kontrolnej
2import { NestFactory } from '@nestjs/core';
3import { ValidationPipe } from '@nestjs/common';
4
5// TODO: Skonfiguruj ValidationPipe globalnie
6async function bootstrap() {
7  const app = await NestFactory.create(AppModule);
8
9  // TODO: Dodaj app.useGlobalPipes z ValidationPipe
10  // Wymagane opcje:
11  // - whitelist: true
12  // - forbidNonWhitelisted: true
13  // - transform: true
14
15  await app.listen(3000);
16  console.log('Brama Imperium otwarta na porcie 3000');
17}
18
19// Przykład użycia per-route
20import { Controller, Post, Body, UsePipes } from '@nestjs/common';
21import { IsString, IsNumber, IsNotEmpty, Min } from 'class-validator';
22
23class CreateSoldierDto {
24  @IsString()
25  @IsNotEmpty()
26  name: string;
27
28  @IsNumber()
29  @Min(16)
30  age: number;
31}
32
33@Controller('soldiers')
34class SoldierController {
35  // TODO: Dodaj @UsePipes(new ValidationPipe({ whitelist: true }))
36  @Post()
37  create(@Body() dto: CreateSoldierDto) {
38    return { message: 'Żołnierz zrekrutowany!', data: dto };
39  }
40}
41
42console.log('ValidationPipe skonfigurowany!');
43

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. Jaka jest główna rola ValidationPipe w NestJS?

  2. 2. Co robi opcja whitelist: true w ValidationPipe?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij konfigurację ValidationPipe w funkcji bootstrap

  • Układanie w poziomie

    Ułóż elementy konfiguracji globalnego ValidationPipe w prawidłowej kolejności:

  • Edytor kodu

    Uzupełnij komentarze opisujące zachowanie każdego trybu ValidationPipe

  • Układanie w pionie

    Uporządkuj tryby ValidationPipe od najmniej do najbardziej restrykcyjnego:

Przydatne artykuły