Kurs NestJS · Moduł 11: Swagger i OpenAPI
Wersjonowanie API - ery Imperium
W tej lekcji6
Zmiana w API, z którego korzysta kilku klientów, ma jedną nieprzyjemną właściwość: nie da się jej wycofać z cudzych aplikacji. Usuwasz pole, którego nikt - jak sądzisz - nie używa, i tego samego popołudnia dowiadujesz się, że używał go integrator z drugiego końca kraju.
Rzym radził sobie z tym przez ery. Prawo Republiki nie znikało wraz z nastaniem Cesarstwa - obowiązywało dalej dla spraw wszczętych wcześniej, a nowe reguły szły obok. Wersjonowanie API jest tym samym: stara wersja żyje, dopóki ktoś z niej korzysta, a zmiany trafiają do nowej.
Trzy strategie
NestJS zna trzy sposoby wskazania wersji:
- URI Versioning - wersja w ścieżce adresu, jak
/v1/legiones. Najczęściej używana, bo widać ją w logach, w przeglądarce i w zakładkach. - Header Versioning - wersja w nagłówku HTTP, na przykład
X-Version: 1. Adres pozostaje czysty, ale wersji nie widać bez podglądu żądania. - Media Type Versioning - wersja w nagłówku
Content-Type. Najbardziej zgodna z duchem HTTP i najrzadziej spotykana w praktyce.
Przy URI Versioning z wersją pierwszą adres wygląda tak: /v1/legiones. Nie /legiones?version=1 - to byłby parametr zapytania, nie wersja. Nie /legiones/v1 - tu v1 wyglądałoby jak identyfikator zasobu. I nie /legiones z nagłówkiem X-Version - to już inna strategia.
Włączenie wersjonowania
Wersjonowanie włącza się raz, w main.ts:
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.enableVersioning({
5 type: VersioningType.URI,
6 defaultVersion: '1',
7 });
8
9 await app.listen(3000);
10}Zapis składa się z czterech części w stałej kolejności: app.enableVersioning({ otwiera konfigurację, type: VersioningType.URI, wybiera strategię, defaultVersion: '1' ustala wersję dla kontrolerów, które jej nie podają, a }); domyka.
defaultVersion warto ustawić od razu. Bez niego każdy kontroler bez jawnej wersji przestanie odpowiadać - a przy włączaniu wersjonowania w istniejącym projekcie to zwykle wszystkie kontrolery naraz.
Kontroler w wersji
Kontroler deklaruje swoją wersję w tym samym dekoratorze, w którym podaje ścieżkę:
1@Controller({
2 path: 'legiones', version: '1' })
3export class LegionV1Controller {}Kolejność zapisu jest stała: @Controller({ otwiera obiekt konfiguracji, path: 'legiones', version: '1' }) podaje ścieżkę i wersję, a export class LegionV1Controller {} deklaruje samą klasę.
Zwróć uwagę, że @Controller('legiones') z napisem zamienia się tu w @Controller({ path, version }) z obiektem - to ta sama funkcja, tylko w wariancie przyjmującym więcej opcji. Pojedyncze metody można wersjonować osobno dekoratorem @Version('2'), gdy tylko jedna z nich się zmieniła.
Nazwa klasy - LegionV1Controller - nie ma znaczenia dla routingu; wersję ustala wyłącznie pole version. Warto ją jednak nazwać zgodnie z wersją, bo w projekcie z dwiema erami dwie klasy LegionController w różnych plikach szybko stają się źródłem pomyłek.
Osobna dokumentacja dla każdej wersji
Jeden dokument Swagger z pomieszanymi wersjami jest mniej użyteczny niż dwa osobne. Proces ma cztery kroki, w tej kolejności:
app.enableVersioning()- najpierw aplikacja musi w ogóle rozróżniać wersje.DocumentBuilderdla każdej wersji - osobna konfiguracja z własnym tytułem i numerem.SwaggerModule.createDocumentz opcjąinclude- zbudowanie dokumentu wyłącznie z wybranych modułów.SwaggerModule.setupdla każdej wersji - wystawienie każdego dokumentu pod własnym adresem.
1const configV1 = new DocumentBuilder()
2 .setTitle('Legion API v1')
3 .setDescription('Pierwsza era Imperium')
4 .setVersion('1.0')
5 .setContact('Kancelaria', 'https://imperium.rome', 'kancelaria@imperium.rome')
6 .setLicense('MIT', 'https://opensource.org/licenses/MIT')
7 .addServer('https://api.imperium.rome')
8 .addTag('Legiony')
9 .addBearerAuth(undefined, 'JWT-auth')
10 .build();
11
12const documentV1 = SwaggerModule.createDocument(app, configV1, {
13 include: [LegionV1Module],
14});
15
16SwaggerModule.setup('api/v1', app, documentV1);DocumentBuilder buduje się łańcuchem metod, z których każda dokłada jeden element opisu: setTitle, setDescription i setVersion to podstawa, setContact wskazuje, do kogo pisać, setLicense podaje licencję, addServer - adres, pod którym API naprawdę działa, addTag deklaruje grupę endpointów, a addBearerAuth włącza pole na token.
Opcja include uwzględnia w dokumentacji tylko wybrane moduły. Nie dodaje plików CSS do Swagger UI, nie importuje zewnętrznych specyfikacji OpenAPI i nie włącza dodatkowych dekoratorów. To ona sprawia, że dokument v1 zawiera wyłącznie endpointy pierwszej ery - bez niej oba dokumenty byłyby identyczne i pokazywałyby całe API.
Na końcu SwaggerModule.setup('api/v1', ...) wystawia dokument pod adresem /api/v1. Drugą wersję budujesz analogicznie: configV2 z własnym tytułem i numerem, include: [LegionV2Module], a na końcu setup('api/v2', ...).
Wycofywanie endpointów
Wersja druga nie unieważnia pierwszej z dnia na dzień. Endpointy, które mają zniknąć, oznacza się najpierw jako wycofane:
1@ApiOperation({
2 summary: 'Lista legionów (stara wersja)',
3 deprecated: true,
4})
5@Get()
6findAllLegacy() {
7 return this.legionService.findAll();
8}Endpoint oznaczasz jako wycofany właściwością deprecated: true w @ApiOperation. Nie removed, nie obsolete, nie disabled - te trzy nazwy brzmią sensownie, ale w OpenAPI nie istnieją; specyfikacja zna wyłącznie deprecated.
W Swagger UI taki endpoint zostaje przekreślony i opatrzony ostrzeżeniem, ale nadal działa. I o to chodzi: oznaczenie jest zapowiedzią, nie wyłączeniem. Daje integratorom czas na przejście, zanim endpoint zniknie naprawdę - a bez tego okresu przejściowego wersjonowanie nie ma sensu, bo zmiana i tak łamie cudze aplikacje.
Podsumowanie
Prawo Republiki obowiązuje, dopóki są sprawy wszczęte za Republiki:
- trzy strategie: URI (wersja w ścieżce), Header (w nagłówku, np.
X-Version), Media Type (wContent-Type), - przy URI Versioning adres wygląda tak:
/v1/legiones- nie/legiones?version=1, nie/legiones/v1, - włączenie w
main.ts:app.enableVersioning({→type: VersioningType.URI,→defaultVersion: '1'→});, defaultVersionchroni kontrolery, które wersji nie podają,- kontroler wersjonowany:
@Controller({→path: 'legiones', version: '1' })→export class LegionV1Controller {}, - osobna dokumentacja w czterech krokach:
app.enableVersioning()→DocumentBuilderdla każdej wersji →createDocumentzinclude→SwaggerModule.setupdla każdej wersji, DocumentBuilder:setTitle,setDescription,setVersion,setContact,setLicense,addServer,addTag,addBearerAuth,- opcja
includeuwzględnia tylko wybrane moduły - nie dodaje CSS, nie importuje zewnętrznych specyfikacji, nie włącza dekoratorów, deprecated: truew@ApiOperationoznacza endpoint jako wycofany - nieremoved, nieobsolete, niedisabled; endpoint nadal działa.
W następnej lekcji zbierzemy cały moduł w jeden projekt - kompletną dokumentację Imperium API. A na razie zapamiętaj: wersjonowanie nie służy do wprowadzania zmian. Służy do tego, żeby zmiana nie zepsuła nikomu dnia.
Kod do tej lekcji: src/versioning.ts
1// Wersjonowanie API z dokumentacja Swagger
2import { VersioningType } from '@nestjs/common';
3import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4
5// Konfiguracja wersjonowania w main.ts
6async function bootstrap() {
7 // const app = await NestFactory.create(AppModule);
8
9 // TODO: Wlacz URI versioning z defaultVersion '1'
10 // app.enableVersioning({
11 // type: VersioningType.URI,
12 // defaultVersion: '1',
13 // });
14
15 // TODO: Stworz DocumentBuilder dla wersji 1
16 const configV1 = new DocumentBuilder()
17 .setTitle('Imperium API v1')
18 .setDescription('Era Republiki')
19 .setVersion('1.0')
20 .build();
21
22 // TODO: Stworz DocumentBuilder dla wersji 2
23 // const configV2 = new DocumentBuilder()
24 // .setTitle('Imperium API v2')
25 // .setDescription('Era Cesarstwa')
26 // .setVersion('2.0')
27 // .build();
28
29 // TODO: Stworz osobne dokumenty Swagger dla kazdej wersji
30 // const docV1 = SwaggerModule.createDocument(app, configV1);
31 // SwaggerModule.setup('api/v1/docs', app, docV1);
32
33 // const docV2 = SwaggerModule.createDocument(app, configV2);
34 // SwaggerModule.setup('api/v2/docs', app, docV2);
35
36 console.log("Swagger v1: http://localhost:3000/api/v1/docs");
37 console.log("Swagger v2: http://localhost:3000/api/v2/docs");
38}
39
40bootstrap();
41
42console.log("=== Strategie wersjonowania ===");
43console.log("URI: /v1/legiones, /v2/legiones");
44console.log("Header: X-API-Version: 1");
45console.log("Media Type: Accept: application/json;v=1");
46console.log("");
47console.log("Kazda wersja moze miec osobny dokument Swagger");
48console.log("deprecated: true oznacza wycofany endpoint");
49Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak wygląda URL endpointu przy URI Versioning z wersją 1?
2. Jak oznaczyć endpoint jako wycofany (deprecated) w @ApiOperation?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj kroki tworzenia wersjonowanego API z osobnymi dokumentami Swagger:
- Edytor kodu
Stwórz DocumentBuilder dla wersji 1 i wersji 2 API z odpowiednimi ustawieniami
- Układanie w pionie
Ułóż elementy deklaracji wersjonowanego kontrolera w prawidłowej kolejności:
- Klikanie w kolejności
Ułóż elementy włączania URI Versioning w main.ts:
- Edytor kodu
Stwórz konfigurację z title, description, version, contact, license, servers, tags i bearerAuth