Kurs NestJS · Moduł 1: Podstawy NestJS
DTO i podstawowa walidacja
W tej lekcji8
Ave, strażniku bram! Konsul Caesar.js wie, że bezpieczeństwo imperium zaczyna się od kontroli tego, co wchodzi przez bramy. Nie możemy wpuścić do naszego systemu nieprawidłowych danych - to jak wpuszczenie szpiegów do obozu legionów! Czas poznać DTO i walidację - tarcze ochronne naszego API.
Czym jest DTO?
DTO (Data Transfer Object) to klasa, która definiuje kształt danych przesyłanych do i z naszego API. Pomyśl o DTO jak o formularzu rekrutacyjnym do legionu - każdy nowy legionista musi wypełnić odpowiednie pola, zanim zostanie przyjęty.
Bez DTO nasz kontroler przyjmuje cokolwiek - to jak otwarta brama bez straży:
1// BEZ DTO - niebezpiecznie!
2@Post()
3create(@Body() body: any) {
4 // body może zawierać cokolwiek... niebezpiecznie!
5 return this.legionariesService.create(body);
6}Z DTO dokładnie definiujemy, jakie dane akceptujemy:
1// Z DTO - bezpieczne jak fort rzymski!
2@Post()
3create(@Body() createLegionaryDto: CreateLegionaryDto) {
4 return this.legionariesService.create(createLegionaryDto);
5}Tworzenie klas DTO
Stwórzmy DTO dla naszego systemu legionistów:
1// dto/create-legionary.dto.ts
2export class CreateLegionaryDto {
3 name: string;
4 rank: string;
5 province: string;
6 email: string;
7 experience: number;
8}To jednak tylko kształt danych - nie sprawdza jeszcze, czy dane są prawidłowe. Tu wkracza walidacja!
Instalacja class-validator i class-transformer
Aby dodać walidację, potrzebujemy dwóch bibliotek:
1npm install class-validator class-transformer- class-validator - dostarcza dekoratory walidacji (@IsString, @IsNumber, itd.)
- class-transformer - przekształca zwykłe obiekty JSON na instancje klas DTO
Dekoratory walidacji
Teraz dodajmy walidację do naszego DTO:
1// dto/create-legionary.dto.ts
2import {
3 IsString, IsNumber, IsEmail,
4 MinLength, MaxLength, Min, Max,
5 IsNotEmpty, IsOptional, IsIn
6} from 'class-validator';
7
8export class CreateLegionaryDto {
9 @IsString()
10 @IsNotEmpty()
11 @MinLength(2)
12 @MaxLength(50)
13 name: string;
14
15 @IsString()
16 @IsIn(['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'])
17 rank: string;
18
19 @IsString()
20 @IsNotEmpty()
21 province: string;
22
23 @IsEmail()
24 email: string;
25
26 @IsNumber()
27 @Min(0)
28 @Max(30)
29 experience: number;
30}Oto najczęściej używane dekoratory walidacji:
| Dekorator | Opis |
|---|---|
@IsString() | Sprawdza, czy wartość to tekst |
@IsNumber() | Sprawdza, czy wartość to liczba |
@IsEmail() | Sprawdza, czy wartość to prawidłowy email |
@IsNotEmpty() | Sprawdza, czy pole nie jest puste |
@MinLength(n) | Minimalna długość tekstu |
@MaxLength(n) | Maksymalna długość tekstu |
@Min(n) | Minimalna wartość liczbowa |
@Max(n) | Maksymalna wartość liczbowa |
@IsOptional() | Pole jest opcjonalne |
@IsIn([...]) | Wartość musi być jedną z podanych |
DTO do aktualizacji - PartialType
Do częściowej aktualizacji (PATCH) możemy użyć PartialType, który sprawia, że wszystkie pola są opcjonalne:
1// dto/update-legionary.dto.ts
2import { PartialType } from '@nestjs/mapped-types';
3import { CreateLegionaryDto } from './create-legionary.dto';
4
5export class UpdateLegionaryDto extends PartialType(CreateLegionaryDto) {}
6// Teraz wszystkie pola są opcjonalne!PartialType pochodzi z osobnego pakietu, który doinstalujesz poleceniem npm i @nestjs/mapped-types. Automatycznie tworzy nową klasę, w której każde pole z CreateLegionaryDto staje się opcjonalne. Dzięki temu nie musimy pisać osobnego DTO z @IsOptional() na każdym polu.
ValidationPipe - globalna walidacja
Aby walidacja działała, musimy aktywować ValidationPipe w naszej aplikacji:
1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { ValidationPipe } from '@nestjs/common';
4import { AppModule } from './app.module';
5
6async function bootstrap() {
7 const app = await NestFactory.create(AppModule);
8
9 app.useGlobalPipes(new ValidationPipe({
10 whitelist: true, // Usuwa pola, które nie są w DTO
11 forbidNonWhitelisted: true, // Rzuca błąd, gdy są nieznane pola
12 transform: true, // Automatyczna konwersja typów
13 }));
14
15 await app.listen(3000);
16}
17bootstrap();Trzy kluczowe opcje ValidationPipe:
- whitelist: true - automatycznie usuwa pola, których nie ma w DTO (jak strażnik odrzucający podejrzane przesyłki)
- forbidNonWhitelisted: true - zamiast cicho usuwać nieznane pola, rzuca błąd (surowsza kontrola)
- transform: true - automatycznie konwertuje dane na odpowiednie typy (string "5" na number 5)
Kompletny przykład kontrolera z DTO
1@Controller('legionaries')
2export class LegionariesController {
3 constructor(private readonly legionariesService: LegionariesService) {}
4
5 @Post()
6 create(@Body() createDto: CreateLegionaryDto) {
7 return this.legionariesService.create(createDto);
8 }
9
10 @Patch(':id')
11 update(
12 @Param('id') id: string,
13 @Body() updateDto: UpdateLegionaryDto,
14 ) {
15 return this.legionariesService.update(Number(id), updateDto);
16 }
17}Kiedy ktoś prześle nieprawidłowe dane, ValidationPipe automatycznie zwróci czytelny błąd:
1{
2 "statusCode": 400,
3 "message": [
4 "name must be longer than or equal to 2 characters",
5 "email must be an email",
6 "experience must not be greater than 30"
7 ],
8 "error": "Bad Request"
9}Dlaczego DTO są ważne?
- Bezpieczeństwo - blokują niechciane dane zanim dotrą do logiki biznesowej
- Dokumentacja - jasno definiują kontrakt API
- Walidacja - automatyczna weryfikacja poprawności danych
- Typowanie - TypeScript zna kształt danych w całej aplikacji
- Separacja - oddzielają warstwę transportu od logiki biznesowej
DTO i walidacja to tarcza i miecz naszego imperium - chronią bramy API i zapewniają, że do systemu trafiają tylko prawidłowe dane. Każdy dobry budowniczy imperium wie, że silne mury to podstawa!
Kod do tej lekcji: src/dto-validation.ts
1// DTOs i Walidacja - Tarcze Ochronne Imperium
2import { IsString, IsNumber, IsEmail, MinLength, MaxLength, Min, Max, IsNotEmpty, IsOptional, IsIn } from 'class-validator';
3
4console.log("DTOs i walidacja - strażnik bram imperium!");
5
6// ===========================================
7// 1. DTO - Data Transfer Object
8// ===========================================
9
10// Klasa DTO definiuje kształt danych
11class CreateLegionaryDto {
12 // @IsString()
13 // @IsNotEmpty()
14 // @MinLength(2)
15 // @MaxLength(50)
16 name: string;
17
18 // @IsString()
19 // @IsIn(['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'])
20 rank: string;
21
22 // @IsString()
23 // @IsNotEmpty()
24 province: string;
25
26 // @IsEmail()
27 email: string;
28
29 // @IsNumber()
30 // @Min(0)
31 // @Max(30)
32 experience: number;
33}
34
35console.log("=== CREATE LEGIONARY DTO ===");
36console.log("name: @IsString, @IsNotEmpty, @MinLength(2)");
37console.log("rank: @IsString, @IsIn([...])");
38console.log("province: @IsString, @IsNotEmpty");
39console.log("email: @IsEmail");
40console.log("experience: @IsNumber, @Min(0), @Max(30)");
41
42// ===========================================
43// 2. Dekoratory walidacji
44// ===========================================
45
46console.log("\n=== DEKORATORY WALIDACJI ===");
47console.log("@IsString() - sprawdza czy tekst");
48console.log("@IsNumber() - sprawdza czy liczba");
49console.log("@IsEmail() - sprawdza czy email");
50console.log("@IsNotEmpty() - sprawdza czy niepuste");
51console.log("@MinLength(n) - minimalna długość tekstu");
52console.log("@MaxLength(n) - maksymalna długość tekstu");
53console.log("@Min(n) - minimalna wartość liczbowa");
54console.log("@Max(n) - maksymalna wartość liczbowa");
55console.log("@IsOptional() - pole opcjonalne");
56console.log("@IsIn([...]) - wartość z listy");
57
58// ===========================================
59// 3. Update DTO z PartialType
60// ===========================================
61
62// import { PartialType } from '@nestjs/mapped-types';
63// class UpdateLegionaryDto extends PartialType(CreateLegionaryDto) {}
64// -> Wszystkie pola stają się opcjonalne!
65
66class UpdateLegionaryDto {
67 name?: string;
68 rank?: string;
69 province?: string;
70 email?: string;
71 experience?: number;
72}
73
74console.log("\n=== UPDATE DTO (PartialType) ===");
75console.log("PartialType sprawia, że wszystkie pola są opcjonalne");
76console.log("Idealne do PATCH - częściowa aktualizacja");
77
78// ===========================================
79// 4. ValidationPipe - globalna walidacja
80// ===========================================
81
82// W main.ts:
83// app.useGlobalPipes(new ValidationPipe({
84// whitelist: true, // Usuwa nieznane pola
85// forbidNonWhitelisted: true, // Błąd na nieznane pola
86// transform: true, // Konwersja typów
87// }));
88
89console.log("\n=== VALIDATION PIPE ===");
90console.log("whitelist: true -> usuwa pola spoza DTO");
91console.log("forbidNonWhitelisted: true -> błąd na nieznane pola");
92console.log("transform: true -> konwersja typów (string -> number)");
93
94// ===========================================
95// 5. Przykład użycia w kontrolerze
96// ===========================================
97
98console.log("\n=== KONTROLER Z DTO ===");
99console.log("@Post()");
100console.log("create(@Body() createDto: CreateLegionaryDto) { ... }");
101console.log("");
102console.log("@Patch(':id')");
103console.log("update(@Param('id') id, @Body() updateDto: UpdateLegionaryDto) { ... }");
104
105// ===========================================
106// 6. Przykład odpowiedzi błędnej walidacji
107// ===========================================
108
109const validationError = {
110 statusCode: 400,
111 message: [
112 "name must be longer than or equal to 2 characters",
113 "email must be an email",
114 "experience must not be greater than 30"
115 ],
116 error: "Bad Request"
117};
118
119console.log("\n=== PRZYKŁAD BŁĘDU WALIDACJI ===");
120console.log(JSON.stringify(validationError, null, 2));
121
122console.log("\n=== PODSUMOWANIE ===");
123console.log("1. DTO = kształt danych (kontrakt API)");
124console.log("2. class-validator = dekoratory walidacji");
125console.log("3. class-transformer = konwersja obiektów");
126console.log("4. ValidationPipe = automatyczna walidacja");
127console.log("5. PartialType = opcjonalne pola (do PATCH)");
128Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza skrót DTO w kontekście NestJS?
2. Który dekorator class-validator sprawdza, czy wartość jest prawidłowym adresem email?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj kroki dodawania walidacji DTO w NestJS:
- Edytor kodu
Napisz klasę CreateLegionaryDto z polami name (@IsString, @IsNotEmpty) i email (@IsEmail)
- Klikanie w kolejności
Ułóż składnię aktywacji ValidationPipe w main.ts:
- Układanie w poziomie
Ułóż elementy tworzenia UpdateDto z PartialType:
- Edytor kodu
W pliku main.ts napisz funkcję setupValidation(app), która włącza w aplikacji globalną walidację: wywołuje app.useGlobalPipes() z nowym ValidationPipe, w którym opcje whitelist i transform są ustawione na true.