Kurs NestJS · Moduł 1: Podstawy NestJS

Kontrolery - centurionowie Imperium

5 min czytania
W tej lekcji5

Do bramy obozu przybywa posłaniec z żądaniem: GET /legion/5. Serwer je odebrał - ale kto ma je obsłużyć? Aplikacja ma kilkadziesiąt metod i żadna nie wie, że to właśnie do niej.

W legionie od rozdzielania rozkazów jest centurion. Stoi między posłańcem a żołnierzami: przyjmuje rozkaz, rozpoznaje, kogo dotyczy, przekazuje dalej i odsyła odpowiedź. Sam nie wykonuje pracy - on ją kieruje. W NestJS tym centurionem jest kontroler.

Deklaracja kontrolera

Kontroler to klasa z dekoratorem wskazującym, jakim odcinkiem tras się zajmuje:

1@Controller('legion')
2export class LegionController { }

Kolejność jest zawsze ta sama: @Controller('legion'), potem export class, na końcu nazwa klasy z ciałem.

Argument 'legion' to prefiks trasy. Wszystkie metody tej klasy będą obsługiwać adresy zaczynające się od /legion - i nie musisz tego powtarzać przy każdej z nich. Kontroler bez argumentu, @Controller(), przejmuje trasy od korzenia.

Cztery czasowniki

Wewnątrz kontrolera każda metoda dostaje dekorator mówiący, na jakie żądanie odpowiada:

1@Controller('tributes')
2export class TributesController {
3  @Get()
4  findAll() { }
5
6  @Post()
7  create() { }
8
9  @Put(':id')
10  update() { }
11
12  @Delete(':id')
13  remove() { }
14}

Cztery dekoratory odpowiadają czterem czynnościom na zasobie. @Get() pobiera, @Post() tworzy nowy zasób, @Put() aktualizuje istniejący, @Delete() usuwa.

Rozróżnienie @Post od @Put bywa mylące, więc zapamiętaj je po skutku powtórzenia: wysłanie tego samego @Post dwa razy utworzy dwa zasoby, a tego samego @Put - zostawi jeden, po prostu zapisany dwukrotnie.

Argument dekoratora dokłada kawałek trasy. @Put(':id') w kontrolerze z prefiksem tributes obsłuży adres /tributes/42. Dwukropek oznacza parametr - miejsce, w które wpada dowolna wartość.

Trzy miejsca, z których biorą się dane

Skoro trasa może zawierać parametr, trzeba go jakoś odczytać. Dane przychodzą w żądaniu trzema drogami i każda ma swój dekorator:

1@Get('search')
2search(@Query('province') province: string) { }
3
4@Get(':id')
5findOne(@Param('id') id: string) { }
6
7@Post()
8create(@Body() dto: CreateTributeDto) { }

@Param('id') wyciąga wartość ze ścieżki adresu - to on odczyta 5 z /legion/5. @Query('province') bierze parametr zapytania, czyli to, co stoi po znaku zapytania: ?province=rome. @Body() sięga po ciało żądania, wysyłane przy @Post i @Put.

Zwróć uwagę na kolejność dwóch pierwszych metod. NestJS dopasowuje trasy w kolejności deklaracji, a :id pasuje do każdego tekstu - także do słowa search. Gdyby findOne stało wyżej, żądanie GET /legion/search trafiłoby do niego z id równym 'search'. Dlatego trasy ze stałym fragmentem deklarujemy przed trasami z parametrem.

Te trzy pokrywają wszystko, czego zwykle potrzebujesz. Uwaga na dwie nazwy, które brzmią prawdopodobnie: @Path() w NestJS nie istnieje - do ścieżki służy @Param. @Header() istnieje, ale robi coś innego: ustawia nagłówek odpowiedzi. Do czytania nagłówków żądania służy @Headers(), w liczbie mnogiej.

Metoda kontrolera od środka

Złóżmy to w całość. Kolejność elementów metody jest niezmienna:

1@Get()
2getAllLegionaries() {
3  return this.service.findAll();
4}

Najpierw dekorator @Get(), potem nazwa metody, dalej ciało w nawiasach klamrowych, a w nim return this.service.findAll().

I tu widać, czym kontroler naprawdę jest. Ta metoda nie zawiera logiki - przyjmuje żądanie i przekazuje je serwisowi. Centurion nie kuje mieczy; wie, do którego kowala posłać.

To jest reguła, którą polecam trzymać od pierwszego dnia: w kontrolerze nie ma zapytań do bazy, obliczeń ani reguł biznesowych. Gdy metoda kontrolera rośnie ponad kilka linii, to znak, że robi coś, co należy do serwisu. Zysk jest praktyczny - tę samą logikę wywołasz potem z zadania wsadowego albo konsumenta kolejki, gdzie żadnego HTTP nie ma.

Kontroler nie jest też pierwszym przystankiem żądania. Po drodze stoją warstwy, które poznasz w kolejnych modułach: najpierw middleware, potem guardy (straże, które decydują, czy wpuścić żądanie), dalej interceptory (przed wywołaniem metody) i pipe'y (sprawdzają i zamieniają dane, np. ValidationPipe). Dopiero wtedy działa metoda kontrolera, a jej wynik wraca do klienta przez interceptory.

Zwróć uwagę, że nie budujemy odpowiedzi ręcznie. Zwrócona wartość zostanie zamieniona na JSON, a kod odpowiedzi NestJS dobierze sam: 200 dla większości metod, 201 dla @Post, bo coś powstało.

Podsumowanie

Centurion stoi u bramy i wie, komu przekazać rozkaz:

  • kontroler przyjmuje żądanie i kieruje je dalej - sam nie wykonuje pracy,
  • deklaracja w kolejności: @Controller('prefiks'), export class, nazwa klasy,
  • argument @Controller to prefiks trasy wspólny dla wszystkich metod klasy,
  • cztery czasowniki: @Get() pobiera, @Post() tworzy nowy zasób, @Put() aktualizuje, @Delete() usuwa,
  • powtórzony @Post tworzy dwa zasoby, powtórzony @Put - jeden,
  • dwukropek w trasie oznacza parametr: @Put(':id') obsłuży /tributes/42,
  • trasę stałą (search) deklaruj przed trasą z parametrem (:id), bo parametr przechwyciłby jej adres,
  • @Param('id') czyta ze ścieżki, @Query('nazwa') czyta parametr zapytania po znaku ?, @Body() czyta ciało żądania,
  • @Path() nie istnieje; @Header() ustawia nagłówek odpowiedzi, a nagłówki żądania czyta @Headers(),
  • kolejność w metodzie: dekorator, nazwa, ciało, return this.service.findAll(),
  • w kontrolerze nie ma logiki biznesowej - dzięki temu wywołasz ją też poza HTTP,
  • kod odpowiedzi dobiera NestJS: 200, a dla @Post - 201.

W następnej lekcji poznasz tego, komu centurion przekazuje rozkazy - serwisy, czyli miejsce, w którym mieszka prawdziwa logika. A na razie zapamiętaj: kontroler rozpoznaje żądanie i wskazuje wykonawcę - a każde dane, których potrzebuje, przychodzą jedną z trzech dróg: ścieżką, zapytaniem albo ciałem.

Kod do tej lekcji: src/legion.controller.ts
1// Kontrolery - Centurionowie Struktur Imperium
2import {
3  Controller, Get, Post, Put, Delete,
4  Body, Param, Query, HttpCode, Header,
5  ParseIntPipe, DefaultValuePipe,
6} from '@nestjs/common';
7
8console.log("Kontrolery NestJS - centurionowie przyjmujący rozkazy!");
9
10// ===========================================
11// 1. Podstawowy kontroler z dekoratorami HTTP
12// ===========================================
13
14interface Legionary {
15  id: number;
16  name: string;
17  rank: string;
18  experience: number;
19}
20
21@Controller('legiones')
22export class LegionController {
23  private legionaries: Legionary[] = [
24    { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', experience: 10 },
25    { id: 2, name: 'Julia Domna', rank: 'Optio', experience: 8 },
26    { id: 3, name: 'Titus Flavius', rank: 'Miles', experience: 3 },
27  ];
28
29  // GET /legiones
30  @Get()
31  findAll(
32    @Query('rank') rank?: string,
33    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
34  ) {
35    let result = this.legionaries;
36    if (rank) {
37      result = result.filter(l => l.rank === rank);
38    }
39    return { legionaries: result.slice(0, limit), total: result.length };
40  }
41
42  // GET /legiones/:id
43  @Get(':id')
44  findOne(@Param('id', ParseIntPipe) id: number) {
45    const legionary = this.legionaries.find(l => l.id === id);
46    if (!legionary) {
47      return { error: 'Legionary non inventus!' };
48    }
49    return legionary;
50  }
51
52  // POST /legiones
53  @Post()
54  @HttpCode(201)
55  create(@Body() data: Omit<Legionary, 'id'>) {
56    const newLegionary: Legionary = {
57      id: Math.max(...this.legionaries.map(l => l.id)) + 1,
58      ...data,
59    };
60    this.legionaries.push(newLegionary);
61    return { message: 'Legionista zwerbowany!', legionary: newLegionary };
62  }
63
64  // PUT /legiones/:id
65  @Put(':id')
66  update(@Param('id', ParseIntPipe) id: number, @Body() data: Partial<Legionary>) {
67    const index = this.legionaries.findIndex(l => l.id === id);
68    if (index === -1) return { error: 'Legionary non inventus!' };
69    this.legionaries[index] = { ...this.legionaries[index], ...data };
70    return { message: 'Dane zaktualizowane!', legionary: this.legionaries[index] };
71  }
72
73  // DELETE /legiones/:id
74  @Delete(':id')
75  @HttpCode(204)
76  remove(@Param('id', ParseIntPipe) id: number) {
77    this.legionaries = this.legionaries.filter(l => l.id !== id);
78  }
79
80  // GET /legiones/search/:name
81  @Get('search/:name')
82  @Header('X-Imperium', 'Roma-Aeterna')
83  search(@Param('name') name: string) {
84    return this.legionaries.filter(
85      l => l.name.toLowerCase().includes(name.toLowerCase())
86    );
87  }
88}
89
90console.log("\n=== PODSUMOWANIE KONTROLERÓW ===");
91console.log("@Controller('prefix') - ustawia prefiks tras");
92console.log("@Get, @Post, @Put, @Delete - metody HTTP");
93console.log("@Param, @Query, @Body - pobieranie danych z żądania");
94console.log("@HttpCode, @Header - konfiguracja odpowiedzi");
95

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. Jak pobrać parametr z URL w kontrolerze NestJS (np. /legion/5)?

  2. 2. Który dekorator HTTP w NestJS służy do tworzenia nowego zasobu?

  3. 3. Jak pobrać query parameter (np. ?from=rome) w kontrolerze NestJS?

Zadania praktyczne w grze

  • Edytor kodu

    Napisz kontroler TributesController z dekoratorem @Controller('tributes') i czterema metodami: findAll z @Get(), create z @Post(), update z @Put(':id') i remove z @Delete(':id').

  • Układanie w pionie

    Ułóż elementy deklaracji kontrolera w prawidłowej kolejności:

  • Klikanie w kolejności

    Ułóż elementy metody GET kontrolera w prawidłowej kolejności:

  • Edytor kodu

    Napisz kontroler LegionController z dekoratorem @Controller('legion') i dwoma endpointami GET: search z @Get('search'), który odczytuje @Query('province') province i zwraca { province }, oraz findOne z @Get(':id'), który odczytuje @Param('id') id i zwraca { id }. Endpoint search zadeklaruj przed :id, bo trasa z parametrem przechwyciłaby adres /legion/search.

Przydatne artykuły