Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Wersjonowanie API - ery Imperium

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:

  1. app.enableVersioning()
    - najpierw aplikacja musi w ogóle rozróżniać wersje.
  2. DocumentBuilder
    dla każdej wersji
    - osobna konfiguracja z własnym tytułem i numerem.
  3. SwaggerModule.createDocument
    z opcją
    include
    - zbudowanie dokumentu wyłącznie z wybranych modułów.
  4. 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

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, @name:

  • trzy strategie: URI (wersja w ścieżce), Header (w nagłówku, np.
    X-Version
    ), Media Type (w
    Content-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'
    });
    ,
  • defaultVersion
    chroni kontrolery, które wersji nie podają,
  • kontroler wersjonowany:
    @Controller({
    path: 'legiones', version: '1' })
    export class LegionV1Controller {}
    ,
  • osobna dokumentacja w czterech krokach:
    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
    ,
  • opcja
    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.

Przejdź do CodeWorlds