Projekt internetowego interfejsu API RESTful

Większość nowoczesnych aplikacji internetowych udostępnia interfejsy API, których klienci mogą używać do komunikowania się z daną aplikacją. Projektując internetowy interfejs API w przemyślany sposób, należy uwzględnić te kwestie:

  • Niezależność platformy. Wywołanie interfejsu API powinno być możliwe za pośrednictwem dowolnego klienta, niezależnie od wewnętrznej implementacji interfejsu API. Wymaga to przy użycia standardowych protokołów oraz wdrożenia mechanizmu, który umożliwia uzgodnienie formatu danych wymienianych między klientem a usługą internetową.

  • Ewolucja usługi. Należy zapewnić możliwość rozbudowy interfejsu API i dodawania do niego funkcji w sposób niezależny od aplikacji klienckich. W miarę rozwoju interfejsu API istniejące aplikacje klienckie powinny nadal działać bez żadnych modyfikacji. Wszystkie funkcje powinny być wykrywalne, aby aplikacje klienckie mogły w pełni z niego korzystać.

W tym poradniku opisano, jakie kwestie należy uwzględnić podczas projektowania internetowego interfejsu API.

Co to jest REST?

W 2000 r Roy Fielding zaproponował architekturę REST (Representational State Transfer) — nowe podejście do projektowania usług internetowych. REST to styl architektoniczny dotyczący tworzenia systemów rozproszonych opartych na hipermediach. Architektura REST jest niezależna od wszelkich podstawowych protokołów, w tym HTTP. Jednak większość typowych implementacji interfejsu API REST używa protokołu HTTP jako protokołu aplikacji, a ten przewodnik koncentruje się na projektowaniu interfejsów API REST dla protokołu HTTP.

Główną zaletą interfejsu REST za pośrednictwem protokołu HTTP jest to, że używa otwartych standardów i nie wiąże implementacji interfejsu API ani aplikacji klienckich z żadną konkretną implementacją. Na przykład usługa internetowa REST może być napisana w języku ASP.NET, a aplikacje klienckie mogą używać dowolnego języka lub zestawu narzędzi, który umożliwia generowanie żądań HTTP i analizowanie odpowiedzi HTTP.

Oto niektóre z najważniejszych zasad dotyczących projektowania interfejsów API RESTful korzystających z protokołu HTTP:

  • Interfejsy API REST są oparte na zasobach — dowolnym obiekcie, danych lub usłudze, które są dostępne dla klienta.

  • Zasób ma identyfikator URI służący do unikatowej identyfikacji tego zasobu. Oto przykładowy identyfikator URI zamówienia klienta:

    https://adventure-works.com/orders/1
    
  • Interakcja klientów z usługą odbywa się przez wymianę reprezentacji zasobów. W przypadku wielu internetowych interfejsów API formatem wymiany danych jest notacja JSON. Na przykład żądanie GET wysłane do powyższego identyfikatora URI może zwrócić tę treść odpowiedzi:

    {"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
    
  • Interfejsy API REST korzystają z ujednoliconego interfejsu, co ułatwia rozdzielenie implementacji klienta od implementacji usługi. W przypadku interfejsów API REST opartych na protokole HTTP jednolity interfejs obejmuje używanie standardowych czasowników HTTP do wykonywania operacji na zasobach. Najczęściej używane operacje to GET, POST, PUT, PATCH i DELETE.

  • Interfejsy API REST korzystają z bezstanowego modelu żądań. Żądania HTTP powinny być niezależne i mogą występować w dowolnej kolejności, dlatego zachowywanie informacji o stanie przejściowym między żądaniami nie jest możliwe. Informacje są przechowywane jedynie w zasobach, a każde żądanie powinno być niepodzielną operacją. Pozwala to uzyskać wysoki poziom skalowalności usług internetowych, ponieważ nie ma potrzeby zachowywania koligacji między klientami a określonymi serwerami. Dowolny serwer może obsługiwać każde żądanie pochodzące od dowolnego klienta. Jednak skalowalność może być ograniczana przez inne czynniki. Na przykład wiele usług internetowych zapisuje dane w magazynie danych zaplecza, co może być trudne do skalowania w poziomie. Aby uzyskać więcej informacji na temat strategii skalowania magazynu danych w poziomie, zobacz Partycjonowanie danych poziomych, pionowych i funkcjonalnych.

  • Interfejsy API REST są sterowane za pomocą hipermedialnych linków, zawartych w reprezentacji. Na przykład poniżej przedstawiono reprezentację zamówienia w postaci danych JSON. Zawiera ona linki umożliwiające pobranie lub zaktualizowanie klienta skojarzonego z zamówieniem.

    {
      "orderID":3,
      "productID":2,
      "quantity":4,
      "orderValue":16.60,
      "links": [
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" },
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" }
      ]
    }
    

W 2008 r. Leonard Richardson zaproponował poniższy model dojrzałości obejmujący internetowe interfejsy API:

  • Poziom 0: zdefiniowanie jednego identyfikatora URI używanego przez operacje, które mają postać żądań POST.
  • Poziom 1: utworzenie oddzielnych identyfikatorów URI dla poszczególnych zasobów.
  • Poziom 2: zdefiniowanie operacji na zasobach za pomocą metod HTTP.
  • Poziom 3: Korzystanie z hipermediów (architektura HATEOAS, opisana poniżej).

Trzeci poziom dojrzałości odpowiada interfejsowi API RESTful zgodnie z definicją Fieldinga. W praktyce wiele opublikowanych internetowych interfejsów API charakteryzuje się dojrzałością zbliżoną do poziomu 2.

Organizowanie projektu interfejsu API wokół zasobów

Należy skoncentrować się na jednostkach biznesowych udostępnianych przez internetowy interfejs API. Na przykład w systemie handlu elektronicznego głównymi jednostkami mogą być klienci i zamówienia. Aby utworzyć zamówienie, można wysłać żądanie HTTP POST, które zawiera informacje o tym zamówieniu. Odpowiedź HTTP zawiera informację, czy składanie zamówienia zakończyło się powodzeniem. Jeśli to możliwe, identyfikatory URI zasobów powinny zawierać rzeczowniki (zasób), a nie czasowniki (operacje na zasobie).

https://adventure-works.com/orders // Good

https://adventure-works.com/create-order // Avoid

Zasób nie musi być oparty na jednym fizycznym elemencie danych. Na przykład zasób zamówienia może być zaimplementowany wewnętrznie jako kilka tabel w relacyjnej bazie danych, ale udostępniany klientowi jako jedna całość. Należy unikać tworzenia interfejsów API, które są po prostu duplikatami wewnętrznej struktury bazy danych. Architektura REST ma służyć do modelowania jednostek i operacji, które aplikacja może wykonywać na tych jednostkach. Nie należy uwidaczniać klienta w wewnętrznej implementacji.

Jednostki są często grupowane w kolekcje (zamówienia, klienci). Zasób kolekcji jest oddzielony od elementu kolekcji i powinien mieć własny identyfikator URI. Na przykład następujący identyfikator URI może reprezentować kolekcję zamówień:

https://adventure-works.com/orders

Wysłanie żądania HTTP GET do identyfikatora URI kolekcji umożliwia pobranie listy elementów kolekcji. Poszczególne elementy kolekcji również mają własne unikatowe identyfikatory URI. Żądanie HTTP GET wysłane do identyfikatora URI elementu zwraca szczegóły tego elementu.

Należy przyjąć spójną konwencję nazewnictwa identyfikatorów URI. W przypadku identyfikatorów URI odwołujących się do kolekcji zwykle sprawdza się używanie rzeczowników w liczbie mnogiej. Dobrym rozwiązaniem jest utworzenie hierarchii identyfikatorów URI kolekcji i elementów. Na przykład /customers jest ścieżką do kolekcji klientów, a /customers/5 — ścieżką do klienta o identyfikatorze równym 5. Takie podejście pomaga utrzymać intuicyjność internetowego interfejsu API. Ponadto wiele struktur internetowych interfejsów API umożliwia kierowanie żądań na podstawie sparametryzowanych ścieżek identyfikatorów URI, co pozwala zdefiniować trasę dla ścieżki /customers/{id}.

Należy również uwzględnić relacje między różnymi typami zasobów i zastanowić się, jak można uwidocznić te skojarzenia. Na przykład ścieżka/customers/5/orders może reprezentować wszystkie zamówienia klienta nr 5. Można również przyjąć inne podejście: identyfikator URI, taki jak /orders/99/customer, może reprezentować skojarzenie zamówienia z klientem. Jednak zaawansowana implementacja tego modelu może okazać się skomplikowana. Lepszym rozwiązaniem jest udostępnienie w treści komunikatu odpowiedzi HTTP linków umożliwiających przejście do skojarzonych zasobów. Ten mechanizm został opisany bardziej szczegółowo w sekcji Używanie funkcji HATEOAS w celu umożliwienia nawigacji do powiązanych zasobów.

W przypadku bardziej złożonych systemów korzystne może wydawać się udostępnianie identyfikatorów URI, takich jak /customers/1/orders/99/products, które umożliwiają klientom przechodzenie przez kilka poziomów relacji. Jednak utrzymanie takiego poziomu złożoności może okazać się trudne, a ponadto jest on nieelastyczny w perspektywie przyszłych zmian relacji między zasobami. Zamiast tego należy starać się utrzymać stosunkowo proste identyfikatory URI. Odwołanie do zasobu, które zostanie udostępnione aplikacji, powinno umożliwiać znalezienie elementów powiązanych z tym zasobem. Poprzednie zapytanie można zastąpić kolejno identyfikatorem URI /customers/1/orders, który pozwala znaleźć wszystkie zamówienia klienta 1, oraz identyfikatorem URI /orders/99/products, który pozwala znaleźć produkty w tym zamówieniu.

Napiwek

Należy unikać używania identyfikatorów URI zasobów w postaci bardziej skomplikowanej niż kolekcja/element/kolekcja.

Kolejna kwestia jest związana z obciążeniem serwera internetowego przez wszystkie żądania. Im więcej żądań, tym większe obciążenie. W związku z tym należy unikać rozbudowanych internetowych interfejsów API, które udostępniają wiele małych zasobów. Takie interfejsy mogą wymagać od aplikacji klienckiej wysyłania wielu żądań w celu znalezienia wszystkich potrzebnych danych. Zamiast tego warto zdenormalizować dane i połączyć powiązane informacje w większe zasoby, które można pobrać za pomocą pojedynczego żądania. Należy jednak uwzględnić obciążenie związane z pobieraniem niepotrzebnych danych. Pobieranie dużych obiektów może zwiększyć opóźnienie i spowodować naliczanie dodatkowych kosztów przepustowości. Aby uzyskać więcej informacji o antywzorcach wydajności, zobacz Duża liczba operacji we/wy i Nadmiarowe pobieranie.

Należy unikać wprowadzania zależności między internetowym interfejsem API a bazowymi źródłami danych. Na przykład jeśli dane są przechowywane w relacyjnej bazie danych, internetowy interfejs API nie musi ujawniać każdej tabeli jako kolekcji zasobów. Takie podejście raczej sugeruje, że projekt nie został dobrze przemyślany. Internetowy interfejs API należy traktować jako abstrakcję bazy danych. W razie potrzeby między bazę danych a internetowy interfejs API należy wprowadzić warstwę mapującą. Pozwoli to odizolować aplikacje klienckie od zmian w schemacie bazy danych.

Zamapowanie każdej operacji implementowanej przez internetowy interfejs API na określony zasób może również okazać się niemożliwe. Takie scenariusze, niezwiązane z zasobami, można obsługiwać za pomocą żądań HTTP, które wywołują funkcję i zwracają wyniki w postaci komunikatu odpowiedzi HTTP. Na przykład internetowy interfejs API implementujący proste operacje obliczeniowe, takie jak dodawanie i odejmowanie, może udostępniać identyfikatory URI, które ujawniają te operacje jako pseudozasoby i korzystają z ciągu zapytania do określania wymaganych parametrów. Na przykład żądanie GET do identyfikatora URI /add?operand1=99&operand2=1 zwróci komunikat odpowiedzi z treścią zawierającą wartość 100. Należy jednak rzadko używać identyfikatorów URI w takiej formie.

Definiowanie operacji interfejsu API pod względem metod HTTP

Protokół HTTP definiuje szereg metod, które umożliwiają przypisanie żądaniu znaczenia semantycznego. Poniżej wymieniono typowe metody HTTP używane przez większość internetowych interfejsów API RESTful:

  • GET: pobiera reprezentację zasobu o wskazanym identyfikatorze URI. Treść komunikatu odpowiedzi zawiera szczegółowe informacje o żądanym zasobie.
  • POST: tworzy nowy zasób o wskazanym identyfikatorze URI. Treść komunikatu żądania zawiera szczegółowe informacje o nowym zasobie. Warto pamiętać, że metoda POST pozwala również wyzwalać operacje, które nie tworzą zasobów.
  • PUT: tworzy lub zastępuje zasób o wskazanym identyfikatorze URI. Treść komunikatu żądania określa zasób do utworzenia lub zaktualizowania.
  • PATCH: wykonuje częściową aktualizację zasobu. Treść żądania określa zestaw zmian, które mają zostać zastosowane do zasobu.
  • DELETE: usuwa zasób o wskazanym identyfikatorze URI.

Wynik konkretnego żądania zależy od tego, czy zasób jest kolekcją czy pojedynczym elementem. Poniższa tabela zawiera podsumowanie wspólnych konwencji przyjętych przez większość implementacji RESTful przy użyciu przykładu handlu elektronicznego. Nie wszystkie te żądania mogą być implementowane — zależy to od konkretnego scenariusza.

Zasób POST GET PUT DELETE
/customers Tworzenie nowego klienta Pobieranie wszystkich klientów Zbiorcza aktualizacja klientów Usuwanie wszystkich klientów
/customers/1 Błąd Pobieranie szczegółowych informacji o kliencie customer 1 Aktualizowanie szczegółowych informacji o kliencie customer 1, jeśli istnieje Usuwanie klienta customer 1
/customers/1/orders Tworzenie nowego zamówienia klienta customer 1 Pobieranie wszystkich zamówień klienta customer 1 Zbiorcza aktualizacja zamówień klienta customer 1 Usuwanie wszystkich zamówień klienta customer 1

Zrozumienie różnic dotyczących działania metod POST, PUT i PATCH może sprawiać trudności.

  • Żądanie POST tworzy zasób. Serwer przypisuje identyfikator URI do nowego zasobu i zwraca ten identyfikator do klienta. W modelu REST żądania POST są często stosowane do kolekcji. Nowy zasób jest dodawany do kolekcji. Żądanie POST może również służyć do przesyłania danych do istniejącego zasobu w celu ich przetworzenia, bez tworzenia nowego zasobu.

  • Żądanie PUT tworzy zasób lub aktualizuje istniejący zasób. Klient określa identyfikator URI zasobu. Treść żądania zawiera pełną reprezentację zasobu. Jeśli zasób o wskazanym identyfikatorze URI już istnieje, jest on zastępowany. W przeciwnym razie jest tworzony nowy zasób, jeśli serwer to umożliwia. Żądania PUT są najczęściej stosowane do zasobów, które są pojedynczymi elementami, takimi jak konkretny klient, a nie do kolekcji. Serwer może obsługiwać aktualizowanie zasobów za pomocą metody PUT, ale nie ich tworzenie. Obsługa tworzenia zasobów zależy od tego, czy klient może wiarygodnie przypisać identyfikator URI do zasobu przed jego utworzeniem. Jeśli nie, do tworzenia zasobów należy użyć metody POST, a do ich aktualizacji — metody PUT lub PATCH.

  • Żądanie PATCH umożliwia częściową aktualizację istniejącego zasobu. Klient określa identyfikator URI zasobu. Treść żądania określa zestaw zmian, które mają zostać zastosowane do zasobu. Takie rozwiązanie może być wydajniejsze niż używanie metody PUT, ponieważ klient wysyła tylko informacje o zmianach, a nie całą reprezentację zasobu. Metoda PATCH pozwala również na utworzenie nowego zasobu (przez określenie zestawu aktualizacji dla zasobu o wartości „null”), jeśli serwer to umożliwia.

Żądania PUT muszą być idempotentne. Wielokrotne wysłanie tego samego żądania PUT powinno dać jednakowy efekt — zmodyfikowanie zasobu przy użyciu tych samych wartości. Nie gwarantuje się idempotentności żądań POST i PATCH.

Zgodność z semantyką HTTP

W tej sekcji opisano niektóre typowe kwestie dotyczące projektowania interfejsu API, który jest zgodny ze specyfikacją protokołu HTTP. Nie omówiono jednak wszystkich szczegółów ani scenariuszy. W razie wątpliwości należy zapoznać się ze specyfikacją protokołu HTTP.

Typy nośników

Jak wspomniano wcześniej, między klientami a serwerami są przesyłane reprezentacje zasobów. Na przykład treść żądania POST zawiera reprezentację zasobu do utworzenia. Z kolei treść odpowiedzi żądania GET zawiera reprezentację pobranego zasobu.

W protokole HTTP formaty są określane za pośrednictwem typów nośników, nazywanych również typami MIME. W przypadku danych innych niż binarne większość internetowych interfejsów API obsługuje kod JSON (typ nośnika = application/json) i prawdopodobnie XML (typ nośnika = application/xml).

Nagłówek Content-Type żądania lub odpowiedzi określa format reprezentacji. Oto przykładowe żądanie POST, które zawiera dane JSON:

POST https://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57

{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

Jeśli serwer nie obsługuje typu nośnika, powinien zostać zwrócony kod stanu HTTP 415 (Nieobsługiwany typ nośnika).

Żądanie klienta może zawierać nagłówek Accept, który zawiera listę typów nośników akceptowanych przez klienta w komunikacie odpowiedzi z serwera. Na przykład:

GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json

Jeśli serwer nie może dopasować żadnego z wymienionych typów nośników, powinien zostać zwrócony kod stanu HTTP 406 (Niedozwolone).

Metody GET

Metoda GET, której wywołanie przebiegło pomyślnie, zwykle zwraca kod stanu HTTP 200 (OK). Jeśli zasób nie został odnaleziony, metoda powinna zwrócić kod 404 (Nie znaleziono).

Jeśli żądanie zostało spełnione, ale nie ma treści odpowiedzi zawartej w odpowiedzi HTTP, powinien zostać zwrócony kod stanu HTTP 204 (Brak zawartości); na przykład operacja wyszukiwania, która nie daje dopasowań, może zostać zaimplementowana przy użyciu tego zachowania.

Metody POST

Jeśli metoda POST utworzy nowy zasób, zwracany jest kod stanu HTTP 201 (Utworzono). Identyfikator URI nowego zasobu znajduje się w nagłówku Location odpowiedzi. Treść odpowiedzi zawiera reprezentację zasobu.

Jeśli nastąpi przetworzenie danych bez utworzenia nowego zasobu, metoda może zwrócić kod stanu HTTP 200 i przekazać wynik operacji w treści odpowiedzi. Z kolei w przypadku braku wyniku metoda może zwrócić kod stanu HTTP 204 (Brak zawartości) bez treści odpowiedzi.

Jeśli żądanie klienta zawiera nieprawidłowe dane, serwer powinien zwrócić kod stanu HTTP 400 (Nieprawidłowe żądanie). Treść odpowiedzi może zawierać dodatkowe informacje o błędzie lub link do identyfikatora URI, który udostępnia więcej szczegółowych informacji.

Metody PUT

Jeśli metoda PUT utworzy nowy zasób, zwracany jest kod stanu HTTP 201 (Utworzono), podobnie jak w przypadku metody POST. Jeśli metoda PUT zaktualizuje istniejący zasób, zwracany jest kod 200 (OK) lub 204 (Brak zawartości). W niektórych przypadkach zaktualizowanie istniejącego zasobu może okazać się niemożliwe. W takiej sytuacji można zwrócić kod stanu HTTP 409 (Konflikt).

Warto zastanowić się nad implementacją zbiorczych operacji HTTP PUT, które umożliwiają przeprowadzenie aktualizacji wsadowej wielu zasobów w kolekcji. Żądanie PUT powinno określać identyfikator URI kolekcji, a treść żądania powinna zawierać szczegóły zasobów, które mają zostać zmodyfikowane. Takie podejście może pomóc zmniejszyć liczbę operacji i zwiększyć wydajność.

Metody PATCH

Żądanie PATCH pozwala klientowi wysłać zestaw aktualizacji istniejącego zasobu w formie dokumentu poprawki. Serwer przetwarza dokument poprawki, aby przeprowadzić aktualizację. Dokument poprawki nie zawiera opisu całego zasobu, a tylko zestaw zmian do zastosowania. Specyfikacja metody PATCH (RFC 5789) nie definiuje formatu dokumentów poprawek. Format jest ustalany na podstawie typu nośnika w żądaniu.

Prawdopodobnie najczęściej używanym formatem danych internetowych interfejsów API jest JSON. Dwa najważniejsze formaty poprawek oparte na notacji JSON to: poprawka JSON i poprawka scalająca JSON.

Budowa poprawki scalającej JSON jest nieco prostsza. Dokument poprawki ma tę samą strukturę co oryginalny zasób JSON, ale zawiera tylko podzbiór pól, które powinny zostać zmienione lub dodane. Ponadto można usunąć pole, podając wartość null jako wartość pola w dokumencie poprawki. (Z tego względu nie należy używać poprawki scalającej, jeśli oryginalny zasób może zawierać jawne wartości null).

Oryginalny zasób może mieć na przykład następującą reprezentację JSON:

{
    "name":"gizmo",
    "category":"widgets",
    "color":"blue",
    "price":10
}

Oto przykładowa poprawka scalająca JSON dla tego zasobu:

{
    "price":12,
    "color":null,
    "size":"small"
}

Informuje to serwer o zaktualizowaniu price, usunięciu colori dodaniu sizeelementu , a nie namecategory są modyfikowane. Szczegółowe informacje dotyczące poprawki scalającej JSON można znaleźć w specyfikacji RFC 7396. Typ nośnika dla poprawki scalania JSON to application/merge-patch+json.

Poprawka scalająca nie nadaje się do stosowania w przypadkach, w których oryginalny zasób zawiera jawne wartości null, ponieważ wartość null ma specjalne znaczenie w dokumencie poprawki. Ponadto dokument poprawki nie określa kolejności, w której należy zastosować aktualizacje na serwerze. Ewentualne konsekwencje tego faktu zależą od danych i domeny. Poprawka JSON, zdefiniowana w specyfikacji RFC 6902, jest bardziej elastyczna. Zmiany są określane za pomocą sekwencji operacji do wykonania. Operacje obejmują dodawanie, usuwanie, zastępowanie, kopiowanie i testowanie (w celu walidacji wartości). Typ nośnika dla poprawki JSON to application/json-patch+json.

Poniżej przedstawiono niektóre typowe błędy, które mogą wystąpić podczas przetwarzania żądania PATCH, wraz z odpowiednimi kodami stanów HTTP.

Błąd Kod stanu HTTP
Format dokumentu poprawki nie jest obsługiwany. 415 (Nieobsługiwany typ nośnika)
Nieprawidłowa postać dokumentu poprawki. 400 (Nieprawidłowe żądanie)
Dokument poprawki jest prawidłowy, ale nie można zastosować zmian ze względu na bieżący stan zasobu. 409 (Konflikt)

Metody DELETE

Jeśli operacja usuwania zakończy się pomyślnie, serwer internetowy powinien odpowiedzieć przy użyciu kodu stanu HTTP 204 (Brak zawartości), wskazując, że proces został pomyślnie obsłużony, ale treść odpowiedzi nie zawiera żadnych dalszych informacji. Jeśli zasób nie istnieje, serwer internetowy może zwrócić kod HTTP 404 (Nie znaleziono).

Operacje asynchroniczne

Czasami operacja POST, PUT, PATCH lub DELETE może wymagać przetwarzania, które trwa trochę czasu. Może to powodować nieakceptowalne opóźnienie, jeśli wysłanie odpowiedzi do klienta następuje po zakończeniu operacji. W takiej sytuacji należy rozważyć wprowadzenie operacji asynchronicznych. Zwrócony kod stanu HTTP 202 (Zaakceptowane) informuje o tym, że żądanie zostało przyjęte, ale jego przetworzenie nie zostało ukończone.

Należy udostępnić punkt końcowy, który zwraca stan żądania asynchronicznego, tak aby klient mógł monitorować ten stan przez sondowanie punktu końcowego. Identyfikator URI punktu końcowego stanu należy dołączyć do nagłówka Location odpowiedzi o kodzie 202. Na przykład:

HTTP/1.1 202 Accepted
Location: /api/status/12345

Odpowiedź na żądanie GET wysłane przez klienta do tego punktu końcowego powinna zawierać bieżący stan tego żądania. Opcjonalnie może zawierać również szacowany czas do zakończenia przetwarzania lub link umożliwiający anulowanie operacji.

HTTP/1.1 200 OK
Content-Type: application/json

{
    "status":"In progress",
    "link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
}

Jeśli operacja asynchroniczna tworzy nowy zasób, po jej zakończeniu punkt końcowy powinien zwrócić kod stanu 303 (Inne). Do odpowiedzi o kodzie 303 należy dołączyć nagłówek Location zawierający identyfikator URI nowego zasobu:

HTTP/1.1 303 See Other
Location: /api/orders/12345

Aby uzyskać więcej informacji na temat implementowania tego podejścia, zobacz Zapewnianie asynchronicznej obsługi długotrwałych żądań i wzorzec asynchronicznej odpowiedzi żądania.

Puste zestawy w treści komunikatów

Za każdym razem, gdy treść pomyślnej odpowiedzi jest pusta, kod stanu powinien mieć wartość 204 (Brak zawartości). W przypadku pustych zestawów, takich jak odpowiedź na przefiltrowane żądanie bez elementów, kod stanu powinien nadal mieć wartość 204 (Brak zawartości), a nie 200 (OK).

Filtrowanie i stronicowanie danych

Udostępnianie kolekcji zasobów za pomocą pojedynczego identyfikatora URI może powodować, że aplikacje będą pobierały duże ilości danych, mimo że potrzebny będzie tylko podzestaw jakichś informacji. Na przykład załóżmy, że aplikacja kliencka musi znaleźć zamówienia, których koszt przekracza określoną wartość. Aplikacja ta może pobrać wszystkie zamówienia z identyfikatora URI /orders, a następnie je przefiltrować po stronie klienta. Wyraźnie widać, że proces ten jest mało wydajny. Powoduje on straty przepustowości sieci i mocy obliczeniowej na serwerze hostującym internetowy interfejs API.

Zamiast tego interfejs API może zezwalać na przekazywanie filtru, takiego jak /orders?minCost=n, w ciągu zapytania identyfikatora URI. Internetowy interfejs API jest następnie odpowiedzialny za analizowanie i obsługę parametru minCost w ciągu zapytania i zwracanie filtrowanych wyników po stronie serwera.

Żądania GET wysyłane do zasobów kolekcji mogą potencjalnie zwracać dużą liczbę elementów. Projekt internetowego interfejsu API powinien ograniczać ilość danych zwracanych przez pojedyncze żądania. Należy rozważyć obsługę ciągów zapytań określających maksymalną liczbę elementów do pobrania i zastanowić się nad wprowadzeniem przesunięcia początkowego do kolekcji. Na przykład:

/orders?limit=25&offset=50

Należy również rozważyć zastosowanie górnego limitu liczby zwracanych elementów, aby zapobiec atakom typu „odmowa usługi”. Aby pomóc aplikacjom klienckim, żądania GET zwracające dane podzielone na strony powinny również zawierać jakąś postać metadanych, które informują o łącznej liczbie zasobów dostępnych w kolekcji.

Przy użyciu podobnej strategii można sortować pobierane dane, podając parametr sortowania, taki jak /orders?sort=ProductID, który przyjmuje nazwę pola jako wartość. Jednak ta metoda może mieć negatywny wpływ na buforowanie, ponieważ parametry ciągu zapytania stanowią część identyfikatora zasobu używanego w wielu implementacjach pamięci podręcznej jako klucz do danych.

Jeśli poszczególne elementy zawierają dużą ilość danych, można rozszerzyć to podejście, ograniczając zakres zwracanych pól. Na przykład parametr ciągu zapytania może przyjmować listę pól rozdzieloną przecinkami, taką jak /orders?fields=ProductID,Quantity.

Wszystkim parametrom opcjonalnym w ciągach zapytań należy nadać sensowne wartości domyślne. Na przykład w przypadku implementacji podziału na strony parametr limit należy ustawić na wartość 10, a parametr offset na wartość 0. W przypadku implementacji porządkowania parametr sortowania należy ustawić na klucz zasobu. Jeśli mają być obsługiwane projekcje, parametr fields należy ustawić na wszystkie pola w zasobie.

Obsługa częściowych odpowiedzi w przypadku dużych zasobów binarnych

Zasób może zawierać duże pola binarne, takie jak pliki lub obrazy. Aby wyeliminować problemy spowodowane przez zawodne i przerywane połączenie oraz skrócić czas odpowiedzi, należy rozważyć wprowadzenie obsługi pobierania tych zasobów we fragmentach. Aby uzyskać ten efekt, internetowy interfejs API powinien obsługiwać nagłówek Accept-Ranges w żądaniach GET dotyczących dużych zasobów. Ten nagłówek oznacza, że operacja GET obsługuje żądania częściowe. Aplikacja kliencka może przesyłać żądania GET, które zwracają podzestaw zasobu, określony jako zakres bajtów.

Ponadto należy wziąć pod uwagę zaimplementowanie żądań HTTP HEAD dla tych zasobów. Żądanie HEAD jet podobne do żądania GET. Różnica polega na tym, że żądanie HEAD zwraca tylko nagłówki HTTP, które opisują zasób, z pustą treścią komunikatu. Aplikacja kliencka może wysłać żądanie HEAD w celu ustalenia, czy zasób należy pobrać za pomocą żądań częściowych GET. Na przykład:

HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1

Oto przykładowy komunikat odpowiedzi:

HTTP/1.1 200 OK

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580

Nagłówek Content-Length zawiera informację o całkowitym rozmiarze zasobu, a nagłówek Accept-Ranges określa, że odpowiednia operacja GET obsługuje wyniki częściowe. Dzięki tym informacjom aplikacja kliencka może pobrać obraz podzielony na mniejsze fragmenty. Pierwsze żądanie pobiera pierwszych 2500 bajtów przy użyciu nagłówka Range:

GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499

Kod stanu HTTP 206 zwracany w komunikacie odpowiedzi oznacza, że jest to częściowa odpowiedź. Nagłówek Content-Length zawiera faktyczną liczbę bajtów zwróconych w treści komunikatu (nie rozmiar zasobu), a nagłówek Content-Range informuje o części zasobu (bajty 0–2499 z 4580):

HTTP/1.1 206 Partial Content

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580

[...]

Kolejne żądanie aplikacji klienckiej może pobrać pozostałą część zasobu.

Jednym z głównych celów wprowadzenia architektury REST jest umożliwienie nawigowania po całym zestawie zasobów bez znajomości schematu identyfikatora URI. Każde żądanie HTTP GET powinno — za pomocą hiperlinków zawartych w odpowiedzi — zwracać informacje umożliwiające odnalezienie zasobów bezpośrednio powiązanych z żądanym obiektem. Żądaniom GET należy również udostępnić informacje opisujące operacje dostępne na poszczególnych zasobach. Ta zasada jest określana jako HATEOAS — hipertekst jako aparat stanu aplikacji (Hypertext as the Engine of Application State). W praktyce system jest maszyną o stanie skończonym. Odpowiedź na każde żądanie zawiera informacje niezbędne do przejścia do kolejnego stanu. Dodatkowe informacje nie powinny być wymagane.

Uwaga

Obecnie nie ma żadnych standardów ogólnego przeznaczenia, które definiują sposób modelowania zasady HATEOAS. Przykłady przedstawione w tej sekcji ilustrują jedno możliwe, zastrzeżone rozwiązanie.

Aby na przykład zapewnić obsługę relacji między zamówieniem a klientem, reprezentacja zamówienia może zawierać linki, które identyfikują dostępne dla klienta operacje na zamówieniu. Oto możliwa reprezentacja:

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"DELETE",
      "types":[]
    }]
}

W tym przykładzie tablica links zawiera zestaw linków. Każdy link reprezentuje operację na powiązanej jednostce. Dane dla poszczególnych linków obejmują relację („customer”), identyfikator URI (https://adventure-works.com/customers/3), metodę HTTP oraz obsługiwane typy MIME. Są to wszystkie informacje, których potrzebuje aplikacja kliencka, aby wywołać operację.

Tablica links zawiera także informacje z odwołaniem do samej siebie dotyczące pobranego zasobu. Mają one relację self.

Zestaw zwracanych linków może się zmieniać w zależności od stanu zasobu. Właśnie na tym polega użycie hipertekstu jako „aparatu stanu aplikacji”.

Obsługa wersji internetowego interfejsu API RESTful

Jest mało prawdopodobne, że internetowy interfejs API pozostanie statyczny. W odpowiedzi na bieżące wymagania biznesowe zasoby mogą ulec zmianie po dodaniu nowych kolekcji lub przekształceniu relacji albo modyfikacji struktury danych. Aktualizacja internetowego interfejsu API pod kątem obsługi nowych lub zmienionych wymagań jest stosunkowo prosta. Należy jednak uwzględnić wpływ tych zmian na aplikacje klienckie korzystające z internetowego interfejsu API. Problem polega na tym, że chociaż deweloper projektujący i implementujący internetowy interfejs API ma pełną kontrolę nad tym interfejsem API, deweloper nie ma takiej samej kontroli nad aplikacjami klienckimi, które mogą być tworzone przez organizacje innych firm działające zdalnie. Najważniejsze jest zapewnienie niezmienionego działania istniejących aplikacji klienckich przy jednoczesnym umożliwieniu nowym aplikacjom korzystania z nowych funkcji i zasobów.

Kontrola wersji umożliwia wskazanie funkcji i zasobów udostępnianych przez internetowy interfejs API. Aplikacja kliencka może przesyłać żądania kierowane do określonej wersji funkcji lub zasobu. W poniższych sekcjach opisano kilka różnych rozwiązań. Każde z nich ma swoje zalety i charakterystyczne kompromisowe podejście.

Brak obsługi wersji

Ta najprostsza metoda może się sprawdzić w przypadku niektórych wewnętrznych interfejsów API. Istotne zmiany mogą być reprezentowane jako nowe zasoby lub nowe linki. Dodanie zawartości do istniejących zasobów może nie spowodować zmiany powodującej niezgodność, ponieważ aplikacje klienckie, które nie oczekują, że ta zawartość zostanie zignorowana.

Na przykład żądanie do identyfikatora URI https://adventure-works.com/customers/3 powinno zwrócić szczegóły pojedynczego klienta zawierającego idpola , namei address oczekiwane przez aplikację kliencką:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Uwaga

Dla uproszczenia przykładowe odpowiedzi przedstawione w tej sekcji nie zawierają linków HATEOAS.

Jeśli do schematu zasobu klienta zostanie dodane pole DateCreated, odpowiedź będzie wyglądać następująco:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}

Jeśli istniejące aplikacje klienckie mogą zignorować nierozpoznane pola, będą nadal działać prawidłowo. Z kolei nowe aplikacje klienckie mogą zostać zaprojektowane tak, aby obsługiwać nowe pole. Jednak wprowadzenie istotnych zmian, takich jak znaczące przekształcenia schematu zasobów (np. usunięcie lub zmiana nazw pól) albo modyfikacje relacji między zasobami, może uniemożliwić poprawne działanie istniejących aplikacji klienckich. W takich sytuacjach należy wziąć pod uwagę jedną z następujących metod.

Obsługa wersji za pomocą identyfikatora URI

Przy każdej modyfikacji internetowego interfejsu API lub zmianie schematu zasobów do identyfikatorów URI poszczególnych zasobów jest dodawany numer wersji. Istniejące identyfikatory URI powinny nadal działać jak wcześniej, zwracając zasoby, które są zgodne z oryginalnym schematem.

Rozszerzenie poprzedniego przykładu, jeśli address pole jest przekształcone w podpola zawierające każdą część składową adresu (na przykład streetAddress, citystate, i zipCode), ta wersja zasobu może być uwidoczniona za pośrednictwem identyfikatora URI zawierającego numer wersji, na przykład https://adventure-works.com/v2/customers/3:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

Opisany mechanizm kontroli wersji jest bardzo prosty i zależy od tego, w jaki sposób serwer kieruje żądania do odpowiednich punktów końcowych. Jednak po kilku iteracjach związanych z rozbudową internetowego interfejsu API mechanizm ten może okazać się nieporęczny, gdy serwer będzie musiał obsługiwać wiele różnych wersji. Ponadto, z punktu widzenia purysty, we wszystkich przypadkach aplikacje klienckie pobierają te same dane (klient 3), więc identyfikator URI nie powinien być naprawdę inny w zależności od wersji. Schemat ten komplikuje również implementację architektury HATEOAS, ponieważ wszystkie linki muszą zawierać numery wersji w identyfikatorach URI.

Obsługa wersji za pomocą ciągu zapytania

Zamiast dostarczać wiele identyfikatorów URI, możesz określić wersję zasobu przy użyciu parametru w ciągu zapytania dołączonego do żądania HTTP, takiego jak https://adventure-works.com/customers/3?version=2. Parametr wersji powinien mieć zrozumiałą wartość domyślną, taką jak 1, na wypadek jego pominięcia przez starsze aplikacje klienckie.

Semantyczna zaleta takiego podejścia polega na tym, że dany zasób jest zawsze pobierany przy użyciu tego samego identyfikatora URI. Jednak analiza ciągu zapytania w celu wysłania odpowiedniej odpowiedzi HTTP odbywa się w kodzie, który obsługuje żądanie. Ponadto podejście to stwarza takie same komplikacje dotyczące implementacji architektury HATEOAS co mechanizm kontroli wersji za pomocą identyfikatorów URI.

Uwaga

Niektóre starsze przeglądarki oraz internetowe serwery proxy nie buforują odpowiedzi na żądania zawierające ciąg zapytania w identyfikatorze URI. Może to obniżyć wydajność aplikacji internetowych korzystających z internetowego interfejsu API i uruchamianych z poziomu takiej przeglądarki internetowej.

Obsługa wersji za pomocą nagłówka

Zamiast dołączać numer wersji jako parametr ciągu zapytania, można zaimplementować niestandardowy nagłówek, który wskazuje na wersję zasobu. Metoda ta wymaga od aplikacji klienckiej dodawania odpowiedniego nagłówka do wszystkich żądań. Jednak w przypadku pominięcia nagłówka wersji w kodzie obsługującym żądanie klienta można użyć wartości domyślnej (wersja 1). W poniższych przykładach użyto niestandardowego nagłówka o nazwie Custom-Header. Wartość tego nagłówka stanowi informację o wersji internetowego interfejsu API.

Wersja 1:

GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Wersja 2:

GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

Podobnie jak w przypadku dwóch poprzednich podejść implementacja funkcji HATEOAS wymaga włączenia odpowiedniego nagłówka niestandardowego we wszystkich linkach.

Obsługa wersji za pomocą typu nośnika

Żądanie HTTP GET, które aplikacja kliencka wysyła do serwera internetowego, powinno zawierać obsługiwany format zawartości, określony przy użyciu nagłówka Accept. Zostało to opisane we wcześniejszej części tego przewodnika. Nagłówek Accept często służy do umożliwienia aplikacji klienckiej określenia, czy treść odpowiedzi powinna mieć format XML, JSON lub inny typowy format, obsługiwany przez klienta. Można jednak zdefiniować niestandardowe typy nośników, zawierające informacje, które pozwalają aplikacji klienckiej wskazać oczekiwaną wersję zasobu.

W poniższym przykładzie przedstawiono żądanie, które określa nagłówek Accept o wartości application/vnd.adventure-works.v1+json. Element vnd.adventure-works.v1 informuje serwer internetowy, że należy zwrócić wersję 1 zasobu. Z kolei element json oznacza, że treść odpowiedzi powinna być w formacie JSON:

GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json

W kodzie obsługi żądania, który przetwarza nagłówek Accept, należy zapewnić maksymalną zgodność z informacjami zawartymi w tym nagłówku (aplikacja kliencka może określić wiele formatów w nagłówku Accept — serwer internetowy wybiera wtedy najbardziej odpowiedni format treści odpowiedzi). Serwer internetowy potwierdza format danych treści odpowiedzi przy użyciu nagłówka Content-Type:

HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Jeśli nagłówek Accept nie zawiera znanych typów nośników, serwer internetowy może wygenerować komunikat odpowiedzi HTTP 406 (Niedozwolone) lub zwrócić komunikat z domyślnym typem nośnika.

To podejście jest prawdopodobnie „najczystszym” mechanizmem obsługi wersji. W naturalny sposób nadaje się ono do zaimplementowania architektury HATEOAS, w której typ MIME powiązanych danych może być zawarty w linkach do zasobów.

Uwaga

Wybierając strategię obsługi wersji, należy również uwzględnić jej wpływ na wydajność, a szczególnie na buforowanie na serwerze internetowym. Schematy obsługi wersji za pomocą identyfikatora URI i ciągu zapytania są przyjazne dla pamięci podręcznej, o ile dana kombinacja identyfikatora URI/ciągu zapytania za każdym razem odnosi się do tych samych danych.

Mechanizmy obsługi wersji za pomocą nagłówka i typu nośnika zwykle wymagają dodatkowej logiki, umożliwiającej sprawdzenie wartości w niestandardowym nagłówku lub nagłówku Accept. W dużym środowisku, w którym wielu klientów używa różnych wersji internetowego interfejsu API, może nastąpić nagromadzenie znacznej ilości zduplikowanych danych w pamięci podręcznej po stronie serwera. Może to stać się istotnym problemem, jeśli aplikacja kliencka komunikuje się z serwerem internetowym za pośrednictwem serwera proxy, na którym zaimplementowano buforowanie, a żądania są wysyłane do serwera internetowego tylko wtedy, gdy pamięć podręczna nie zawiera kopii żądanych danych.

Inicjatywa Open API

Celem projektu Open API, utworzonego przez konsorcjum branżowe, jest standaryzacja opisów interfejsów API REST pochodzących od różnych dostawców. W ramach tej inicjatywy specyfikację Swagger 2.0 przekształcono w specyfikację OpenAPI (OAS, OpenAPI Specification) i włączono do projektu Open API.

Warto uwzględnić specyfikację OpenAPI, projektując internetowe interfejsy API. Oto niektóre ważne kwestie:

  • Specyfikacja OpenAPI zawiera zestaw uzgodnionych wskazówek dotyczących projektowania interfejsu API REST. Ma to zalety w zakresie współdziałania, ale wymaga dodatkowej pracy związanej z zapewnieniem zgodności projektowanego interfejsu API ze specyfikacją.

  • Specyfikacja OpenAPI promuje podejście „najpierw kontrakt”, a nie podejście „najpierw implementacja”. Oznacza to, że najpierw projektuje się kontrakt interfejsu API (interfejs), a następnie pisze kod, który implementuje ten kontrakt.

  • Dostępne są narzędzia, takie jak Swagger, umożliwiające wygenerowanie bibliotek klientów lub dokumentacji na podstawie kontraktów interfejsów API. Zobacz na przykład strony pomocy interfejsu API sieci Web ASP.NET korzystające z programu Swagger.

Następne kroki