Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

ValidationPipe - automatyczna stacja kontrolna

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, @name:

  • 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.

Przejdź do CodeWorlds