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.
NestJS zna trzy sposoby wskazania wersji:
/v1/legiones. Najczęściej używana, bo widać ją w logach, w przeglądarce i w zakładkach.X-Version: 1. Adres pozostaje czysty, ale wersji nie widać bez podglądu żądania.Content-Type. Najbardziej zgodna z duchem HTTP i najrzadziej spotykana w praktyce.Przy URI Versioning z wersją pierwszą adres wygląda tak:
. Nie /v1/legiones
/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.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:
otwiera konfigurację, app.enableVersioning({
wybiera strategię, type: VersioningType.URI,
ustala wersję dla kontrolerów, które jej nie podają, a defaultVersion: '1'
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 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:
otwiera obiekt konfiguracji, @Controller({
podaje ścieżkę i wersję, a path: 'legiones', version: '1' })
deklaruje samą klasę.export class LegionV1Controller {}
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.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.DocumentBuilder dla każdej wersji - osobna konfiguracja z własnym tytułem i numerem.SwaggerModule.createDocument z opcją include - zbudowanie dokumentu wyłącznie z wybranych modułów.SwaggerModule.setup dla 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
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 include
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', ...).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ą
w deprecated: true
@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.
Prawo Republiki obowiązuje, dopóki są sprawy wszczęte za Republiki, @name:
X-Version), Media Type (w Content-Type),/v1/legiones - nie /legiones?version=1, nie /legiones/v1,main.ts: app.enableVersioning({ → type: VersioningType.URI, → defaultVersion: '1' → });,defaultVersion chroni kontrolery, które wersji nie podają,@Controller({ → path: 'legiones', version: '1' }) → export class LegionV1Controller {},app.enableVersioning() → DocumentBuilder dla każdej wersji → createDocument z include → SwaggerModule.setup dla każdej wersji,DocumentBuilder: setTitle, setDescription, setVersion, setContact, setLicense, addServer, addTag, addBearerAuth,include uwzględnia tylko wybrane moduły - nie dodaje CSS, nie importuje zewnętrznych specyfikacji, nie włącza dekoratorów,deprecated: true w @ApiOperation oznacza endpoint jako wycofany - nie removed, nie obsolete, nie disabled; 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.