Kurs NestJS · Moduł 1: Podstawy NestJS

DTO i podstawowa walidacja

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

DekoratorOpis
@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?

  1. Bezpieczeństwo - blokują niechciane dane zanim dotrą do logiki biznesowej
  2. Dokumentacja - jasno definiują kontrakt API
  3. Walidacja - automatyczna weryfikacja poprawności danych
  4. Typowanie - TypeScript zna kształt danych w całej aplikacji
  5. 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)");
128

Sprawdź się

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

  1. 1. Co oznacza skrót DTO w kontekście NestJS?

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

Przydatne artykuły