REST (Representational State Transfer) api to jedna z tych rzeczy, z których możemy korzystać całymi latami, kompletnie nie zdając sobie sprawy z tego jak wyglądają podstawy jej działania.

RESTful api oraz sposób jego implementacji zostało zdefiniowane przez Roy’a Fielding’a w jego rozprawie doktorskiej z 2000 roku. REST zostało zaprojektowane w taki sposób, aby w komunikacji pomiędzy urządzeniami korzystać z istniejących już protokołów. Oznacz to, że programista nie musi instalować żadnych dodatkowych bibliotek. Nie mówimy tu oczywiście o takich narzędziach jak Node.js. W końcu jakoś samą aplikację musimy napisać. Chodzi tutaj o fakt, że wykorzystując do komunikacji np. protokół HTTP nie musimy na naszej maszynie instalować żadnych dodatkowych narzędzi. HTTP jest powszechnie dostępnym i wykorzystywanym protokołem komunikacji. W przypadku RESTful api, HTTP jest też najczęściej wykorzystywanym protokołem.

Należy pamiętać o jednej bardzo ważnej rzeczy. REST sam w sobie nie jest frameworkiem, tak jak na przykład SOAP. REST jest jedynie zbiorem pewnych dobrych praktyk (o których przeczytacie za chwilę), które mają działać jako drogowskaz. Oznacza to, że RESTful api jest rozwiązaniem bardzo elastycznym. REST jest w stanie obsługiwać wiele rodzajów połączeń oraz zwracać kilka typów danych. Dla przykładu SOAP jest ograniczone jedynie formatu XML, podczas gdy REST jest w stanie pracować z XML, JSON lub nawet YAML.

Wyżej wspomniana elastyczność niesie jednak ze sobą ryzyko, że przekombinujemy i ostatecznie nasze api nie będzie tak naprawdę RESTful. Dlatego przy ocenie danego api należy wspierać się 6 kluczowymi elementami, które zostały zdefiniowane przez Roy’a Fielding’a:

Client-Server – architektura typu Klient – Serwer.

Cache – buforowanie danych tam gdzie to możliwe (po stronie klienta).

Stateless – poszczególne połączenia są od siebie niezależne.

Uniform Interface – separacja implementacji po stronie klienta oraz serwera poprzez zastosowanie jednolitego interfejsu.

Layered System – dzielnie architektury REST na współdziałające ze sobą warstwy.

Code on Demand – pozwala na przesyłanie nowych fragmentów kodu poprzez api.

Poniżej przybliżę Wam każdy punkt z osobna.

Client – Server

Architektura klient – serwer zakłada, że aplikacja kliencka powinna być całkowicie niezależna od aplikacji serwerowej. Każda z implementacji powinna mieć możliwość samodzielnego rozwoju. Jeżeli na przykład dokonamy jakiejś zmiany w aplikacji pisanej dla systemu iOS (to będzie nasz klient), to zmiana ta nie powinna w żaden sposób wpływać na aplikacje serwerową (strukturę bazy danych itp.). Tak samo powinno to działać w drugą stronę. Zmiana w strukturze bazy danych po stronie serwera nie powinna w żaden sposób wpływać na aplikację kliencką. Taka separacja pozwala na niezależny rozwój każdej z aplikacji.

Stateless

Zrozumienie tego punktu na samym początku przysporzyło mi najwięcej problemów. Rozjaśniło mi się dopiero, gdy zbudowałem sobie proste api w Node.js.

 

 

Stateless oznacza, że każde połączenie z serwerem ze strony klienta powinno być całkowicie niezależne. Powinno zwierać wszystkie informację potrzebne do tego, aby połączenie mogło zakończyć się sukcesem. Może wydawać się to trochę nie jasne, dlatego w części praktyczniej będzie mogli zobaczyć sobie jak to będzie wyglądało w kodzie.

Wracając jednak do teorii. REST api obsługując dane połączenie, powinni całkowicie polegać na danych, które zostały dostarczone razem z tym połączeniem. Mówimy tutaj o takich danych jak id użytkownika, token, czy klucz api. Dane te nie mogą być przechowywane przez serwer pomiędzy sesjami. Takie rozwiązanie wpływa bardzo pozytywnie na niezawodność REST api.

Pomyślcie chwilę o tym – wykonanie każdego połączenia z serwerem jest obarczone ryzykiem, że coś może się nie udać. W najbardziej banalnym przypadku zostanie zerwane przez brak dostępu urządzenia do sieci. Jeżeli uzyskanie konkretnych danych wymagałoby kilku połączeń do utworzenia jednego żądania, to ryzyko niepowodzenia znacznie by wzrosło. Przesyłanie wszystkich danych w ramach jednego połączenia działa odwrotnie, minimalizując to ryzyko.

 

Cache

REST api powinno być projektowane z myślą o możliwości buforowania danych będących przedmiotem wymiany na linii klient – serwer. Buforowanie to powinno dobywać się oczywiście po stronie klienta, ponieważ to klientowi będą one najbardziej potrzebne. Takie podejście jest spowodowane implementacją zasady Stateless – serwer musi poradzić sobie z bardzo dużą ilość przychodzących zapytań oraz odpowiedzi, które musi udzielić na każde z nich.

Serwer powinien informować klienta w odpowiedzi, czy dane mogą być buforowane (i przez jaki okres czasu), czy też powinny być dostępne w czasie rzeczywistym (brak buforowania po stronie klienta). Takie podejście jest w stanie w znacznym stopniu zredukować ilość połączeń z serwerem, a tym samy pozwoli znacznie zmniejszyć wykorzystywane zasoby. Z drugiej strony na wydajności może także zyskać aplikacja kliencka. Po co pobierać za każdym razem nowy pakiet danych jeżeli nie będzie się on niczym różnił do poprzedniego? Jest to szczególnie ważne w przypadku aplikacji mobilnych.

 

Layered System

REST API powinno być projektowane z podziałem na poszczególne warstwy, które powinny pełnić odmienne funkcje. Nie różni się to aż tak bardzo od architektury, którą implementujemy dla przykładowej aplikacji iOS. W takim na przykład MVVM jest bardzo wyraźny podział na warstwy obarczone innym zakresem odpowiedzialności. Właściwa implementacja każdej z warstw ułatwia skalowanie aplikacji, a także jej testowanie.

Dzielenie systemu na poszczególne warstwy jest również bardzo istotne ze względu na aspekt bezpieczeństwa RESTful api. Każda warstwa to potencjalne miejsce obrony przed atakiem na nasz serwer. Oczywiście może to być broń obosieczna, bo niewłaściwa implementacja może doprowadzić do sytuacji, w której każda warstwa stanie się potencjalną luką w bezpieczeństwie.

 

Code on Demand

Jeden z najmniej znanych elementów designu REST api. Sam trafiłem na niego dopiero podczas pracy nad tym artykułem. Być może jest tak dlatego, że punkt ten został oznaczony przez dr. Roy’a Fielding’a jako opcjonalny. A być może dlatego, że jest to technika, która wybiega trochę w przyszłość i nie jest w tej chwili powszechnie stosowana.

Code on demand zakłada, że aplikacja kliencka nie musi polegać jedynie na kodzie, który został umieszczony w samej aplikacji. Zamiast tego dodatkowy kod mógłby zostać przesłany właśnie za pośrednictwem REST api. Wyobraźcie sobie sytuację, w której możecie dodać nową funkcjonalność do Waszej aplikacji poprzez pobranie nowego pakietu kody z serwera, bez konieczności ponownej kompilacji. Prawda, że ciekawa opcja? Założenie to było jednak przewidziane do wykorzystania głównie w aplikacjach webowych. Trudno mi ocenić, czy jest miejsce na takie rozwiązanie w aplikacjach mobilnych, gdzie środowisko wykonania aplikacji (szczególnie na iOS) jest bardzo restrykcyjne.

 

REST od kuchni

 

Teorię zawsze dobrze jest przełożyć na praktykę, dlatego teraz sprawdzimy sobie jak zbudować typowe zapytanie w stylu REST (REST style 😎). Założenie jest proste. Klient o coś pyta a serwer odpowiada. Żądanie ze strony klienta powinno jednak posiadać pewną strukturę, tak aby serwer wiedział o co klientowi chodzi. Jako że RESTful api do komunikacji wykorzystuje najczęściej protokół HTTP, to poniższe przykłady również zostaną oparte na tym protokole.

 

 

Budowa zapytania (HTTP request)

Żądanie w formie HTTP powinno składać się z następujących elementów (każdy z nich zostanie szerzej omówiony w dalszej części):

Request method (Metoda HTTP) – za jej pomocą określamy jaką operację chcemy wykonać na danym zasobie.

Header – nagłówek może składać się z kilku pól, a jego przeznaczeniem jest przechowanie dodatkowych informacji o żądaniu.

Resource path – ścieżka dostępu do danego zasobu – czyli tzw. URI.

Body – miejsce do umieszczenia dodatkowych informacji. Wykorzystywane głównie podczas edycji / tworzenia zasobów na serwerze. Może być puste.

Tak będzie wyglądała przykładowa budowa żądania HTTP, jeżeli będziemy chcieli utworzyć nowy artykuł na stronie z newsami:

 

HTTP request method

Do najbardziej podstawowych należą:

GET – pobieranie danych z serwera.

POST – umieszczanie nowy danych na serwerze.

PUT – podmiana już istniejących danych na serwerze.

DELETE – usunięcie danych z serwera.

PATCH – częściowa modyfikacja danych na serwerze.

Po więcej informacji oraz dodatkowe metody HTTP zapraszam do tej dokumentacji.

 

HTTP request header

W nagłówku można umieścić bardzo dużo dodatkowych informacji korzystając z par key – value. Rodzaje nagłówków zostały jednak z góry określone, a pełną listę możecie znaleźć pod tym adresem (po kliknięciu w link sprawdźcie menu po lewej stronie). Lista jest bardzo długa, dlatego my skupimy się tylko na kliku opcjach.

Nagłówek Accept będzie określał jakiego typu danych aplikacja kliencka będzie oczekiwała. Dzięki temu serwer nie będzie wysyłał czegoś co będzie dla klienta niezrozumiałe. Wartość nagłówka Accept będzie zawierała jeden z kilku dostępnych typów określanych jako MIME (Multipurpose Internet Mail Extensions). Typ MIME dzieli się dodatkowo na dwie części – typ właściwy (type) oraz typ pochodny (subtype). Tak będzie wyglądała wartość dla pola Accept, jeżeli klient w odpowiedzi serwera będzie oczekiwał danych w formacie JSON:

A tak, gdy klienta będzie oczekiwał w odpowiedzi pliku png:

Wartości pola można łączyć, dlatego taki zapis jest również możliwy:

 

HTTP request resource path

Ścieżka określająca, gdzie na serwerze należy szukać określonego zasobu. W RESTful api ścieżki powinny być projektowane w taki sposób, aby samy zapisem jasno wyrażały swoje przeznaczenie. Jeżeli na przykład chcemy uzyskać dostęp do określonego artykułu (korzystając z GET) na stronie z newsami, to powinniśmy według konwencji użyć liczby mnogiej danego zasobu:

Zapis ten jasno wskazuje, że chcemy uzyskać dostęp do artykułu o id równym 45.

Ścieżka powinna zawierać jedynie minimum danych niezbędnych do identyfikacji zasobu. Przykład z metodą POST:

Korzystając z metody POST doskonale wiemy, że klient będzie chciał utworzyć na serwerze nowy artykuł, dlatego nie ma potrzeby dodawania dodatkowych argumentów w ścieżce (jak na przykład pole id).

 

HTTP message body

Tutaj możemy umieścić dodatkowe dane przesyłane na serwer. Body nie ma żadnej określonej struktury, ale to co tam umieścimy musi być zrozumiałe dla serwera. W pracy za aplikacjami mobilnymi powszechnym zwyczajem jest umieszczanie w body danych potrzebnych do utworzenia nowego obiektu w bazie danych na serwerze. Wykorzystamy ponownie przykład ze stroną z newsami. Załóżmy, że chcemy utworzyć nowy artykuł. Żądanie wysłane na serwer, w uproszczonej formie mogłoby wyglądać tak:

Korzystając z formatu JSON, przesyłamy w body wszystkie dane potrzebne do utworzenia artykułu.

Body jest wykorzystywane w operacjach takich jak POST, PUT, czy PATCH. Czyli takich, w których wymagana jakaś ingerencja w dane znajdujące się na serwerze.

 

Odpowiedź z serwera

Jak już serwer o coś zapytamy, to oczywiście powinniśmy dostać odpowiedź, która podobnie do request’a powinna mieć określoną strukturę. Ta będzie bardzo podobna do struktury samego żądania. Główną różnicę stanowi pierwsza linijka. Zamiast metody HTTP i ścieżki do zasobu otrzymamy odpowiedni kod odpowiedzi z serwera (więcej za chwilę). Wróćmy jeszcze raz do przykładu z pobieraniem artykułów ze strony z newsami. Wysyłamy poniższe żądanie na serwer:

A w odpowiedzi możemy od serwera uzyskać poniższe informacje:

Nagłówek Content-Type określa jakiego typu danych powinien spodziewać się klient. W tym wypadku serwer korzystając z body prześle do klienta wszystkie informacje o artykule przechowywanym w bazie danych pod id równym 45.

 

Kody odpowiedzi serwera (Response Codes)

Każda z odpowiedzi serwera zawiera na samym początku informację o tym jak dane żądanie klienta zostało obsłużone. Jeżeli wszytko poszło zgodnie z planem to serwer może odpowiedzieć standardowym kodem 200 (OK) lub 201 (CREATED) w przypadku pomyślnego utworzenia nowego rekordu w bazie danych. Jeżeli żądanie klienta zostało błędnie sformułowane (niewłaściwa struktura, szukanie zasobu, który nieistnieje) serwer może odpowiedzieć kodem 400 (BAD REQUEST) lub 404 (NOT FOUND).

Jaki kod serwer wyśle w odpowiedzi zależy oczywiście od tego co programista backendu ustawi w kodzie aplikacji. Może on równie dobrze wysłać klientowi kod 200 (OK) w przypadku, gdy tak naprawdę dany zasób nie istnieje. To oczywiście skrajny przypadek, ale chciałem uświadomić Wam, że po stronie serwera nie ma żadnej magii i nic nie dzieje się automatycznie. Serwer tak naprawdę składa się z kolejnej aplikacji oraz maszyny zdalnej, na której aplikacja ta została uruchomiona.

Dlatego też dla żądań HTTP istnieją określone standardy, dzięki którym w komunikacji pomiędzy klientem i serwerem zachowywana jest pewna spójność. Na przykład w przypadku żądań zakończonych sukcesem przyjęło się następuję kody odpowiedzi:

GET — return 200 (OK)
POST — return 201 (CREATED)
PUT — return 200 (OK)

Pozostałe kody odpowiedzi serwa oraz dodatkowe informacje możecie sprawdzić pod tym adresem (menu po lewej stronie).

 

Słowo na drogę

Wyszło znacznie dłużej niż planowałem, ale chciałem Wam przedstawić szerszą perspektywę. W codziennej pracy zwykle nie zastanawiamy się jak wygląda komunikacja naszej aplikacji z serwerem. A wiedza to może nam znacznie ułatwić życie w chwili, gdy coś zaczyna się sypać. Na przykład kiedy serwer bez najmniejszego ostrzeżenia zaczyna wysyłać w odpowiedzi jakieś niezrozumiałe „krzaki” 😩. Do następnego 🧐.

 

Link do tytułowej grafiki


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *