GraphQL to język zapytań dla interfejsów programowania aplikacji oraz środowisko uruchomieniowe po stronie serwera, które pozwala klientom prosić dokładnie o te dane, których w danej chwili potrzebują. Główna różnica między GraphQL a REST API polega na tym, że REST opiera się na wielu sztywnych adresach URL zwanych endpointami, podczas gdy GraphQL wysyła wszystkie zapytania do jednego punktu, oddając pełną kontrolę nad strukturą odpowiedzi aplikacji klienckiej.
Zrozumieliśmy to boleśnie. Wdrażaliśmy nową aplikację do obsługi wniosków dla lokalnego urzędu na Mokotowie. Architektura klient-serwer oparta na REST dławiła się przy każdym odświeżeniu widoku. Mieliśmy widok profilu mieszkańca. Żeby go wygenerować, aplikacja mobilna musiała uderzyć w endpoint z danymi użytkownika, potem w endpoint z jego historią logowań, a na koniec odpytać bazę o statusy pism. Trzy osobne żądania HTTP. Każde zwracało ogromny payload JSON. Większość tych danych po prostu odrzucaliśmy na froncie. Zmiana na GraphQL obcięła nam transfer z 120 kilobajtów do niecałych 14 w jednym zapytaniu. Tyle. Żadnej magii.
Dlaczego w ogóle musieliśmy szukać alternatywy dla REST?
Przez lata wszyscy budowaliśmy systemy oparte o standardy RESTful. Wchodzisz w projekt, definiujesz zasoby, przypisujesz im czasowniki GET, POST, PUT, DELETE. Wydaje się to sensowne na początku pracy. Problem pojawia się w momencie, gdy interfejs użytkownika staje się skomplikowany. Aplikacje rosną. Wymagania biznesowe zmieniają się co tydzień. REST po prostu WYMUSZA na tobie tworzenie kolejnych, specjalistycznych endpointów typu /api/users/123/posts/comments/recent. Baza kodu puchnie. Back-end developerzy spędzają godziny na dopisywaniu kontrolerów pod konkretne widoki w aplikacji mobilnej. Prawda jest zresztą absolutnie taka, że utrzymanie tego chaosu w ryzach przy zespole liczącym więcej niż dziesięć osób to koszmar.
GraphQL odwraca ten model. Zamiast budować osobny adres dla każdego zasobu, stawiamy jeden główny węzeł. Często jest to po prostu /graphql. Klient wysyła tam zapytanie tekstowe, definiując dokładnie strukturę oczekiwanego JSON-a. Serwer to analizuje, uruchamia odpowiednie funkcje zwane resolverami i skleja odpowiedź. Ty decydujesz, czy chcesz samo imię użytkownika, czy od razu jego imię, nazwisko i dwa ostatnie komentarze. Zmiana wymagań na froncie nie wymaga już angażowania ludzi od serwera by dopisali nowy routing. Front-end modyfikuje swoje zapytanie. Gotowe.
Czym różni się over-fetching od under-fetchingu na produkcji?
To są dwa główne nowotwory klasycznego REST API. Zaczynamy od over-fetchingu. Pobierasz listę artykułów na stronę główną bloga. Potrzebujesz tylko tytułu i miniatury. Uderzasz w endpoint /articles. Serwer zwraca ci tytuł, miniaturę, ale też pełną treść artykułu w HTML, datę edycji, ID autora i listę tagów. Pobierasz megabajty danych przez sieć komórkową, by wyświetlić dwa paski tekstu. Telefony użytkowników mielą to w pamięci. Zjadamy baterię i transfer.
Under-fetching to sytuacja odwrotna. Masz endpoint /orders/99. Zwraca on kwotę zamówienia i ID klienta. Ale na ekranie musisz pokazać też adres wysyłki. Musisz więc wykonać kolejne zapytanie pod /customers/ID. Czekasz na pierwszą odpowiedź, wyciągasz z niej identyfikator, robisz drugie zapytanie. Mamy tu tak zwane N+1 problemów. Przy słabym zasięgu sieci powstają lagi niszczące doświadczenie użytkownika. W GraphQL rozwiązujesz oba te problemy jednym, dobrze sformułowanym żądaniem. Wskazujesz relacje między typami i silnik sam wędruje po grafie danych.
Jak dokładnie działa zapytanie w GraphQL i czym jest schemat?
Fundamentem każdej instalacji GraphQL jest schemat (schema). To twardy kontrakt między front-endem a back-endem. Definiujemy go w języku SDL (Schema Definition Language). Zapisujemy tam wszystkie dostępne typy danych. Określamy, że Typ Użytkownik ma pole ID w formacie String, a Typ Post ma pole Treść.
Zapytania dzielimy grubo na dwie kategorie. Oczywiście istnieją też subskrypcje oparte na WebSocketach do obsługi danych w czasie rzeczywistym, ale w codziennej pracy korzystamy z zapytań (Queries) do pobierania danych oraz mutacji (Mutations) do ich modyfikacji. Kiedy wysyłasz Query, serwer parsuje ten tekst. Buduje drzewo wykonania (Abstract Syntax Tree). Następnie dla każdego pola w zapytaniu wywołuje resolver. Resolver to po prostu funkcja w kodzie. Idzie do bazy PostgreSQL, uderza do zewnętrznego mikroserwisu albo czyta plik z dysku. Silnik GraphQL czeka na zakończenie wszystkich resolverów. Skleja to w jeden obiekt. Odrzuca błędy autoryzacji. Zwraca czysty payload do klienta.
W ubiegły wtorek siedzieliśmy nad poprawką do systemu rezerwacji do 3 nad ranem. Zdropowaliśmy bazę na środowisku testowym bo resolvery zaczęły zwracać nulle w zagnieżdżonych zapytaniach przy obciążeniu. Ktoś zapomniał o implementacji DataLoaderów do batchowania zapytań SQL. Silnik GraphQL próbował pobrać autora dla każdego z tysiąca postów osobnym zapytaniem SELECT * FROM users WHERE id = X. Położyliśmy instancję chmurową w pięć minut. To pokazuje, że elastyczność dla klienta oznacza przerzucenie całej złożoności obliczeniowej na serwer. Trzeba to kontrolować.
Kiedy REST API wciąż wygrywa z nowymi rozwiązaniami?
Mamy tendencję do zachłystywania się nowymi technologiami. Środowisko programistów JavaScript wręcz nałogowo wciska Apollo Server do każdego projektu. Piszemy proste CRUD-y (Create, Read, Update, Delete) dla małej bazy towarów w magazynie. Wrzucanie tam GraphQL to czysty przerost formy nad treścią. REST API ma twarde, niepodważalne zalety, które w wielu scenariuszach deklasują podejście grafowe.
| Cecha systemu | REST API | GraphQL |
| Struktura adresacji | Wiele dedykowanych endpointów URL | Jeden pojedynczy endpoint |
| Mechanizmy HTTP | Używa GET, POST, PUT, DELETE | Zazwyczaj tylko POST (zapytania w body) |
| Cachowanie w przeglądarce | Natywne, opadające na nagłówkach HTTP | Bardzo trudne, wymaga własnych mechanizmów |
| Rozmiar odpowiedzi | Sztywny, często zawiera zbędne dane | Elastyczny, dokładnie taki jak żądanie |
REST opiera się na standardach sieciowych, które budowano od dekad. Wykorzystuje kody odpowiedzi HTTP. Jeśli zasobu nie ma, dostajesz 404. Jeśli nie masz uprawnień, leci 403. W GraphQL wszystko idzie przez POST i zazwyczaj zwraca kod 200 OK, nawet jeśli w środku payloadu znajduje się wielka tablica z błędami (errors array). Musisz parsować ciało odpowiedzi by dowiedzieć się, co poszło nie tak. To łamie zasady monitoringu sieci. Zdecydowana większość starych systemów logowania ruchu, load balancerów i firewalli aplikacyjnych (WAF) kompletnie głupieje widząc ruch GraphQL. Dla nich to tylko ciągły strumień zapytań POST na jeden adres.
(Nawiasem mówiąc, próba wdrożenia agresywnego cachowania na poziomie CDN dla GraphQL to droga przez mękę. Pamiętam jak walczyliśmy z Varnish Cache. REST robi to z pudełka. Wpisujesz Cache-Control w nagłówek i przeglądarka wie co robić. W GraphQL każda zmienna w zapytaniu zmienia jego strukturę. Musisz wdrażać narzędzia typu Persisted Queries, haszować zapytania na froncie i wysyłać je jako GET, by w ogóle zacząć rozmawiać o sensownym buforowaniu. Większość firm po prostu z tego rezygnuje i uderza do bazy przy każdym kliknięciu. Koszty infrastruktury rosną).
Kwestie bezpieczeństwa i ataki na serwer
W architekturze REST kontrola dostępu jest prosta. Endpoint /admin/users jest chroniony na poziomie routera. W GraphQL klient ma dostęp do całego grafu. Możesz napisać zapytanie, które prosi o użytkownika, jego znajomych, znajomych znajomych i tak w nieskończoność, tworząc głęboką pętlę. Taki złośliwy payload (tzw. Deep Query Attack) potrafi zablokować wątek serwera Node.js i odciąć usługę. Musisz ręcznie implementować limity głębokości zapytań (Query Depth Limiting) oraz analize złożoności (Query Complexity Analysis). Wymaga to pisania własnych reguł przed wykonaniem resolverów.
Chociaż prawdę mówiąc brakuje nam twardych statystyk z ataków na polskim rynku z zeszłego miesiąca, więc to chyba tylko jedna z hipotez na najbliższe lata. My w każdym razie blokujemy zapytania głębsze niż cztery poziomy zagnieżdżenia. Klienci mobilni i tak nie potrzebują więcej. Webhooki zostawiamy na osobnym, klasycznym REST API bo łatwiej to spiąć z zewnętrznymi dostawcami płatności. Stripe czy PayPal i tak wyślą ci POST-a na konkretny adres, nie będą budować zapytań w SDL.
Jak migrować architekturę z REST na GraphQL bez przestojów?
Nikt nie przepisuje dużego systemu od zera. Zastosowaliśmy wzorzec fasady. Zostawiliśmy stare mikroserwisy REST w spokoju. Przed nimi postawiliśmy bramę GraphQL (Gateway). Kiedy aplikacja mobilna wysyła zapytanie w nowym standardzie, brama tłumaczy to na kilka starych strzałów REST-owych pod spodem. Zbieramy dane, filtrujemy i oddajemy do klienta. Front-end widzi nowoczesne API. Back-endowcy dalej utrzymują stary kod dopóki nie przepiszemy poszczególnych modułów. Taka migracja zajęła nam cztery miesiące na jednym z projektów e-commerce.
Dobrym pomysłem jest powolne przełączanie widoków. Zaczynasz od strony profilu użytkownika. Potem przenosisz koszyk. Utrzymujesz obie warstwy równolegle. Problemem staje się podwójne utrzymanie dokumentacji. W REST mieliśmy wygenerowanego Swaggera. GraphQL dokumentuje się sam na podstawie schematu, co jest ogromną zaletą. Introspekcja pozwala narzędziom typu GraphiQL wygenerować pełne środowisko do testowania z autouzupełnianiem kodu bezpośrednio w przeglądarce.
Zróbcie audyt endpointów na swoich serwerach do piątku i zobaczcie ile megabajtów pustych JSON-ów wysyłacie w próżnię. Reszta to wymówki.
Najczęściej zadawane pytania (FAQ)
-
Co to jest GraphQL?
GraphQL to język zapytań dla API i środowisko uruchomieniowe. Pozwala klientom na zdefiniowanie dokładnej struktury danych, które chcą pobrać z serwera, eliminując problem pobierania zbyt dużej lub zbyt małej ilości informacji. -
Czym głównie różni się GraphQL od REST API?
REST API opiera się na wielu endpointach URL dostarczających z góry określoną strukturę danych. GraphQL wykorzystuje jeden endpoint, przez który klient wysyła elastyczne zapytania określające kształt oczekiwanej odpowiedzi. -
Co to jest over-fetching?
Over-fetching to pobieranie z serwera większej ilości danych, niż aplikacja potrzebuje do wyrenderowania widoku. Obciąża to niepotrzebnie sieć i pamięć urządzenia klienta. -
Jak GraphQL rozwiązuje problem under-fetchingu?
Under-fetching zmusza do wysyłania wielu zapytań HTTP w architekturze REST. GraphQL pozwala na zagnieżdżanie relacji w jednym zapytaniu, pobierając powiązane zasoby w jednym żądaniu sieciowym. -
Czy GraphQL zastąpi całkowicie REST API?
Nie. REST API nadal wygrywa w prostych aplikacjach CRUD, komunikacji między mikroserwisami, obsłudze webhooków oraz tam, gdzie wymagane jest agresywne cachowanie na poziomie protokołu HTTP. -
Czym są mutacje w GraphQL?
Mutacje to odpowiednik metod POST, PUT, DELETE z architektury REST. Służą do modyfikowania danych na serwerze (np. dodawania użytkowników) i zwracają zaktualizowany stan obiektu w odpowiedzi.
Bibliografia
1. Fundacja GraphQL – https://graphql.org
2. Apollo GraphQL – https://www.apollographql.com
3. Mozilla Developer Network – https://developer.mozilla.org
4. Red Hat – https://www.redhat.com
5. IBM – https://www.ibm.com
