Kurs NestJS · Moduł 11: Swagger i OpenAPI

Dekoratory kontrolerów - pieczęcie na dekretach

5 min czytania
W tej lekcji5

Dekret bez pieczęci to kartka. Nie wiadomo, kto go wydał, czego dotyczy ani czy w ogóle obowiązuje - a urzędnik w prowincji musi to rozstrzygnąć, zanim cokolwiek zrobi.

Endpoint bez opisu jest dokładnie taki sam. Swagger sam odczyta z kodu ścieżkę i metodę HTTP, ale nie odgadnie, co ten endpoint robi, które parametry są obowiązkowe ani co oznacza zwrócony przez niego kod. Dekoratory z pakietu @nestjs/swagger dopisują to, czego z samego kodu nie widać.

Ważna uwaga na początek: wszystkie te dekoratory tylko opisują. Żaden nie zmienia zachowania aplikacji - endpoint bez @ApiOperation działa tak samo, po prostu w dokumentacji jest bezimienny.

@ApiTags i @ApiOperation

Pierwsze dwie pieczęcie odpowiadają na pytania „gdzie to szukać" i „co to robi":

1@ApiTags('Prowincje')
2@Controller('provinces')
3export class ProvincesController {
4  @ApiOperation({
5    summary: 'Lista prowincji',
6    description: 'Zwraca wszystkie prowincje Imperium wraz z ich namiestnikami.',
7  })
8  @Get()
9  findAll() {
10    return this.provincesService.findAll();
11  }
12}

@ApiTags() grupuje endpointy w sekcje - w Swagger UI powstaje z tego rozwijana grupa. Stawiasz go nad klasą kontrolera, bo grupowanie dotyczy wszystkich jego metod naraz. Można podać kilka tagów po przecinku, gdy kontroler należy do dwóch obszarów.

@ApiOperation() przyjmuje dwie główne właściwości: summary i description. Nie url i method - te NestJS odczytuje z @Get i @Controller. Nie host i port - to konfiguracja serwera, nie opis endpointu. I nie controller ani service - to nazwy klas, o których dokumentacja nie musi wiedzieć.

Różnica między nimi jest praktyczna: summary to jedno zdanie widoczne na liście endpointów, description - dłuższy tekst rozwijany po kliknięciu. Jeśli masz napisać tylko jedno, pisz summary.

@ApiParam kontra @ApiQuery

Te dwa dekoratory mylą się najczęściej, bo oba opisują „coś, co przychodzi w adresie". Rozstrzyga miejsce w URL-u:

1@ApiParam({ name: 'id', description: 'Identyfikator prowincji' })
2@Get(':id')
3findOne(@Param('id') id: string) {
4  return this.provincesService.findOne(id);
5}

@ApiParam() opisuje parametry w ścieżce URL (route parameters) - te, które w trasie zapisujesz z dwukropkiem, jak :id, i które są częścią adresu. W /provinces/7 siódemka jest parametrem ścieżki.

@ApiQuery() opisuje parametry zapytania - te po znaku ? w URL. W /provinces?region=Gallia&page=2 parametrami zapytania są region i page. Nie wykonuje żadnego zapytania do bazy danych, nie definiuje schematu GraphQL i nie tworzy query buildera dla ORM-a - słowo „query" odnosi się tu wyłącznie do części adresu.

Pozostałe miejsca mają własne dekoratory: nagłówki HTTP opisuje @ApiHeader, a ciało żądania (request body) - @ApiBody. Cztery różne miejsca, cztery różne dekoratory; pomyłka nie da błędu, tylko dokumentację opisującą coś, czego tam nie ma.

Budowa @ApiQuery

Parametry zapytania bywają opcjonalne i właśnie to trzeba w dokumentacji zaznaczyć:

1@ApiQuery({
2  name: 'province',
3  required: false,
4})
5@Get('search')
6search(@Query('province') province?: string) {
7  return this.provincesService.search(province);
8}

Kolejność zapisu jest stała: @ApiQuery({ otwiera dekorator, name: 'province', podaje nazwę parametru - to jedyne pole obowiązkowe - required: false oznacza go jako opcjonalny, a }) domyka.

required domyślnie ma wartość true, więc opcjonalność trzeba zaznaczyć jawnie. Pominięcie tego jest drobiazgiem, który kosztuje cudzy czas: w Swagger UI parametr pokaże się jako wymagany, a ktoś zbuduje klienta wysyłającego region przy każdym wywołaniu.

Kolejność na endpointcie

Gdy zbierzemy wszystko razem, dekoratory układają się od najogólniejszego do najbardziej szczegółowego:

1@ApiTags('Prowincje')
2@Controller('provinces')
3export class ProvincesController {
4  @ApiOperation({ summary: 'Szczegóły prowincji' })
5  @ApiResponse({ status: 200, description: 'Prowincja odnaleziona' })
6  @ApiResponse({ status: 404, description: 'Nie ma takiej prowincji' })
7  @ApiParam({ name: 'id', description: 'Identyfikator prowincji' })
8  @ApiBearerAuth('JWT-auth')
9  @Get(':id')
10  findOne(@Param('id') id: string) {
11    return this.provincesService.findOne(id);
12  }
13}

Czyta się to z góry na dół: @ApiTags('Nazwa') na klasie kontrolera obejmuje wszystkie metody, potem nad konkretną metodą @ApiOperation({ summary }) mówi, co ona robi, następnie @ApiResponse({ status }) wylicza możliwe odpowiedzi, a na końcu wykonuje się ciało metody.

@ApiResponse() przyjmuje kod statusu i opis; można go podać wiele razy, po jednym na każdą możliwą odpowiedź. W następnych lekcjach zastąpimy go krótszymi odpowiednikami w rodzaju @ApiOkResponse.

@ApiBearerAuth('JWT-auth') oznacza endpoint jako chroniony - w Swagger UI pojawi się przy nim pole na token, a podana nazwa musi zgadzać się z konfiguracją addBearerAuth.

Podsumowanie

Dekret bez pieczęci to kartka:

  • dekoratory Swagger tylko opisują - nie zmieniają zachowania endpointu,
  • @ApiTags() grupuje endpointy i stoi nad klasą kontrolera,
  • @ApiOperation() przyjmuje summary i description - nie url/method, nie host/port, nie controller/service,
  • @ApiParam() opisuje parametry w ścieżce URL (te z dwukropkiem, jak :id),
  • @ApiQuery() opisuje parametry zapytania po znaku ? - nie odpytuje bazy, nie definiuje GraphQL-a, nie buduje zapytań ORM,
  • nagłówki opisuje @ApiHeader, ciało żądania - @ApiBody,
  • budowa @ApiQuery: @ApiQuery({ → name: 'province', → required: false → }); bez required: false parametr uchodzi za wymagany,
  • kolejność: @ApiTags('Nazwa') na klasie → @ApiOperation({ summary }) nad metodą → @ApiResponse({ status }) nad metodą → ciało metody,
  • @ApiBearerAuth('JWT-auth') oznacza endpoint chroniony tokenem.

W następnej lekcji zejdziemy poziom niżej - do dokumentowania samych DTO, czyli tego, co w tych żądaniach i odpowiedziach naprawdę siedzi. A na razie zapamiętaj: dokumentacja opisuje to, co napisałeś w dekoratorze, nie to, co robi kod. Rozjazd między nimi jest gorszy niż brak dokumentacji, bo w ten pierwszy ktoś uwierzy.

Kod do tej lekcji: src/province.controller.ts
1// Dekoratory Swagger na kontrolerze prowincji
2import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common';
3import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
4
5// TODO: Dodaj dekorator @ApiTags('Provinciae')
6@Controller('provinciae')
7export class ProvinceController {
8
9  // TODO: Dodaj @ApiOperation z summary i description
10  // TODO: Dodaj @ApiResponse dla statusu 200
11  @Get()
12  findAll() {
13    return [
14      { name: 'Pannonia', governor: 'Lucius Verus' },
15      { name: 'Britannia', governor: 'Agricola' },
16      { name: 'Africa', governor: 'Septimius' },
17    ];
18  }
19
20  // TODO: Dodaj @ApiOperation z summary
21  // TODO: Dodaj @ApiParam opisujacy parametr 'id'
22  // TODO: Dodaj @ApiResponse dla 200 i 404
23  @Get(':id')
24  findOne(@Param('id') id: string) {
25    return { name: 'Pannonia', governor: 'Lucius Verus', legions: 3 };
26  }
27
28  // TODO: Dodaj @ApiOperation z summary
29  // TODO: Dodaj @ApiQuery dla 'region' (opcjonalne)
30  // TODO: Dodaj @ApiQuery dla 'minLegions' (opcjonalne, typ Number)
31  @Get('search')
32  search(
33    @Query('region') region?: string,
34    @Query('minLegions') minLegions?: number,
35  ) {
36    return [{ name: 'Pannonia', region: 'Europa', legions: 3 }];
37  }
38
39  // TODO: Dodaj @ApiOperation z summary
40  // TODO: Dodaj @ApiResponse dla 201
41  @Post()
42  create(@Body() dto: any) {
43    return { id: '1', ...dto };
44  }
45}
46
47console.log("Dekoratory kontrolerow Swagger:");
48console.log("@ApiTags('nazwa') - grupuje endpointy");
49console.log("@ApiOperation({ summary, description })");
50console.log("@ApiResponse({ status, description })");
51console.log("@ApiParam({ name, description, example })");
52console.log("@ApiQuery({ name, required, description })");
53

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. Jakie główne właściwości przyjmuje dekorator @ApiOperation()?

  2. 2. Kiedy używamy dekoratora @ApiParam() w kontrolerze NestJS?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Dodaj @ApiQuery do endpointu wyszukiwania i @ApiParam do endpointu z parametrem :id

  • Układanie w pionie

    Uporządkuj dekoratory Swagger na endpointcie od dekoratora klasy do ciała metody:

  • Klikanie w kolejności

    Ułóż elementy dekoratora @ApiQuery w prawidłowej kolejności:

  • Edytor kodu

    Dodaj @ApiTags, @ApiOperation, @ApiResponse, @ApiParam i @ApiQuery do kontrolera prowincji

Przydatne artykuły