REST API

Created with Sketch.

REST API

Representational state transfer (REST) to styl architektoniczny stworzony przez Roy’a Fielding’a w 2000r oparty o protokół HTTP do komunikacji klient-server. Wprowadza on określone zasady co do budowy i działania API dzięki czemu jest samo-dokumentujacy się, bez stanowy i łatwo cachowalny.

Co to oznacza w praktyce? Moim zdaniem dwie cechy tego stylu są kluczowe; bezstanowość i „smo-dokumentacja”.

Bez-stanowość czyli każdy request do zasobu musi zawierać komplet informacji aby móc zostać obsłużony. Bez-stanowość pozwala na łatwe użycie cache zarówno po stronie klienta jak i serwera.

Samo-dokumentujący się, czyli łatwy do zrozumienia przez użytkownika. Bez dużego progu wejścia czyli bez konieczności znajomości dużej ilości dokumentacji, można zacząć konsumować REST API.

Schemat działania opartej o REST

  1. Klient przygotowuje zapytanie http do zasobu – body, nagłówki http
  2. Klient wysyła odpowiednia metodą http przygotowane wcześniej zapytanie na adres url zasobu
  3. Serwer odbiera zapytanie i zwraca odpowiedź. Odpowiedź składa się ze statusu http oraz opcjonalnie: body, nagłówków http i linków do powiązanych zasobów i operacji*

Co jest co ?

Zasób

Podstawowy byt w REST. Jest to ważny, nazwany byt w naszym systemie. Każdy zasób jest określony przez jednoznaczny url, którego częścią jest nazwa zasobu. Zasoby nazywane są zgodnie z następującą zasadą:

nazwa zasobu to rzeczownik w liczbie mnogiej.

Operacje na zasobach

Operacji na zasobach dokonuje się poprzez pomocy wysłania żądania do zasobu odpowiednią metdą http, i tak:

  • POST – utworzenie zasobu (operacja nie jest idemotentna)
  • PUT – aktualizacja całego zasobu
  • GET – pobieranie zasobu
  • DELETE – usuwanie zasobu
  • PATH – częściowa aktualizacja zasobu, tylko wyspecyfikowanych pół na przesłane wartości treści żądania

pozostałe metody http mogą być używane do np diagnostyki lub odkrywania api.

  • HEAD – zwraca sam nagłówek odpowiedzi bez BODY (przydatne jeżeli zwracany zasób jest bardzo duży a my chcemy zobaczyć np tylko jego rozmiar)
  • OPTIONS – informacje o możliwych opcjach na zasobie (np obsługiwane metody http)
Różnica pomiędzy PUT a PATH

załóżmy że posiadamy w naszym api zasób „products” i skalda się z 4 pól id, nazwa, opis, cena oraz produkt:

metodabodywynik
PUT{
„name”: „new name”
}

{
„id”: 1,
„name”: „new name”,
„description”: null,
„price”: null
}
PUT{
„name”: „new name”,
„description”: „the best product in the shop”,
„price”: „99.99”
}
{
„id”: 1,
„name”: „new name”,
„description”: „the best product in the shop”,
„price”: „99.99”
}
PATH{
„name”: „new name”
}
{
„id”: 1,
„name”: „new name”,
„description”: „the best product in the shop”,
„price”: „99.99”
}

Statusy HTTP

  • 1XX – statusy informacyjne,
  • 2XX – operacja zakończona powodzeniem, np 201 zasób utworzony poprawnie
  • 3XX – przekierowanie, np 301 zasób trwale przeniesiony
  • 4XX – błąd po stronie klienta, użytkownika, np. 404 odwołanie się do nie istniejącego zasobu, 422 niepoprawna zawartość żądania
  • 5XX – błąd po stronie serwera

Nagłówki http

To dodatkowe informacje w postaci listy par klucz-wartość, gdzie klucze są ciągami znaków nie wrażliwymi na wielkość liter, a wartości po prostu ciągami znaków. Do odzielenia klucza od wartości używany jest jest „:” (dwukropek). Jeżeli chcemy dodać własny nagłówek zgodnie z konwencją należy dodać na początku X-.

Najczęściej używane nagłówki od strony klienta:

NagłówekPrzeznaczenie
Authorization
przesłanie danych do uwierzytelnienia w postaci:
Authorization: TYP CREDENTIALS
np.:
„Authorization: Barer -secret_token”,
„Authorization: Basic admin:pass123”,
Acceptinformacja jakie typy danych potrafi poprawnie obsłużyć (zrozumieć) klient, np.:
„Accept: application/json”,
„Accept: */*”
Content-typetyp danych przesyłany w body

Najczęściej używane nagłówki od strony klienta:

NagłówekPrzeznaczenie
Content-Lengthdługość w znakach body
Content-typetyp danych przesyłany w body
Access-Control-Allow-Origin informacja o możliwościach używania zasobów zdefiniowanych cross origin policy

Idempotentność

Jeżeli wielokrotne zastosowanie danej operacji nie ma wpływu na wynik to taka operacja jest idempotentna. Formalnie wrunek idempotentości dla funkcji można by zapisać

f(x) = f(f(x))

Przykładem takiej operacji może być funkcja która do liczby dodaje 0 jest idempotentna.

Jakie to ma znaczeni kontekście definicji REST API ?

Bardzo duże! Jeżeli wiemy jakie operacje są idempotentne to ich wynik możemy np cachować lub bez ryzyka uszkodzenia zasobu ponawiać.

Które zatem operacje w REST muszą być idempotentne ?

MetodIdempotentnaUzasadnienie
PUTTAKniezależnie ile razy wyślemy aktualizacje zasobu z tą samą treścią w wyniku otrzymamy ten sam wynik, np. jeżeli zaktualizujemy nazwę pola „name” na „new name” i wyślemy takie żądanie 3 razy z każdym razem nazwa będzie taka sama tj. „new name”
DELETETAKniezależnie ile razy usuniemy zasób, a dokładniej rzecz ujmując wyślemy żądanie usunięcia zasobu to zasobu cały czas nie będzie
GETTAKpobranie zasobu, listy zasobów zawsze skutkuje tym samym wynikiem, a samo pobranie nie może mienić zasobu
HEADTAKponieważ jest to de facto okrojony fragment GET
OPTIONSTAKponieważ niezależnie ile razy zapytamy o opcje dla zasobu. to zawsze dostaniemy ten sam zbiór możliwości
POSTNIEponieważ za każdym razem tworzony jest nowy zasób z nowym identyfikatorem, tj wielokrotne

Parametry

Aby przekazać parametr do żądania możemy do zrobić na dwa sposoby.

  • jako path param – zmienna znajduje się na scieżce do zasobu, np. pobierając wszystkie adresy użytkownika moglibyśmy mieć adres w postaci: „users/123/adresses/”, gdzie 123 to podstawiany identyfikator użytkownika
  • jako query param – zmienna znajduje się w zapytaniu po znaku zapytania, np. filtrujac zamówienia w sklepie tylko do tych dostarczonych „orders?status=DELIVERED

HATEOAS

 Hypermedia As The Engine Of Application State – to dodatkowe informacja o potencjalnych krokach jakie może wykonać klient / użytkownik API dodawane do odpowiedzi, np. jeżeli mamy zasób użytkownik, który ma z kolei ma adresy to odpowiedź gdy zapytamy o adres może wyglądać tak:

Jak widać patrząc tylko na response możemy łatwo zauważyć że adresów jest wiele dla jednego użytkownika i są one z nim ściśle powiązane.

Spójność

Projektując ub rozwijając API należy pamietać o jego holistycznej spójności, ponieważ każde łamanie przyjętych konwencji powodują nadmierne skomplikowanie, trudności z zrozumieniem, daje przestrzeń do występowaniu błędów integracji i wiele wiele innych.

  • nazwy – należy pamietać również o przyjętych w projekcie konwencjach nazewniczych zarówno do zasobów jak i pól np. wielowyrazowych zasobów tj. jeżeli w jednej części nazwy wielowyrazowe zapisujemy z pauzami (myślnikami) to w całym api ta konwencja powinna być spójna,
  • obsługa błędów – jeżeli przy operacjach na jednym zasobie zwracamy body z treścią błędu to analogiczne body powinniśmy zwracać w innych miejscach, z dokładnością do jego zawartości; jeżeli w jednym zasobie nieudana walidacji przy próbie stworzenia zasobu daje 422 to w innych miejscach nie na ten sam błąd nie powinniśmy używać np ogólnego 400.
  • obsługa autoryzacji do zasobu – sposób uwierzytelniania powinien być taki sam na wszystkich zasobach, nie powinniśmy wymagać np. na jednym zasobie uwierzytelnienia w postaci tokenu a na innym user credentials, niepotrzebnie komplikuje to zarówno obsługę po stronie klienta jak i serwera.
  • obsługa parametrów

Przykład

Teraz wszystko co jest do kupy postaram się zobrazować prostym przykładam w postaci fragmentu api dla sklepu internetowego do obsługi produktów. Funkcjonalności naszego api to CRUD plus dodatkowa możliwość filtrowania/wyszukiwania produktów.

Base url dla naszego api to będzie /awsome-shop/api/rest, wszystkie zasoby będą produkowały i konsumowały JSON

Zacznijmy od ustalenia nazwy zasobu i path dla niego. Zgodnie z zasadą rzeczownik liczba mnoga, otrzymujemy „/products” a path „awsome-shope/api/rest/products„. Done.

Załóżmy, że produkt składa się z nazwy, opisu, ceny, listy atrybutów.

Przejdźmy od pobrania wszystkich dostępnych produktów z możliwością paginacji i ich wyszukiwania. Będzie to jeden endpoint, ponieważ obie operacje dotyczyć będą tego samego zasobu. Parametry do paginacji i wyszukiwanie w query params jako opcjonalne. Domyślnie zwracana jest pierwsza strona wielkości 2, i otrzymujemy:

Dobra możemy już przeglądać nasze produkty teraz dodamy możliwość ich tworzenia:

W celu aktualizacji produktu musimy przekazać identyfikator produktu. Tu zasadniczo są dwie opcje:

  1. przekazujemy w path
  2. przykazujemy w body

Ja w przykładzie postanowiłem informacje o identyfikatorze zawrzeć w path, wydaje mi się to być bardziej czytelne.

Jak wspominałem wcześnie PUT nadpisuje wszystkie pola przesłane w zapytaniu, w celu ułatwienia korzystania z API możemy zaimplementować PATCH w celu udostępnienia częściowej aktualizacji. Jest to wygodne w przypadku rozbudowanych encji. Żądanie będzie identyczne jak w przypadku PUT z różnica polegać będzie na wykorzystaniu innej metody HTTP, natomiast odpowiedź powinna być identyczna.

Operacją, której nam do kompletnego CRUD to usuwania, czyli metoda DELETE. Tu sprawa jest dość prosta. Zwróć proszę uwagę na status odpowiedzi. 204 to status, który mówi, że „czegoś nie ma, ale dobrze że tego nie ma„.