8 rzeczy, które warto wiedzieć projektując REST-owe API

Jakiś czas temu Microsoft opublikował na githubie “REST API Guidelines“. Reakcja Roya Fieldinga, a więc człowieka, który de facto opracował styl REST-owy była dość jednoznaczna:

fie

Z jednej strony REST to dziś dla wielu buzzword, oznaczający jedynie komunikację przez HTTP z użyciem kilku metod tegoż HTTP i wymiana danych w formacie JSON. Zintegrowano w ten sposób niejeden system i w wielu przypadkach to po prostu wystarczy.

Z drugiej strony, jeśli chcemy upublicznić swoje API i nazywamy je REST-owym, to warto wiedzieć trochę więcej. Nie zapominajmy jednak, że nie zdefiniowano nigdzie czegoś takiego jak REST RFC.Rozwiązania wielu problemów w sposób, który nazywamy REST-owym, pozostają jedynie interpretacjami tego, co Roy Fielding chciał przekazać w 2001 roku w swoim doktoracie.

Ten POST został wysłany do serwera wordpressa metodą POST i zawiera kilka praktycznych wskazówek, jak budować API w sposób REST-owy.

201 i Location jako odpowiedź na POST

POST jest z kilku względów wyjątkową metodą HTTP z punktu widzenia REST. Najpopularniejsze jej zastosowanie to dodanie zasobu do kolekcji. Co ważne, identyfikator takim zasobom nadaje serwer, a co za tym idzie, klient musi mieć możliwość przenawigowania do dodanego przez siebie zasobu. Dlatego jeśli żądanie przejdzie poprawnie walidację i zasób zostaje utworzony, to serwer musi zwrócić kod 201 / Created z adresem URI nowo utworzonego zasobu zamieszczonym w nagłówku Location. Przykładowa odpowiedź:

HTTP/1.1 201 Created
Location: http://localhost:5000/api/documents/7c65e898-eceb-42ec-a608-c02a1db4e781
Server: Kestrel

PUT vs POST czyli idempotentność

Dodawać zasoby możemy również za pomocą metody PUT. Różnica polega na tym, że żądanie PUT musi być idempotentne. Operacje idempotentne (w algebrze) to takie, które wielokrotnie stosowane nie zmienią wyniku. Zatem jedyną możliwość idempotentnego dodania zasobu jest wstawienie go pod Id nadany przez klienta. Każdy kolejny, taki sam request będzie traktowany jako aktualizacja zasobu znajdującego się pod wskazanym Id.

PUT http://localhost:5000/api/documents/7c65e898-eceb-42ec-a608-c02a1db4e781 HTTP/1.1
Content-type: application/json
Host: localhost:5000
Content-Length: 48

{
 "author": "John Doe",
 "content" : "..."
}

Konsekwencja jest taka, że jeżeli metodę PUT wywołamy 100 razy z takimi samymi nagłówkami i body, to w bazie danych będzie cały czas jeden zasób, natomiast w przypadku dodawania przez POST utworzyć się powinno 100 zasobów z różnymi Id.

PATCH czyli partial update

PUT służy do aktualizacji/zapisu zasobu, ale tylko w sytuacji, gdy klient przyśle cały zasób. Bardzo często gdy mamy do czynienia z dużymi zasobami i chcemy oszczędzić transferu danych interesuje nas aktualizacja tylko jednego pola. Możliwości są dwie. Pierwszą jest użycie metody PATCH. Zgodnie ze specyfikacją, jako body należy podać opis zmian dla danego zasobu. Można w tym celu wykorzystać dedykowany format application/merge-patch+json. Przykład zmiany tytułu dla zasobu dokumentu:

PATCH http://localhost:5000/api/documents/7c65e898-eceb-42ec-a608-c02a1db4e781 HTTP/1.1
Content-type: application/merge-patch+json
Content-Length: 30

{
 "title": "my new title"
}

Inny sposób to wydzielenie podzasobu (/api/documents/{documentId}/title) i wykonywanie na nim operacji zgodnych ze znanymi regułami REST.

HEAD czyli pobranie metadanych

Kolejną metodą HTTP, która może się przydać, gdy chcemy uniknąć niepotrzebnego transferu danych jest HEAD. Metoda ta ma zadanie bardzo podobne do GET-a, z tą różnicą, że nie jest zwracany message bod, a więc konwersacja odbywa się na poziomie nagłówków HTTP. Najczęstsze zastosowania to sprawdzenie, czy zasób istnieje lub czy w cache znajduje się ta sama wersja, którą ma klient. Przykład:

HEAD http://localhost:5000/api/documents/7c65e898-eceb-42ec-a608-c02a1db4e781 HTTP/1.1
Host: localhost:5000

HTTP/1.1 200 OK
Date: Sun, 09 Oct 2016 12:36:27 GMT
Server: Kestrel

POST gdy inne metody nie pasują

POST ma jedną bardzo ważną cechę – nie musi być idempotentny. Microsoft pisze w swoich wskazówkach, że jednym z zastosowań POST-a jest “submit a command”. Oznacza to mniej więcej tyle, że wszystkie operacje, które nie do końca pasują nam do innych metod HTTP powinniśmy wykonywać POST-em właśnie.

Czasami trzeba też dokonać wyboru pomiędzy GET-em i POST-em. Warto tutaj pamiętać, że GET jest dedykowany do operacji bezpiecznych. Dzisiejsze przeglądarki wykonują tzw.URL prefetching gdy użytkownik wejdzie na daną stronę, a to oznacza, że GET-y mogą być wykonywane bez jawnej akcji użytkownika. Dobrym przykładem dla GET vs POST jest akcja wylogowania.

craver

202 Accepted

Projektując API nie zawsze mamy możliwość tworzenia zasobów w sposób synchroniczny. Odpowiedzią serwera na niezakończone, asynchroniczne operacje powinien być kod 202 i URI do tymczasowego zasobu reprezentującego stan operacji. Przykładowa wymiana wiadomości:

POST http://localhost:5000/api/documents HTTP/1.1
Content-type: application/json
Host: localhost:5000
Content-Length: 47

{
 "author": "John Doe",
 "content" : "..."
}
HTTP/1.1 202 Accepted
Date: Sun, 09 Oct 2016 19:19:54 GMT
Location: api/documentsqueue/cf67eca7-e822-44bb-a185-bdc262fbe52b
Server: Kestrel

 HATEOAS

Jedną z trudniejszych do zrealizowania cech REST-owego API jest bezstanowość. Oczywiście pomiędzy zasobami występują relacje, natomiast klient powinien móc nawigować pomiędzy nimi za pomocą tzw. hypermedia. Zasada ta nazywana jest HATEOAS, czyli Hypermedia As The Engine Of Application State.

Przykładem naruszenia bezstanowości API jest paginacja kolekcji. Jeżeli realizujemy ją np. poprzez dwa parametry skip i take, to klient musi pomiędzy requestami pamiętać parametry z poprzedniego żądania i odpowiednio je modyfikować.

W odpowiedzi na HATEOAS powstało kilka niedużych standardów. Jednym z popularniejszych jest HAL (Hypermedia Application Language), który definiuje jasne reguły budowania hypermediów poprzez linki. Powstało też całkiem sporo bibliotek dla różnych technologii ułatwiających budowanie odpowiedzi zgodnie z HAL.

Przykładowa odpowiedź w formacie zgodnym z HAL:

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

{
    "_links": {
        "self": {
            "href": "http://localhost:5000/api/documents?page=3"
        },
        "prev": {
            "href": "http://localhost:5000/api/documents?page=2"
        },
        "next": {
            "href": "http://localhost:5000/api/documents?page=4"
        }
    },
    "count": 2,
    "total": 498,
    "_embedded": {
        "documents": [
            {
                "_links": {
                    "self": {
                        "href": "http://localhost:5000/api/documents/585755f0-3e80-465e-91c7-6f1847330afb"
                    }
                },
                "title": "My new document",
                "content": "..."
            },
            {
                "_links": {
                    "self": {
                        "href": "http://localhost:5000/api/documents/9a36f3a6-b2c8-451f-9f8c-f24bc96ac301"
                    }
                },
                "id": "Another document",
                "content": "..."
            }
        ]
    }
}

Tak więc oprócz spodziewanej kolekcji zasobów otrzymujemy kilka sekcji _links, umożliwiających nawigowanie do innych zasobów / kolekcji zasobów.

Resource expansion

Ciekawa idea podpatrzona na jednej z prezentacji. API wystawiające zasoby jest OK, ale w skrajnych przypadkach może się zdarzyć, że klient w jednej chwili potrzebuje zasób i kilka innych będących z nim w relacji. Jeżeli wystawiamy nasze zasoby w sposób “znormalizowany”, to efektem będzie wiele requestów do naszego serwera wysyłanych w jednym momencie.

Odpowiedzią na tego typu problemy jest resource expansion, czyli technika bardzo analogiczna do Eager Loadingu z ORM-ów. Odpytując o zasób jawnie podajemy, które relacje nas interesują i serwer załączy je wszystkie w jednej odpowiedzi.Przykładowe zapytanie

GET http://localhost:5000/api/documents/585755f0-3e80-465e-91c7-6f1847330afb?expand=attachments,comments

 

I na koniec bardzo dobry link,  wyjaśniający, jak dobrze wybrać odpowiedni kod HTTP dla naszego API.

Advertisements

4 thoughts on “8 rzeczy, które warto wiedzieć projektując REST-owe API

  1. REST super, ale kombinacja REST i DDD nie bardzo. Co o tym myslisz, masz z tym jakies doswiadczenie?

    Resource expansion – dalsza mozliwosc jest naprz. normalizowane API (backend) i klient dalej uzyje GraphQL u siebie (naprz. web server, sam definiuje strukture API).

    Like

    • DDD nie stosowałem nigdy w praktyce więc z oczywistych względów nie chcę się wypowiadać, natomiast zachęcam do odpowiedzi innych czytających.

      Co do resource expansion, to rzeczywiście widziałem gdzieś opinię, że GraphQL adresuje te problemy. W przypadku facebooka (który o ile dobrze pamiętam stoi za GQL), gdzie występuje mnóstwo relacji między zasobami, wydaje mi się, że po prostu doszli do ściany pod względem wydajnościowym już jakiś czas temu i stąd właśnie GQL. Nie wykluczam, że za jakiś czas pojawi się coś więcej o nim na blogu.

      Pozdrawiam i dzięki za komentarz.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s