REST, co oznacza Representational State Transfer, jest stylem architektonicznym wykorzystywanym w
rozwoju usług internetowych. Został opracowany przez Roya Fieldinga w jego rozprawie doktorskiej w
2000 roku. REST wykorzystuje zestaw dobrze zdefiniowanych operacji (takich jak GET, POST, PUT,
DELETE) oraz zasobów, które są reprezentowane przez URL-e. Głównymi cechami REST są:
REST, czyli
Representational State Transfer
REST to styl, którego najważniejszym elementem w tworzeniu aplikacji webowych jest zasób (resource).
Co może być zasobem? Tak naprawdę wszystko to, co w rzeczywistości definujemy jako obiekt. Może nim
być, np. użytkownik (user), albo wydarzenie (event), albo dowolna inna reprezentacja podmiotu
występującego w rzeczywistości. W naszej aplikacji także korzystamy z różnych zasobów, na przykład: element
(item), kategoria elementu (item category), typ elementu (item type) oraz wiele innych.
Drugim ważnym aspektem formuły REST jest zdefiniowanie sposobu dostępu do wspomnianych zasobów. W
tym celu korzystamy z bezstanowego protokołu HTTP, który dostarcza kilka istotnych metod
umożliwiających wykonywanie zdalnych operacji na zasobach, m.in
GET, POST, PUT, DELETE. Zarówno o tym, jak i o kilku dodatkowych sprawach pomówimy szerzej w kolejnym punkcie.
Załóżmy, że naszym głównym celem jest budowa aplikacji webowej.
Zanim to zrobimy warto abyśmy rozpoczęli od zapoznania się z całym procesem wymiany danych z "lotu ptaka".
Strona internetowa z kodem HTML w przeglądarce
Proces działania aplikacji rozpoczyna się w przeglądarce. Tak więc, po wpisaniu przez
użytkownika adresu strony przeglądarka wysyła żądanie na serwer w celu uruchomieniu procesu
przetwarzania danych. URL na jaki wysyłane jest żądanie może być zdefiniowany na różne sposoby:
-
Bezparametrowo:
http://localhost:8080/items
-
Z parametrem typu query param:
http://localhost:8080/items?amount=10
-
Z parametrem type path variable:
http://localhost:8080/items/1
Wysyłanie żądania bardzo często jest wykonywane także
już po załadowaniu strony, podczas obsługi zdarzenia użytkownika (np. po kliknięciu w przycisk
submit formularza albo podczas kliknięcia w link). W ten sposób punktem wyjścia dla użytkownika jest
fragment HTML danego przycisku, formularza czy też innego komponentu, z którego nastąpi wygenerowanie
zdarzenia.
Wykorzystanie kodu JavaScript do wysłania żądania (requestu HTTP)
Po wykonaniu przez użytkownika akcji na danym komponencie następuje wykonanie kodu JavaScript, który
zwykle wstępnie przygotowuje dane przesyłane z komponentu HTML oraz uruchamia mechanizmy, których
zadaniem jest przygotowanie i wysłanie requesta na serwer.
Wykonanie obsługi żądania HTTP odbywa się za pomocą kilku powszechnie znanych metod:
GET,
HEAD,
PUT,
POST,
DELETE,
PATCH.
Appa Notka.
Zanim przejdziemy do omawiania charakterystyki metod HTTP, zacznijmy od wyjaśnienia pojęcia, które jest mocno związane z
tworzeniem systemów informatycznych, w tym aplikacji webowych. Pojęciem tym jest akronim CRUD. Definiuje on cztery podstawowe operacje, które możemy wykonać na danych:
- Create - zapis danych
- Read - odczyt danych
- Update - modyfikacja danych
- Delete - usunięcie danych
Tworząc aplikację prawie zawsze używamy pełnego zestawu tych operacji.
Metody HTTP są wręcz stworzone do tego, aby za ich pomocą wykonywać CRUD-owe operacje.
Co zatem musimy zrobić gdy chcemy wysłać żądanie zgodne z efektem jakiego oczekujemy?
Otóż musimy zbudować odpowiedni URL oraz wybrać pasujący rodzaj metody.
Appa Notka.
URL najlepiej jest konstruować zgodnie z dobrymi praktykami. Mówią one, że w przypadku prostych operacji CRUD powinniśmy
stosować liczbę mnogą dla zasobu, który chcemy zmodyfikować. Tak więc, jeśli chcemy dodać, zmienić, odczytać lub usunąć
jakiś item w aplikacji, URL powinien wyglądać tak:
<<PROTOKÓŁ>>://<<HOST>>/items
Na przykład:
http://localhost:8080/items
Użycie liczby pojedynczej nie jest zalecane ze względu na jednolitość definicji dla zasobu. O ile podanie liczby pojedynczej (/item)
pasuje dla odczytu, usunięcia lub modyfikacji pojedynczego zasobu, o tyle już pobranie wszystkich zasobów i tak musiałoby się skończyć użyciem liczby mnogiej (/items).
Nie chcielibyśmy raczej podawać (/item) i spodziewać się pobrania wielu zasobów.
Reasumując, chcąc zachować spójną konwencję, warto używać liczby mnogiej dla wszystkich przypadków. Pasuje ona zarówno do obsługi wszystkich zasobów, jak i pojedynczego zasobu.
No dobrze. To teraz zobaczmy jakie metody HTTP mamy do dyspozycji i do jakich operacji CRUD one pasują:
-
GET - Pobiera zasób, a więc realizuje operację typu Read (CRUD):
Pobranie wszystkich itemów: /items
Pobranie wybranego itema: /items/5
Powyższe dwa przykłady obrazują proste pobranie albo wszystkich albo pojedynczego itema (o id równemu 5). Natomiast poniżej mamy już sposób podejścia do tematu, gdy chcemy pobrać itema będącego w relacji.
Mamy kategorie itemów i tutaj najpierw odwołujemy się do kategorii (id: 2), a następnie do wszystkich itemów z tej kategorii oraz do konkretnego itema (id: 5) z tej kategorii:
Pobranie wszystkich itemów z konkretnej kategorii: /categories/2/items
Pobranie wybranego itema z danej kategorii: /categories/2/items/5
To jest dobry sposób na modelowanie URL-i w przypadku relacji.
-
HEAD - Pobiera metadane o zasobie, a więc w pewien sposób realizuje operację typu Read (CRUD):
Konstrukcja URL-i (poniżej) wygląda podobnie, jak w przypadku samej metody GET.
Pobranie nagłówków dla wszystkich itemów:
/items
Pobranie nagłówków dla wybranego itema:
/items/5
Pobranie nagłówków dla wszystkich itemów z konkretnej kategorii:
/categories/2/items
Pobranie nagłówków dla wybranego itema z danej kategorii:
/categories/2/items/5
Skąd takie podejście? Metoda HEAD nie służy do pobierania zasobu jako takiego, ale jest wykorzystywana do pobrania nagłówków, które zostaną przesłane, jeśli później
wykonamy metodę GET na tym samym URL-u.
Czyli metoda ta działa na zasadzie:
"Przetestuj mi zasób, a ja zdecyduję, czy wysłać po niego żądanie metodą GET"
W ten sposób możemy dowiedzieć się na przykład, jaki jest rozmiar zasobu, który chcemy pobrać (nagłówek
Content-Length).
To może być dla nas ważne, jeśli z jakiegoś powodu nie chcemy pobierać danych większych niż zakładany przez nas rozmiar.
Metoda HEAD posiada specyficzną własność. W trakcie jej użycia niedozwolone jest
przekazywanie body żądania.
-
PUT - Przesyła dane w celu modyfikacji zasobu, a więc realizuje operację typu Update (CRUD):
Modyfikacja wybranego itema: /items/5
Body: {"id": 5, "name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola
Modyfikacja wybranego itema poprzez modyfikację jego *zasobu podrzędnego: /items/5/name
Body: "New name"
Modyfikacja wybranego itema z danej kategorii: /categories/2/items/5
Body: {"id": 5, "name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola
Modyfikacja wybranego itema z danej kategorii poprzez modyfikację jego zasobu podrzędnego: /categories/2/items/5/name
Body: "New name"
* Pole itema name traktujemy jako zasób.
Przy takim podejściu każde kolejne pole musi mieć również swojego dedykowanego URL-a, co delikanie mówiąc nie jest najlepszym pomysłem.
Takie rozwiązanie stosujemy tylko w uzasadnionych przypadkach (o tym nieco dalej).
Metoda PUT posiada pewną własność, co razem z metodą GET i HEAD czyni je
wyjątkowymi. Metody te są idempotentne. Oznacza to, że nawet kilkukrotne wykonanie tej
samej metody dla identycznych danych przekazywanych w żądaniu doprowadzi do osiągnięcia tego
samego rezultatu, np. pobranie zasobu metodą GET zawsze zwróci dokładnie taki sam zasób (o
ile się nie zmienił po stronie źródła danych), a kilkukrotne wykonanie metody PUT zawsze
pozostawi po sobie zasób z takimi samymi danymi jak po pierwszym wykonaniu.
-
PATCH - Przesyła dane w celu modyfikacji części zasobu, a więc także realizuje operację typu Update (CRUD):
Modyfikacja wybranego itema: /items/5
Body: {"name": "Some name"} <--- wybrane pola
Modyfikacja wybranego itema z danej kategorii: /categories/2/items/5
Body: {"name": "Some name"} <--- wybrane pola
Metoda PATCH jest mało popularna, ponieważ ludzie z natury bywają leniwi i nie chcą sobie zbytnio komplikować życia.
Skoro PUT służy do update'u, to czy to ma znaczenie, że aktualizujemy całość obiektu, czy też tylko fragment (na przykład pole ze statusem)?
W teorii nie ma to wielkiego znaczenia, ale warto trzymać porządek i jednak podkreślić rodzajem metody,
że jest ona przeznaczona tylko do częściowego update'u.
-
POST - Przesyła dane w celu stworzenia zasobu, a więc realizuje operację typu Create (CRUD):
Stworzenie itema: /items
Body: {"name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola
Stworzenie itema w ramach wybranej kategorii: /categories/2/items
Body: {"name": "New name", "comment": "Twój komentarz", ...} <--- wszystkie pola
Metoda jest co prawda używana do tworzenia zasobu, ale w specyficznych przypadkach nic nie
stoi na przeszkodzie, aby służyła również do jego modyfikacji. Pamiętajmy jednak, że z
założenia metoda ta nie zapewnia nam idempotentności. Stąd jeśli potrzebujemy wykonać
modyfikację danych, która po każdym wykonaniu tego samego żądania ma zmienić stan zasobu, to
wówczas POST nada się do tego doskonale.
-
DELETE - Umożliwia usunięcie zasobu, a więc realizuje operację typu Delete (CRUD):
Usunięcie wybranego itema: /items/5
Usunięcie wybranego itema z danej kategorii: /categories/2/items/5
Alternatywnie, kiedy chcemy odwołać się do pola jako do zasobu (zasobu podrzędnego):
Usunięcie zasobu podrzędnego (name) z wybranego itema: /items/5/name
Usunięcie zasobu podrzędnego (name) wybranego itema z danej kategorii: /categories/2/items/5/name
Wtedy w kodzie będziemy po prostu czyścić wartość w danym polu.
Metoda DELETE jest idempotentna.
-
OPTIONS - Umożliwia wyświetlenie informacji o dozwolonych opcjach komunikacji dla podanego adresu URL lub serwera
Metoda ta jest specyficzna, ponieważ w ogóle nie odnosi sie do zasobu.
Podajemy ją tutaj dla porządku, ponieważ też jest metodą HTTP i jest przydatna
w kontekście komunikacji opartej o REST. Niektóre przeglądarki wysyłają żądanie typu OPTIONS
przed właściwym żądaniem, gdyż w ten sposób mogą sprawdzić czy dany serwer jest odpowiednio skonfigurowany.
Jest to szczególnie ważne w kontekście zabezpieczeń. W ten sposób przeglądarka może się dowiedzieć
czy dany serwer akceptuje na przykład CORS, czyli Cross-origin resourse sharing).
Metoda OPTIONS jest idempotentna.
Przykład odpowiedzi:
Tak to wygląda, gdy realizujemy typowe operacje CRUD-owe. Sprawa nieco się komplikuje, gdy nasze żądanie ma zrealizować coś
mniej szablonowego. Na przykład, jeśli chcemy uruchomić żądaniem proces usuwania pliku na serwerze,
wówczas możemy przekształcić uruchamianie akcji usuwania w nazwę pola zasobu.
Zamieniamy nazwę "starting" ("uruchamianie") na udawane pole boolean
started w wyimaginowanym zasobie
files
i updateujemy je za pomocą metody PATCH:
PATCH /files/7
Body:
{"started": true}
Możemy też taką akcję potraktować jako modyfikację zasobu z zasobem podrzędnym, czyli w naszym przykładzie uruchomienie potraktujemy
jako zasób
started, który tutaj jest zasobem podrzędnym
files.
Wtedy uruchomienie będzie realizowane metodą PUT (skoro
started jest zasobem, to podmieniamy całość zasobu - nieważne że to de facto prosta wartość):
PUT /files/7/started
Body:
true
Klasycznym problemem jest również implementacja operacji wyszukiwania zwracającego wynik w postaci wielu różnych zasobów.
W takich i podobnych sytuacjach dopuszcza się użycie prostego zapisu nieukierunkowanego na zasób:
/search
Warto to jednak opatrzyć (w kodzie lub dokumentacji) stosownym komentarzem.
Mapowanie zawartości żądania do obiektu Java
Tak pokazaliśmy w poprzednim punkcie, przesyłanie danych czasem wykonujemy w ramach URL, a czasem w body żądania. W tym drugim przypadku
dane są przesyłane w formacie zgodnym z określonym typem zawartości (content-type). Lista dostępnych typów
zawartości jest długa, ale najczęściej używane są:
-
Format JSON:
application/json
-
Format obiektu w wielu częściach, np. plik użytkownika:
multipart
W przypadku aplikacji webowych, na samym końcu po
otrzymaniu żądania przez serwer, następuje mapowanie pól z requestu na pola w obiekcie. W Springu wydarzy się to automatycznie dzięki
mechanizmom frameworka (piszemy o tym w Kursie Springa w rozdziale
Spring MVC - Metody obslugi zadan HTTP (Handler Methods),
a dokładniej - w punkcie opisującym adnotację
@RequestBody).
Przyjęcie danych przez serwer i rozpoczęcie przetwarzania
Po wykonaniu mapowania danych z żądania serwer rozpoczyna uruchamianie mechanizmów do obsługi tych
danych, w szczególności ich przetworzenia (używamy do tego osobnej warstwy kodu
), co zwykle kończy się wykonaniem operacji typu CRUD.
Odczyt lub zapis w źródle danych
Powyższe operacje są wykonywane na dowolnym źródle danych. Najczęściej jest to baza danych ale może
to być też zwykły plik, bądź usługa danych taka jak webservice (typu REST lub SOAP). Pamiętajmy, że
przetwarzanie danych może być także związane tylko pośrednio z operacjami CRUD, np. odczyt danych
może nastąpić na początku procesu, a dalej odbędzie się ich przetworzenie za pomocą dowolnego
algorytmu i dopiero tak przygotowane dane będą gotowe do wykorzystania. Kombinacje są tutaj dowolne
i zależą od wymagań biznesowych.
Zakończenie przetwarzania danych
Wynikiem przetworzenia danych może być tylko finalizacja wykonania konkretnych algorytmów albo też,
dodatkowo, zwrócenie danych przez warstwę przetwarzającą do warstwy wyżej, w celu dalszego ich
wykorzystania (najczęściej wysłania ich w ramach odpowiedzi).
Wysłanie odpowiedzi do przeglądarki
Po przetworzeniu danych proces wraca ze wspomnianą odpowiedzią (HTTP response) z serwera do
przeglądarki. Pamiętajmy, że w zależności od typu operacji (algorytmów przetwarzających) wykonanej
na serwerze odpowiedź może zawierać dane lub nie. Przykładowo, gdy wykonujemy metodę GET w celu
pobrania danych, wówczas w odpowiedzi dostaniemy właśnie te dane. W przypadku gdy tworzymy nowy
zasób, wykonując metodę POST, otrzymamy jego id, a gdy wykonamy metodę DELETE, odpowiedź będzie pusta.
Jeśli dane są przekazywane, to zostaną one przepisane z obiektu do odpowiedzi HTTP.
Każda odpowiedź HTTP zawiera status odpowiedzi. Jest
to uniwersalny kod, który zawsze oznacza coś konkretnego. Kodów jest wiele, ale można wśród nich wydzielić
kilka podstawowych grup:
- 1xx - Kody informacyjne
- 2xx - Kody powodzenia
- 3xx - Kody przekierowania
- 4xx - Kody błędów klienta
- 5xx - Kody błędów serwera
Najpopularniejsze i najczęściej używane kody to:
- 200 (OK) - operacja na zasobie przebiegła pomyślnie
- 201 (Created) - utworzono nowy zasób (np. po wywołaniu metody POST)
- 204 (No content) - serwer wykonał operacje pomyślnie, ale nie zwraca danych
- 304 (Not modified) - zasób nie podlegał zmianie według warunku określonego w żądaniu
- 400 (Bad Request) - zapytanie o zasób zostało błędnie skonfigurowane (np. błędnie podano
parametru w URL)
- 401 (Unauthorized) - nieautoryzowany dostęp do zasobu (wymaga uwierzytelnienia)
- 404 (Not Found) - zasób nie został odnaleziony
- 405 (Method Not Allowed) - podana metoda (GET, POST, etc.) nie jest dozwolna dla danego
zasobu
- 500 (Internal Server Error) - wewnętrzny błąd serwera, niespodziewane zachwianie poprawnego
wykonania procesu
Podobnie jak w przypadku żądania, tak samo w
przypadku odpowiedzi ustawiamy typ zawartości. Najczęściej spotykanymi typami są:
-
Format JSON:
application/json
-
Format danych graficznych:
image/png
,
image/jpeg
-
Format HTML:
text/html
-
Format tekstu:
text/plain
-
Format arkusza stylów:
text/css
Odebranie odpowiedzi w przeglądarce
Kolejnym etapem po przesłaniu odpowiedzi jest jej odebranie w przeglądarce za pomocą kodu JavaScript
(w dedykowanej warstwie), a następnie udostępnienie tej odpowiedzi w celu przygotowania widoku.
Formatowanie i prezentacja
Na samym końcu, po odebraniu danych, są one odpowiednio formatowanie i pokazywane użytkownikowi w
przeglądarce przy pomocy zdefiniowanego layoutu.
REST w aplikacji webowej na bazie Springa
Kontroler Springa wystawia adresy URL związane z konkretnymi metodami Java.
Mówimy, że wystawia on REST API. W aplikacji webowej mamy wiele takich kontrolerów. Każdy z nich jest dedykowany konkretnemu zagadnieniu aplikacji.
I tak na przykład zarządzanie użytkownikami jest zwykle realizowane przez
UserController albo
UserApi.
Wszystko zależy od przyjętej konwencji nazewniczej.
Przyjęło się, że w ramach REST API url-e zaczynają się od
/api. Dalej można albo podać
od razu nazwę podmiotu:
/users, albo też po drodze użyć jeszcze numeru wersji (jeśli przewidujemy kilka różnych wersji,
zaimplementowanych na różne sposoby):
/v1. Całość będzie zatem może przyjmować takie formy:
<<PROTOKÓŁ>>://<<HOST>>/api/users
<<PROTOKÓŁ>>://<<HOST>>/api/v1/users
Appa Notka.
Właśnie tak tworzymy REST API w ramach Kursu Aplikacji Webowej.
W kursie każdy scenariusz (na przykład rejestracja użytkownika, czy też dodawanie itemów) jest realizowany
w oparciu o dedykowane kontrolery restowe takie jak UserController, ItemController, CategoryController itp.
Zobacz Starter projektu aplikacji,
aby dowiedzieć się więcej.