Kurs NestJS · Moduł 1: Podstawy NestJS

Pierwsze REST API - operacje CRUD

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

OperacjaMetoda HTTPDekorator NestJS
CreatePOST@Post()
ReadGET@Get()
UpdatePUT / PATCH@Put() / @Patch()
DeleteDELETE@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:

KodNazwaKiedy używać
200OKSukces (domyślny dla GET, PUT, PATCH)
201CreatedNowy zasób został stworzony (POST)
204No ContentSukces bez treści odpowiedzi (DELETE)
400Bad RequestNieprawidłowe dane wejściowe
404Not FoundZasób nie istnieje
500Internal Server ErrorBłą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/1

Brawo, 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");
126

Widzisz błąd w tej lekcji?

Sprawdź się

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

  1. 1. Co oznacza akronim CRUD?

  2. 2. Jaki kod odpowiedzi NestJS zwraca domyślnie z metody oznaczonej dekoratorem @Post()?

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

Przydatne artykuły