Chyba każdemu programiście, prędzej czy później zdarzy się, że będzie musiał wprowadzić zmiany w swoich endpoint-ach REST API. Zmiany te okażą się łamać kompatybilność wsteczną, a logika używana jest przez zewnętrznych klientów. Co wtedy? Wtedy v2.
Po co to całe wersjonowanie REST API?
Mogłoby się wydawać, że problem jest błachy, ale czy na pewno? Wyobraźmy sobie sytuację, że ukończyliśmy nasz nowy system, który udostępnia piękne REST API. Po jakimś czasie dostajemy zlecenie nowych funkcjonalności, które bezpośrednio ingerują w już istniejące rzeczy. Przykładowo zmienia nam się endpoint, który służył do wyliczania rocznego przychodu, bo zaczyna on uwzględniać dodatkowe parametry na wejściu, zwraca inny typ danych, czy po prostu zmienił się sposób wyliczania.
Oczywiście, możemy if-ować, że jeżeli wartość x została ustawiona, to licz w sposób A, inaczej w sposób B. Nie oszukujmy się jednak, takie rozwiązanie zapędzi nas w kozi róg. Inne potencjalne wyjście, to zrobienie nowego endpoint-u, z innym URL i zalecenie, aby w nowych implementacjach z niego korzystać. To, jak się pewnie domyślasz także jest kiepskim wyjściem, bo łamiemy semantykę REST, nie ma pojedynczego źródła prawdy, a dodatkowo, co, jeśli zmieni się np. prawo podatkowe i trzeba będzie znowu powtórzyć ten zabieg?
Jedyne słuszne wyjście to wersjonowanie REST API
Zamiast przesadnie rzeźbić, możemy zastosować podejście słuszne, czyli wersjonowanie REST API. Polega ono na implementacji nowych endpoint-ów przy zachowaniu kompatybilności wstecznej, gdyż stare nie są zmieniane. Ważne jest jednak dodanie do zapytania informacji o wersji API, z której chcemy skorzystać, a następnie odpowiednio je obsłużyć po stronie serwera – kierując je do odpowiedniego kontrolera / metody handle’ującej.
Informację o wersji zwyczajowo umieszczamy w jednym z trzech miejsc zapytania:
- W samym adresie url, jako przedrostek do właściwych endpoint-ów: https://api.orbisbit.com/v2/incomes. Możemy również desygnować subdomenę w różnych wariacjach: https://api.v2.orbisbit.com/incomes lub https://apiv2.orbisbit.com/incomes. Inne wyjście to parametr zapytania https://api.orbisbit.com/incomes?ver=2.
- Jako wartość nagłówka, która zostanie przekazana wraz z request-em. Przykładowo API-Version: 2.
- W specyfikacji media type nagłówka Accept. Zwyczajowo w REST API dodajemy nagłówek Accept, który zawiera informację o tym, w jakiej postaci chcemy otrzymać odpowiedź: Accept: application/json. Możemy skorzystać z niego i w wartości, dodatkowo określić wersję REST API, z której chcemy skorzystać: Accept: application/json;v=2.
Która opcja jest najlepsza?
Opcji wersjonowania REST API mamy 5 (liczę wszystkie możliwości). Nasuwa się więc pytanie, która z nich będzie najlepsza? Z mojego doświadczenia wynika, że wersja z własnym nagłówkiem jest łatwiejsza dla klientów i czystsza, gdyż nie zaśmieca właściwego adresu URL. Z tego samego też powodu jest mało odporna na błędy, bo nie widać explicite z jakiej wersji korzystamy. Więc obie opcje mają swoje wady i zalety.
Na pewno nie polecam wersjonowania po wartości query param, gdyż nie jest to ani czytelne, ani wygodne. W kwestii poddomeny, moim skromnym zdaniem jest to przerost formy nad treścią, ale nigdy nie spotkałem się z sytuacją, kiedy takie rozwiązanie byłoby optymalne. Spośród nagłówków, najlepsza semantycznie wydaje się opcja z dopisaniem wersji do wartości Accept. Może ona być jednak ciut trudniejsza w zaimplementowaniu, jednak nie musimy dodawać obsługi nowego nagłówka.
Na stole zostają więc takie opcje:
- v2 w url, poprzedzające część ścieżki,
- własny nagłówek,
- dodanie informacji w wartości nagłówka Accept.
Wybierz tę, która pasuje w Twoim projekcie. Teorię mamy za sobą, teraz, jak to zaimplementować?
Implementacja
Implementacja mechanizmu wybierającego wersję zależy od zastosowanej technologii i framework-a. Możemy wersję określić już na etapie routing-u i jawnie wskazać dla każdego endpoint-u jego wersję:
export class HelloController {
@Get('v1/hello')
helloV1(): string {
return 'hello world from v1';
}
@Get('v2/hello')
helloV2(): string {
return 'hello world from v2';
}
}
Jeżeli chcemy zastosować wersjonowanie REST API w nagłówku, bądź wynieść je gdzieś wyżej z adresu, to musimy to zrobić korzystając z narzędzi używanej technologii – czego tutaj niestety nie pokażę. Zademonstruję jednak sposób w jaki mi, najczęściej zdarza się wskazywać, które fragmenty kodu odpowiadają za konkretną wersję.
Bardzo lubię dedykowane klasy kontrolujące pod wersję, a do wskazania, która ma być obsługiwana stosuję dekorator nad klasą.
@Controller({
version: '1',
})
export class HelloControllerV1 {
@Get('hello')
hello(): string {
return 'hello world from v1';
}
}
@Controller({
version: '2',
})
export class HelloControllerV2 {
@Get('hello')
hello(): string {
return 'hello world from v2';
}
}
Dobrym wyjściem jest także tworzenie osobnej struktury plików i katalogów dla nowych wersji. Przykładowo, struktura folderu rest-api, w naszym module, może wyglądać następująco:
- api-rest
- v1
- controllers
- requests
- v2
- controllers
- requests
- v1
Podsumowanie
- Jeżeli musimy wprowadzić zmiany w swoich endpoint-ach REST API, które łamią kompatybilność wsteczną, to powinniśmy zastosować wersjonowanie.
- Należy wymagać od klienta podania informacji o wersji w przesyłanym zapytaniu.
- Zaleca się umieszczać wartość wersji w: adresie url, własnym nagłówku, w wartości nagłówka Accept.
Dodaj komentarz