Walidacja pilnuje tego, co wchodzi. Ta lekcja jest o czymś odwrotnym: co i w jakiej postaci wychodzi z twojego API.
Problem widać najprościej na przykładzie. Serwis pobiera z bazy encję senatora - z hasłem, z wysokością żołdu, z wewnętrznym identyfikatorem sesji. Kontroler zwraca ten obiekt. NestJS zamienia go na JSON i wysyła klientowi w całości, bo nikt nie powiedział, że czegoś nie powinien. Wyciek danych nie wymaga błędu; wystarczy brak decyzji.
class-transformer daje trzy narzędzia do opisania, co ma się stać z polem przy zamianie obiektu na odpowiedź:1export class SenatorResponseDto {
2 @Expose()
3 name: string;
4
5 @Expose()
6 province: string;
7
8 @Expose()
9 @Transform(({ value }) => value.toISOString().slice(0, 10))
10 appointedAt: Date;
11
12 @Exclude()
13 password: string;
14
15 @Exclude()
16 salary: number;
17}
ukrywa pole przy konwersji obiektu na odpowiedź JSON. Nie usuwa pola z bazy danych - encja zostaje nietknięta. Nie wyłącza walidacji dla tego pola. I nie blokuje dostępu do niego w TypeScripcie; w kodzie serwisu @Exclude()
senator.password nadal działa.
robi rzecz odwrotną - oznacza pole jako widoczne. Domyślnie widoczne są wszystkie, więc sam @Expose()
@Expose() niewiele zmienia; nabiera znaczenia przy opcji excludeExtraneousValues: true, która odwraca zasadę: wtedy wychodzi tylko to, co jawnie wystawione.
definiuje własną logikę transformacji wartości pola. Nie animuje zmiany wartości, nie tworzy kopii zapasowej i nie zmienia typu pola w TypeScripcie. Funkcja dostaje obiekt z polem @Transform()
value i zwraca to, co ma trafić do odpowiedzi - tu data skrócona do samego dnia.Najważniejsze zdanie tej lekcji: same dekoratory nic nie robią. To tylko adnotacje; ktoś musi je wykonać.
Aby
i @Exclude
działały w kontrolerze, trzeba dodać @Expose
. Nie instalować dodatkowej biblioteki @UseInterceptors(ClassSerializerInterceptor)
class-serializer - taka nie istnieje. Nie konfigurować middleware w app.module.ts. I nie ma żadnego specjalnego dekoratora @Serialize() nad metodą.1@Controller('senators')
2@UseInterceptors(ClassSerializerInterceptor)
3export class SenatorsController {
4 @Get(':id')
5 findOne(@Param('id') id: string) {
6 return this.senatorsService.findOne(id);
7 }
8}To jest ta klasa błędu, która nie daje żadnego sygnału. Kod się kompiluje, testy jednostkowe serwisu przechodzą, a hasła jadą do klienta - bo interceptora nikt nie podpiął. Sprawdzenie zajmuje pół minuty: wywołaj endpoint i przeczytaj odpowiedź.
Interceptor można też zarejestrować globalnie przez
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))) - i przy danych wrażliwych to bezpieczniejszy wybór, bo pominięcie dekoratora na jednym kontrolerze przestaje być możliwe.Cała transformacja przebiega w czterech krokach, zawsze w tej kolejności:
ClassSerializerInterceptor stosuje @Exclude/@Expose.Zwróć uwagę, gdzie stoi krok trzeci: po kontrolerze, nie przed nim. Interceptor działa na tym, co metoda zwróciła - i dlatego serwis oraz kontroler operują na pełnym obiekcie, ze wszystkimi polami. Filtrowanie jest ostatnią czynnością przed wysłaniem, a nie warunkiem wcześniejszej pracy.
Stąd praktyczny wniosek: w logu wypisanym w serwisie hasło będzie widoczne, choć w odpowiedzi już nie.
@Exclude() chroni wyjście z API, nie twoje własne logi.Czasem potrzebujesz zamienić zwykły obiekt w instancję klasy bez udziału interceptora - na przykład w teście albo przy danych z kolejki:
1const dto = plainToInstance(CreateLegionaryDto, rawData);Kolejność argumentów jest stała:
→ plainToInstance(
→ CreateLegionaryDto
→ , rawData
. Najpierw klasa docelowa, potem surowe dane - łatwo pomylić kierunek, a zamiana miejscami daje obiekt, który wygląda poprawnie i nie ma żadnego z twoich dekoratorów.)
Funkcja odwrotna,
instanceToPlain, zamienia instancję klasy w zwykły obiekt, stosując po drodze @Exclude i @Expose. To dokładnie ta operacja, którą wykonuje za ciebie ClassSerializerInterceptor.Wyciek danych nie wymaga błędu, wystarczy brak decyzji, @name:
@Exclude() ukrywa pole przy konwersji obiektu na odpowiedź JSON - nie usuwa go z bazy, nie wyłącza walidacji, nie blokuje dostępu w TypeScripcie,@Expose() oznacza pole jako widoczne; nabiera znaczenia przy excludeExtraneousValues: true,@Transform() definiuje własną logikę transformacji wartości pola - nie animuje, nie tworzy kopii, nie zmienia typu,@Exclude i @Expose zadziałały, kontroler potrzebuje @UseInterceptors(ClassSerializerInterceptor) - nie biblioteki class-serializer, nie middleware w app.module.ts, nie dekoratora @Serialize(),ClassSerializerInterceptor stosuje @Exclude/@Expose → klient dostaje przefiltrowany JSON,plainToInstance( → CreateLegionaryDto → , rawData → ): najpierw klasa, potem dane,instanceToPlain działa w drugą stronę - to ta sama operacja, którą wykonuje interceptor.W następnej lekcji zejdziemy do walidacji zagnieżdżonej. A na razie zapamiętaj: o tym, co wychodzi z twojego API, decydujesz ty albo przypadek. Trzeciej możliwości nie ma.