Kurs NestJS · Moduł 11: Swagger i OpenAPI
Zaawansowane odpowiedzi - edykty cesarskie
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 OKto „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 Erroroznacza, ż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,@Getalbo@HttpCode, - nazwa dekoratora to nazwa kodu:
@ApiOkResponse(200),@ApiCreatedResponse(201),@ApiBadRequestResponse(400),@ApiUnauthorizedResponse(401),@ApiForbiddenResponse(403),@ApiNotFoundResponse(404),@ApiInternalServerErrorResponse(500), @ApiNewResponsei@ApiSuccessResponsenie 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→}), typewiąże odpowiedź z DTO, więc Swagger zbuduje schemat z jego@ApiProperty,- tablicę zapisujesz na dwa równoważne sposoby:
type: [LegionDto]albotype: 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");
57Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Który dekorator Swagger odpowiada kodowi statusu HTTP 201 (Created)?
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