Kurs NestJS · Moduł 1: Podstawy NestJS
Kontrolery - centurionowie Imperium
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
@Controllerto prefiks trasy wspólny dla wszystkich metod klasy, - cztery czasowniki:
@Get()pobiera,@Post()tworzy nowy zasób,@Put()aktualizuje,@Delete()usuwa, - powtórzony
@Posttworzy 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");
95Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak pobrać parametr z URL w kontrolerze NestJS (np. /legion/5)?
2. Który dekorator HTTP w NestJS służy do tworzenia nowego zasobu?
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.