Kurs NestJS · Moduł 1: Podstawy NestJS
Pierwsze REST API - operacje CRUD
W tej lekcji9
Ave, budowniczy dróg! Konsul Caesar.js powierza Ci teraz jedno z najważniejszych zadań w imperium - budowę systemu dróg REST API. Tak jak rzymskie drogi łączyły wszystkie prowincje imperium, tak REST API łączy naszą aplikację serwerową ze światem zewnętrznym. Czas nauczyć się operacji CRUD - fundamentu każdego REST API!
Czym jest CRUD?
CRUD to akronim od czterech podstawowych operacji na danych:
- Create (Tworzenie) - rekrutacja nowych legionistów
- Read (Odczyt) - przeglądanie rejestrów imperium
- Update (Aktualizacja) - awansowanie legionistów
- Delete (Usuwanie) - wykreślanie z rejestrów
Każda operacja CRUD odpowiada konkretnej metodzie HTTP i dekoratorowi w NestJS:
| Operacja | Metoda HTTP | Dekorator NestJS |
|---|---|---|
| Create | POST | @Post() |
| Read | GET | @Get() |
| Update | PUT / PATCH | @Put() / @Patch() |
| Delete | DELETE | @Delete() |
Budujemy kontroler CRUD krok po kroku
Zbudujmy kompletny kontroler do zarządzania legionistami naszego imperium:
1import {
2 Controller, Get, Post, Put, Delete, Patch,
3 Param, Body, Query, HttpCode, HttpStatus, NotFoundException
4} from '@nestjs/common';
5
6// Interfejs legionisty
7interface Legionary {
8 id: number;
9 name: string;
10 rank: string;
11 province: string;
12}
13
14@Controller('legionaries')
15export class LegionariesController {
16 // Tymczasowa tablica jako "baza danych"
17 private legionaries: Legionary[] = [
18 { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', province: 'Roma' },
19 { id: 2, name: 'Gaius Julius', rank: 'Miles', province: 'Gallia' },
20 ];
21 // Następny wolny numer (1 i 2 są zajęte)
22 private nextId = 3;
23}READ - odczyt danych (@Get)
Dekorator @Get() obsługuje żądania HTTP GET. Używamy go do pobierania danych:
1@Controller('legionaries')
2export class LegionariesController {
3
4 // GET /legionaries - pobierz wszystkich legionistów
5 @Get()
6 findAll(): Legionary[] {
7 return this.legionaries;
8 }
9
10 // GET /legionaries/search?province=Roma - filtrowanie po prowincji
11 @Get('search')
12 findByProvince(@Query('province') province: string): Legionary[] {
13 return this.legionaries.filter(l => l.province === province);
14 }
15
16 // GET /legionaries/1 - pobierz jednego po ID
17 @Get(':id')
18 findOne(@Param('id') id: string): Legionary {
19 const legionary = this.legionaries.find(l => l.id === Number(id));
20 if (!legionary) {
21 throw new NotFoundException('Legionista nie znaleziony!');
22 }
23 return legionary;
24 }
25}Zauważ trzy kluczowe dekoratory parametrów:
@Param('id')- wyciąga parametr z URL (np./legionaries/1)@Query('province')- wyciąga parametr zapytania (np.?province=Roma)@Body()- wyciąga dane z ciała żądania (dla POST/PUT)
Dwie rzeczy w tym kodzie są celowe. Trasa search stoi przed :id - inaczej GET /legionaries/search trafiłoby do findOne z id równym 'search'. A brak legionisty zgłaszamy wyjątkiem NotFoundException z @nestjs/common: NestJS odpowie wtedy kodem 404 Not Found. Zwykły throw new Error(...) dałby klientowi 500 Internal Server Error, jakby zepsuł się serwer.
CREATE - tworzenie danych (@Post)
Dekorator @Post() obsługuje żądania HTTP POST. Służy do tworzenia nowych zasobów:
1// POST /legionaries - rekrutacja nowego legionisty
2@Post()
3@HttpCode(HttpStatus.CREATED) // Zwraca status 201
4create(@Body() newLegionary: { name: string; rank: string; province: string }): Legionary {
5 const legionary: Legionary = {
6 id: this.nextId++,
7 ...newLegionary,
8 };
9 this.legionaries.push(legionary);
10 return legionary;
11}Dekorator @Body() wyciąga dane z ciała żądania JSON. Dekorator @HttpCode() pozwala ustawić kod odpowiedzi HTTP - dla tworzenia zasobów standardem jest 201 Created.
Numer nowego legionisty nadaje licznik nextId z klasy kontrolera. Rośnie przy każdej rekrutacji, więc nie powtórzy się nawet po wykreśleniu kogoś z tablicy - numer liczony jako length + 1 mógłby już należeć do innego legionisty.
UPDATE - aktualizacja danych (@Put i @Patch)
Mamy dwa dekoratory do aktualizacji:
@Put()- zastępuje cały zasób (pełna aktualizacja)@Patch()- aktualizuje tylko wybrane pola (częściowa aktualizacja)
1// PUT /legionaries/1 - pełna aktualizacja legionisty
2@Put(':id')
3update(
4 @Param('id') id: string,
5 @Body() updateData: { name: string; rank: string; province: string },
6): Legionary {
7 const index = this.legionaries.findIndex(l => l.id === Number(id));
8 if (index === -1) {
9 throw new NotFoundException('Legionista nie znaleziony!');
10 }
11 this.legionaries[index] = { id: Number(id), ...updateData };
12 return this.legionaries[index];
13}
14
15// PATCH /legionaries/1 - częściowa aktualizacja (np. tylko ranga)
16@Patch(':id')
17partialUpdate(
18 @Param('id') id: string,
19 @Body() updateData: Partial<Legionary>,
20): Legionary {
21 const index = this.legionaries.findIndex(l => l.id === Number(id));
22 if (index === -1) {
23 throw new NotFoundException('Legionista nie znaleziony!');
24 }
25 this.legionaries[index] = { ...this.legionaries[index], ...updateData };
26 return this.legionaries[index];
27}DELETE - usuwanie danych (@Delete)
Dekorator @Delete() obsługuje żądania HTTP DELETE:
1// DELETE /legionaries/1 - wykreślenie legionisty
2@Delete(':id')
3@HttpCode(HttpStatus.NO_CONTENT) // Zwraca status 204
4remove(@Param('id') id: string): void {
5 const index = this.legionaries.findIndex(l => l.id === Number(id));
6 if (index === -1) {
7 throw new NotFoundException('Legionista nie znaleziony!');
8 }
9 this.legionaries.splice(index, 1);
10}Dla operacji usuwania standardowy kod odpowiedzi to 204 No Content - oznacza sukces bez zwracania danych.
Kody statusów HTTP
Znajomość kodów HTTP to obowiązek każdego budowniczego API:
| Kod | Nazwa | Kiedy używać |
|---|---|---|
| 200 | OK | Sukces (domyślny dla GET, PUT, PATCH) |
| 201 | Created | Nowy zasób został stworzony (POST) |
| 204 | No Content | Sukces bez treści odpowiedzi (DELETE) |
| 400 | Bad Request | Nieprawidłowe dane wejściowe |
| 404 | Not Found | Zasób nie istnieje |
| 500 | Internal Server Error | Błąd serwera |
Kontroler + Serwis - prawidłowy wzorzec
W prawdziwym imperium kontroler nie wykonuje logiki sam - deleguje ją do serwisu:
1// legionaries.service.ts
2@Injectable()
3export class LegionariesService {
4 private legionaries: Legionary[] = [];
5 private nextId = 1;
6
7 findAll(): Legionary[] {
8 return this.legionaries;
9 }
10
11 findOne(id: number): Legionary {
12 return this.legionaries.find(l => l.id === id);
13 }
14
15 create(data: { name: string; rank: string; province: string }): Legionary {
16 const legionary = { id: this.nextId++, ...data };
17 this.legionaries.push(legionary);
18 return legionary;
19 }
20
21 update(id: number, data: Partial<Legionary>): Legionary {
22 const index = this.legionaries.findIndex(l => l.id === id);
23 this.legionaries[index] = { ...this.legionaries[index], ...data };
24 return this.legionaries[index];
25 }
26
27 remove(id: number): void {
28 this.legionaries = this.legionaries.filter(l => l.id !== id);
29 }
30}
31
32// legionaries.controller.ts
33@Controller('legionaries')
34export class LegionariesController {
35 constructor(private readonly legionariesService: LegionariesService) {}
36
37 @Get()
38 findAll() {
39 return this.legionariesService.findAll();
40 }
41
42 @Post()
43 create(@Body() data: { name: string; rank: string; province: string }) {
44 return this.legionariesService.create(data);
45 }
46}Testowanie z curl
Możesz przetestować swoje API używając narzędzia curl w terminalu:
1# GET - pobierz wszystkich
2curl http://localhost:3000/legionaries
3
4# POST - stwórz nowego
5curl -X POST http://localhost:3000/legionaries \
6 -H "Content-Type: application/json" \
7 -d '{"name": "Titus Flavius", "rank": "Miles", "province": "Judea"}'
8
9# PUT - pełna aktualizacja
10curl -X PUT http://localhost:3000/legionaries/1 \
11 -H "Content-Type: application/json" \
12 -d '{"name": "Marcus Aurelius", "rank": "Legatus", "province": "Roma"}'
13
14# DELETE - usunięcie
15curl -X DELETE http://localhost:3000/legionaries/1Brawo, budowniczy! Teraz znasz fundamenty REST API - drogi, które łączą wszystkie prowincje naszego imperium NestJS!
Kod do tej lekcji: src/crud-controller.ts
1// Pierwsze REST API - Operacje CRUD w Imperium
2import {
3 Controller, Get, Post, Put, Delete, Patch,
4 Param, Body, Query, HttpCode, HttpStatus, Injectable
5} from '@nestjs/common';
6
7console.log("REST API CRUD - drogi imperium!");
8
9// ===========================================
10// 1. Interfejs i dane
11// ===========================================
12
13interface Legionary {
14 id: number;
15 name: string;
16 rank: string;
17 province: string;
18}
19
20const legionaries: Legionary[] = [
21 { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', province: 'Roma' },
22 { id: 2, name: 'Gaius Julius', rank: 'Miles', province: 'Gallia' },
23 { id: 3, name: 'Titus Flavius', rank: 'Optio', province: 'Judea' },
24];
25
26// ===========================================
27// 2. READ - @Get() - pobieranie danych
28// ===========================================
29
30// GET /legionaries - lista wszystkich
31// @Get()
32function findAll(): Legionary[] {
33 return legionaries;
34}
35
36// GET /legionaries/:id - jeden po ID
37// @Get(':id')
38function findOne(id: string): Legionary | undefined {
39 return legionaries.find(l => l.id === Number(id));
40}
41
42// GET /legionaries?province=Roma - filtrowanie
43// @Get('search')
44function findByProvince(province: string): Legionary[] {
45 return legionaries.filter(l => l.province === province);
46}
47
48console.log("GET /legionaries:", findAll());
49console.log("GET /legionaries/1:", findOne('1'));
50console.log("GET ?province=Roma:", findByProvince('Roma'));
51
52// ===========================================
53// 3. CREATE - @Post() - tworzenie danych
54// ===========================================
55
56// POST /legionaries
57function create(data: { name: string; rank: string; province: string }): Legionary {
58 const newLegionary: Legionary = {
59 id: legionaries.length + 1,
60 ...data,
61 };
62 legionaries.push(newLegionary);
63 return newLegionary;
64}
65
66const created = create({ name: 'Lucius Verus', rank: 'Miles', province: 'Syria' });
67console.log("\nPOST - nowy legionista:", created);
68
69// ===========================================
70// 4. UPDATE - @Put() / @Patch()
71// ===========================================
72
73// PUT /legionaries/:id - pełna aktualizacja
74function update(id: string, data: Omit<Legionary, 'id'>): Legionary | null {
75 const index = legionaries.findIndex(l => l.id === Number(id));
76 if (index === -1) return null;
77 legionaries[index] = { id: Number(id), ...data };
78 return legionaries[index];
79}
80
81// PATCH /legionaries/:id - częściowa aktualizacja
82function partialUpdate(id: string, data: Partial<Legionary>): Legionary | null {
83 const index = legionaries.findIndex(l => l.id === Number(id));
84 if (index === -1) return null;
85 legionaries[index] = { ...legionaries[index], ...data };
86 return legionaries[index];
87}
88
89console.log("\nPUT /legionaries/2:", update('2', { name: 'Gaius Julius', rank: 'Centurio', province: 'Roma' }));
90console.log("PATCH /legionaries/3:", partialUpdate('3', { rank: 'Centurio' }));
91
92// ===========================================
93// 5. DELETE - @Delete() - usuwanie danych
94// ===========================================
95
96function remove(id: string): boolean {
97 const index = legionaries.findIndex(l => l.id === Number(id));
98 if (index === -1) return false;
99 legionaries.splice(index, 1);
100 return true;
101}
102
103console.log("\nDELETE /legionaries/4:", remove('4'));
104console.log("Stan po operacjach:", legionaries);
105
106// ===========================================
107// 6. Kody statusów HTTP
108// ===========================================
109
110console.log("\n=== KODY STATUSÓW HTTP ===");
111console.log("200 OK - sukces (GET, PUT, PATCH)");
112console.log("201 Created - zasób stworzony (POST)");
113console.log("204 No Content - sukces bez treści (DELETE)");
114console.log("400 Bad Request - nieprawidłowe dane");
115console.log("404 Not Found - zasób nie istnieje");
116
117console.log("\n=== DEKORATORY CRUD ===");
118console.log("@Get() -> odczyt danych");
119console.log("@Post() -> tworzenie danych");
120console.log("@Put() -> pełna aktualizacja");
121console.log("@Patch() -> częściowa aktualizacja");
122console.log("@Delete() -> usuwanie danych");
123console.log("@Param() -> parametr z URL");
124console.log("@Body() -> dane z ciała żądania");
125console.log("@Query() -> parametr zapytania");
126Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza akronim CRUD?
2. Jaki kod odpowiedzi NestJS zwraca domyślnie z metody oznaczonej dekoratorem @Post()?
3. Do czego służy dekorator @Param() w kontrolerze NestJS?
Zadania praktyczne w grze
- Edytor kodu
W kontrolerze LegionController z dekoratorem @Controller('legionaries') trzymaj tablicę legionistów, np. [{ id: 1, name: 'Marcus Aurelius' }]. Napisz metodę findOne z @Get(':id'), która odczytuje id przez @Param('id') i zwraca legionistę o tym id, a gdy go nie ma, rzuca NotFoundException. Pamiętaj, że id z adresu jest tekstem.
- Układanie w poziomie
Ułóż elementy metody POST w kontrolerze NestJS:
- Układanie w pionie
Uporządkuj kody statusów HTTP od sukcesu do błędu serwera:
- Edytor kodu
W kontrolerze LegionController z dekoratorem @Controller('legionaries') i tablicą legionistów napisz metodę remove z @Delete(':id') i @HttpCode(HttpStatus.NO_CONTENT), która odczytuje id przez @Param('id') i usuwa legionistę o tym id z tablicy. Metoda nic nie zwraca.