Kurs NestJS · Moduł 11: Swagger i OpenAPI

Zaawansowane odpowiedzi - edykty cesarskie

6 min czytania
W tej lekcji7

Edykt cesarski nie kończył się na rozkazie. Zawsze wymieniał następstwa: co się stanie, gdy polecenie wykonano, co gdy petent nie ma prawa prosić, a co gdy sprawa w ogóle nie istnieje w annałach. Dopiero taki dokument dawał się stosować bez pytania kancelarii o zdanie.

Twoje API działa tak samo. W poprzedniej lekcji dokumentowaliśmy odpowiedź ogólnym @ApiResponse, podając kod statusu ręcznie. NestJS ma jednak osobny dekorator dla każdego kodu - i o nich jest ta lekcja.

Dekorator opisuje, nie ustawia

Zacznijmy od rzeczy, która bywa źródłem nieporozumień. Te dekoratory nie zmieniają zachowania aplikacji. Trafiają wyłącznie do dokumentacji:

1@ApiResponse({ status: 200, description: 'Lista legionów' })
2@Get()
3findAll() {}
4
5@ApiOkResponse({ description: 'Lista legionów' })
6@Get()
7findAll() {}

Oba zapisy dają w Swagger UI dokładnie to samo. Drugi jest krótszy i trudniej się w nim pomylić, bo kodu statusu nie podajesz - siedzi on w nazwie dekoratora.

O tym, jaki kod naprawdę wróci do klienta, decyduje co innego: @Post domyślnie zwraca 201, pozostałe metody 200, a @HttpCode(204) nadpisuje to jawnie. Dopisanie @ApiCreatedResponse nad metodą @Get nie sprawi, że zacznie ona zwracać 201 - sprawi tylko, że dokumentacja skłamie.

Kod w nazwie dekoratora

Reguła jest prosta: nazwa dekoratora to oficjalna nazwa kodu HTTP. Stąd cały zestaw daje się odtworzyć z pamięci:

1import {
2  ApiOkResponse,                    // 200 OK
3  ApiCreatedResponse,               // 201 Created
4  ApiBadRequestResponse,            // 400 Bad Request
5  ApiUnauthorizedResponse,          // 401 Unauthorized
6  ApiForbiddenResponse,             // 403 Forbidden
7  ApiNotFoundResponse,              // 404 Not Found
8  ApiInternalServerErrorResponse,   // 500 Internal Server Error
9} from '@nestjs/swagger';

@ApiCreatedResponse() odpowiada kodowi 201 (Created), a @ApiNotFoundResponse() reprezentuje kod 404 Not Found - nie 400, nie 401 i nie 403, bo każdy z nich ma własny dekorator z listy powyżej.

Reguła działa też w drugą stronę i pozwala odrzucić nazwy, których nie ma. @ApiNewResponse() ani @ApiSuccessResponse() nie istnieją - brzmią sensownie, ale w protokole HTTP nie ma kodu o nazwie „New" ani „Success". Jeśli nie umiesz wskazać kodu, do którego dekorator się odnosi, to znak, że taki dekorator nie istnieje.

Trzy klasy kodów

Kody układają się w klasy, a klasa mówi, kto zawinił:

  • 2xx - powodzenie. 200 OK to „zrobione", 201 Created - „zrobione i powstał nowy zasób".
  • 4xx - wina klienta. Żądanie było wadliwe albo niedozwolone; powtórzenie go bez zmian nic nie da.
  • 5xx - wina serwera. 500 Internal Server Error oznacza, że aplikacja się wywróciła. Żądanie mogło być całkiem poprawne.

Uporządkowane od powodzenia do błędu serwera dają kolejność: 200 OK → 201 Created → 401 Unauthorized → 500 Internal Server Error.

W obrębie 4xx warto zapamiętać jedną parę. 401 Unauthorized znaczy „nie wiem, kim jesteś" - brakuje tokenu albo jest nieważny. 403 Forbidden znaczy „wiem, kim jesteś, i nie wolno ci" - token jest w porządku, ale rola nie wystarcza. Pierwsze naprawia się logowaniem, drugiego nie naprawi się wcale.

Budowa dekoratora

Dekorator przyjmuje obiekt, a kolejność jego pól jest umowna, choć w praktyce zawsze ta sama - najpierw opis, potem typ:

1@ApiOkResponse({
2  description: 'Lista legionów',
3  type: LegionResponseDto,
4})
5@Get()
6findAll(): LegionResponseDto[] {
7  return this.legionService.findAll();
8}

Czyta się to w czterech krokach: @ApiOkResponse({ otwiera dekorator, description: 'Lista legionów', wyjaśnia człowiekowi, co dostanie, type: LegionResponseDto wskazuje klasę DTO, a }) domyka.

description trafia do opisu odpowiedzi w Swagger UI. type robi więcej: Swagger sięga po dekoratory @ApiProperty z tej klasy - te z poprzedniej lekcji - i buduje z nich pełny schemat odpowiedzi wraz z przykładowymi wartościami. Bez type czytelnik dowie się, że dostanie 200, ale nie dowie się, co w środku.

Odpowiedź, która jest tablicą

Gdy endpoint zwraca listę, masz dwa równoważne zapisy i oba działają poprawnie:

1@ApiOkResponse({ type: [LegionResponseDto] })
2@Get()
3findAll() {}
4
5@ApiOkResponse({ type: LegionResponseDto, isArray: true })
6@Get()
7findAllAgain() {}

Pierwszy - klasa w nawiasach kwadratowych - jest krótszy. Drugi, z jawnym isArray: true, bywa czytelniejszy, gdy obok stoją inne opcje. Wybór jest kwestią stylu; Swagger wygeneruje identyczny schemat.

Nie zadziała natomiast type: Array<LegionResponseDto>. Powód jest głębszy niż składnia: typy generyczne znikają przy kompilacji TypeScriptu. Dekorator dostaje wartość istniejącą w czasie działania programu, a z Array<LegionResponseDto> zostaje po kompilacji samo Array - bez śladu po tym, czego jest tablicą. Nawiasy kwadratowe i isArray działają właśnie dlatego, że przekazują klasę LegionResponseDto jako zwykłą wartość.

Komplet na jednym endpoincie

W praktyce endpoint dokumentuje wszystkie odpowiedzi, jakich klient może się spodziewać:

1@ApiOperation({ summary: 'Awansuj legionistę' })
2@ApiOkResponse({
3  description: 'Legionista został awansowany',
4  type: LegionaryResponseDto,
5})
6@ApiBadRequestResponse({ description: 'Nieprawidłowa ranga docelowa' })
7@ApiUnauthorizedResponse({ description: 'Brak ważnego tokenu JWT' })
8@ApiNotFoundResponse({ description: 'Legionista nie znaleziony' })
9@ApiBearerAuth('JWT-auth')
10@UseGuards(JwtAuthGuard)
11@Patch(':id/promote')
12promote(@Param('id') id: string, @Body() dto: PromoteLegionaryDto) {
13  return this.legionService.promote(id, dto);
14}

Zwróć uwagę na parę @ApiBearerAuth('JWT-auth') i @ApiUnauthorizedResponse. Pierwszy zaznacza endpoint jako chroniony i włącza w Swagger UI pole na token - nazwa 'JWT-auth' musi zgadzać się z tą podaną przy konfiguracji addBearerAuth. Drugi opisuje, co się stanie bez tokenu. Chroniony endpoint bez @ApiUnauthorizedResponse obiecuje czytelnikowi, że 401 nigdy nie nastąpi - a nastąpi przy pierwszej próbie.

Podsumowanie

Edykt wymienia następstwa:

  • dekoratory odpowiedzi tylko dokumentują; kod statusu ustala @Post, @Get albo @HttpCode,
  • nazwa dekoratora to nazwa kodu: @ApiOkResponse (200), @ApiCreatedResponse (201), @ApiBadRequestResponse (400), @ApiUnauthorizedResponse (401), @ApiForbiddenResponse (403), @ApiNotFoundResponse (404), @ApiInternalServerErrorResponse (500),
  • @ApiNewResponse i @ApiSuccessResponse nie istnieją - nie ma kodów HTTP o takich nazwach,
  • klasy kodów: 2xx powodzenie, 4xx wina klienta, 5xx wina serwera; od sukcesu do błędu serwera: 200 OK → 201 Created → 401 Unauthorized → 500 Internal Server Error,
  • 401 to „nie wiem, kim jesteś", 403 to „wiem i nie wolno ci",
  • budowa dekoratora: @ApiOkResponse({ → description: 'Lista legionów', → type: LegionResponseDto → }),
  • type wiąże odpowiedź z DTO, więc Swagger zbuduje schemat z jego @ApiProperty,
  • tablicę zapisujesz na dwa równoważne sposoby: type: [LegionDto] albo type: LegionDto, isArray: true - oba działają poprawnie,
  • type: Array<LegionDto> nie zadziała, bo typy generyczne znikają przy kompilacji,
  • endpoint chroniony oznaczasz parą @ApiBearerAuth('JWT-auth') i @ApiUnauthorizedResponse.

W następnej lekcji zajmiemy się wyglądem samego Forum Annałów - konfiguracją Swagger UI. A na razie zapamiętaj: dokumentacja odpowiedzi nie jest ozdobnikiem. To jedyne miejsce, w którym klient dowie się, na co ma się przygotować, zanim jego kod się o tym przekona.

Kod do tej lekcji: src/tributum.controller.ts
1// Zaawansowane dekoratory odpowiedzi
2import { Controller, Get, Post, Param, Body, Delete, UseGuards } from '@nestjs/common';
3import {
4  ApiTags, ApiOperation, ApiBearerAuth,
5  ApiOkResponse, ApiCreatedResponse,
6  ApiNotFoundResponse, ApiBadRequestResponse,
7  ApiUnauthorizedResponse,
8} from '@nestjs/swagger';
9
10@ApiTags('Tributum')
11@Controller('tributum')
12export class TributumController {
13
14  // TODO: Dodaj @ApiOperation z summary
15  // TODO: Dodaj @ApiOkResponse z description
16  @Get()
17  findAll() {
18    return [
19      { id: '1', province: 'Pannonia', amount: 50000, status: 'collected' },
20      { id: '2', province: 'Britannia', amount: 30000, status: 'pending' },
21    ];
22  }
23
24  // TODO: Dodaj @ApiOperation z summary
25  // TODO: Dodaj @ApiCreatedResponse z description
26  // TODO: Dodaj @ApiBadRequestResponse z description
27  @Post()
28  create(@Body() dto: any) {
29    return { id: '3', ...dto, status: 'pending' };
30  }
31
32  // TODO: Dodaj @ApiOperation z summary
33  // TODO: Dodaj @ApiOkResponse z description
34  // TODO: Dodaj @ApiNotFoundResponse z description
35  @Get(':id')
36  findOne(@Param('id') id: string) {
37    return { id, province: 'Pannonia', amount: 50000 };
38  }
39
40  // TODO: Dodaj @ApiBearerAuth('JWT-auth')
41  // TODO: Dodaj @ApiOperation z summary
42  // TODO: Dodaj @ApiOkResponse z description
43  // TODO: Dodaj @ApiNotFoundResponse z description
44  // TODO: Dodaj @ApiUnauthorizedResponse z description
45  @Delete(':id')
46  remove(@Param('id') id: string) {
47    return { message: 'Trybut anulowany', id };
48  }
49}
50
51console.log("Specjalizowane dekoratory odpowiedzi:");
52console.log("@ApiOkResponse - 200 OK");
53console.log("@ApiCreatedResponse - 201 Created");
54console.log("@ApiNotFoundResponse - 404 Not Found");
55console.log("@ApiBadRequestResponse - 400 Bad Request");
56console.log("@ApiUnauthorizedResponse - 401 Unauthorized");
57

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. Który dekorator Swagger odpowiada kodowi statusu HTTP 201 (Created)?

  2. 2. Jaki kod statusu HTTP reprezentuje dekorator @ApiNotFoundResponse()?

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

Zadania praktyczne w grze

  • Edytor kodu

    Dodaj @ApiOkResponse, @ApiCreatedResponse, @ApiNotFoundResponse i @ApiBadRequestResponse do endpointów

  • Układanie w pionie

    Uporządkuj kody statusów HTTP od sukcesu do błędu serwera:

  • Klikanie w kolejności

    Ułóż elementy dekoratora @ApiOkResponse z typem DTO:

  • Edytor kodu

    Oznacz endpointy jako chronione JWT za pomocą @ApiBearerAuth('JWT-auth') i @ApiUnauthorizedResponse

Przydatne artykuły