Wyślij zapytanie Dołącz do Sii

Serwer GraphQL używa schema do opisania struktury dostępnych danych, definiując w ten sposób hierarchię typów i pól oraz zapytania i mutacje, które klient może wywołać. W niniejszym artykule opiszę kilka dobrych praktyk, jakimi powinni kierować się twórcy serwera GraphQL dla zachowania dobrej jakości produktu.

Jeśli zastanawiasz się, czym jest GraphQL i dlaczego warto z niego korzystać, sięgnij do mojego poprzedniego wpisu.

Zezwalanie na nulle w wartościach pól

W schemacie GraphQL pola są domyślnie „nullable” – warto więc oznaczać je jako non-nullable. Podobną strukturę danych można odzwierciedlić na frontendzie przy pomocy Typescriptu i tym samym zaoszczędzić sprawdzania, czy jakieś pole jest nullem czy nie.

Trzeba jednak pamiętać, że jeśli serwer GraphQL jest czymś podobnym do API gateway, czyli nie operuje tylko na danych ze znanej nam bazy, ale odpytuje np.: inne mikroserwisy lub ściąga dane z legacy systemów, wtedy warto się zastanowić nad wyżej opisaną koncepcją. Może się okazać, że jeśli po drodze jakiś serwis zwróci null lub nie odpowie, wtedy GraphQL API zwróci błąd.

Nulle i listy

Listę wartości możemy zdefiniować na 4 sposoby. Dobrą praktyką jest, aby zespół przyjął na początku projektu standard, jakim się posługuje w przypadku pracy z listami.

Możliwe podejścia:

  1. zezwolenie na null w liście i na to, aby zamiast listy pojawił się null
type User {
    …
    posts: [Post]
}
  1. ograniczenie, aby lista zawsze się pojawiła – może być pusta, ale pole nie może mieć wartości null
type User {
    …
    posts: [Post]!
}
  1. pozwolenie na null zamiast listy, ale wartości wewnątrz listy nie mogą przyjąć wartości null
type User {
    …
    posts: [Post!]
}
  1. kombinacja sposobu 2 i 3 – brak pozwolenia na null w liście oraz null zamiast listy
type User {
    …
    posts: [Post!]!
}

Wsteczna kompatybilność i problem z non-nullable fields

Problem pojawia się głównie przy aplikacjach mobilnych, gdzie nie możemy wymusić na kliencie pobrania aktualizacji aplikacji. Z tego powodu trzeba utrzymać wsteczną kompatybilność, aby starsze wersje aplikacji działały poprawnie.

Jeśli dodamy do API pole, które nie może być nullem, a użytkownik nie ma zaktualizowanej wersji aplikacji, wtedy mutacja będzie próbowała dodać nowy rekord bez nowego pola. W wyniku działania API zwróci klientowi error.

Z przejściem w drugą stronę również może pojawić się problem. Szczególnie, kiedy zmieniamy pole z non-nullable na takie, które może przyjąć null, a jest to typ obiektowy (np. User), w którym, aby otrzymać jego dane, należy odpytać serwer o składową obiektu. Jeśli User nagle okaże się nullem, wtedy również API zwróci błąd.

Obiekty a typy proste

Czasem, kiedy zmieniają się założenia systemu i zachodzi potrzeba przechowania większej ilości danych, pojawia się problem. Dla przykładu: zamiast samego url zdjęcia należy przechować też atrybut alt. Zatem nastąpiła konieczność zmiany z pola imageUrl zwracającego String na obiekt przechowujący dwie dane: url oraz alt.

fragmenty kodu

Jeśli taka zmiana zostanie wprowadzona na serwerze, to istnieje duże ryzyko, że klient, który nie zmienił jeszcze swojego kodu, otrzyma błąd w odpowiedzi z API. Na pomoc przychodzi oznaczenie starego pola jako deprecated:

type Post {
    id: ID!
    title: String!
    imageUrl: String @deprecated(reason: "This field is deprecated and will be removed on Jun 11, 2023. Use `image` field instead.")
    image: {
        url: String!
        Alt: String!
    }
}

W tym wypadku pole imageUrl będzie można usunąć z określoną datą lub wtedy, kiedy zajdzie pewność, że nikt z niego nie korzysta.

Najlepszym wyjściem byłoby jednak przewidzieć już na początku, że zdjęcie powinno mieć też opis. Dlatego właśnie należy dobrze przemyśleć schemat i w razie wątpliwości używać tablic lub obiektów. Obiekty lepiej wpisują się w ewolucyjność schematu i dzięki nim łatwiej zmieniać API.

Projektowanie mutacji

Dobrą praktyką jest zwracanie zawsze modyfikowanego lub dodanego obiektu zamiast np.: stringa „OK”. Unika się wtedy dodatkowego zapytania o wynik mutacji i odświeża się cache apollo klienta na frontendzie. Ewentualnie, można ten typ zagnieździć oraz zwrócić zarówno typ jak i status jako obiekt.

Jeśli chodzi o mutacje, dobrą praktyką jest też niezmienianie wszystkich danych jednocześnie w jednej mutacji, ale robienie tego przy pomocy mniejszych mutacji. Dla przykładu: update osoby można rozdzielić na update danych podstawowych i osobne dodanie postu czy zdjęcia.

Zagnieżdżanie obiektów

Przy tworzeniu schematu warto pamiętać o tym, w jaki sposób powinny zagnieżdżać obiekty w GraphQL. Należy unikać podejścia znanego z baz danych i tworzenia pól z wartością klucza obcego np.:

type Post {
    id: ID!
    title: String!
    authorId: ID!
}

Taki schemat skutkowałby tym, że frontend nie mógłby zapytać od razu o dane autora – authorId musiałoby zostać przekazane do kolejnego zapytania GraphQL. Tym samym uzyskujemy wynik podobny do zapytań REST API i rezygnujemy z zalety GraphQL, jaką niewątpliwie jest zapytanie o wszystkie dane za pomocą jednego requestu.

Poprawny schemat powinien wyglądać tak:

type Post {
    id: ID!
    title: String!
    author: User!
}

Filtrowanie na serwerze

Aby stworzyć dobrze działającą i wydajną aplikację GraphQL, należy zwrócić uwagę na to, żeby filtrowanie i wyszukiwanie były obsłużone po stronie serwera. Powinno się unikać wykonywania operacji na danych po stronie klienta. W ten sposób nie dopuścimy do sytuacji, kiedy każda z aplikacji używających danego API (np.: aplikacja webowa i aplikacja mobilna) musi powielać kod, gdyż to serwer powinien dostarczać gotowy zestaw danych.

Przykład prostego resolvera filtrującego dane:

const resolvers = {
  Query: {
    …,
    post: (parent, args, context, info) => usersList.filter((post) => post.type == args.type),
  },
  …
}

Ograniczanie głębokości zapytań

Zapytania GraphQL mają jedną sporą wadę – mogą być zagnieżdżane w nieskończoność. Zwykle w schemacie GraphQL dane rekursywnie się do siebie odwołują – użytkownicy mają listy postów, posty mają twórców i listę komentujących w postaci użytkowników. Teoretycznie, można zatem stworzyć zapytanie, które będzie miało bardzo dużą liczbę zagnieżdżeń, a następnie wykorzystać je np.: przy ataku hakerskim.

Przykładowy pseudokod:

- pobierz wszystkich użytkowników komentujących dany post
- dla każdego z pobranych użytkowników pobierz skomentowane przez niego posty
- dla każdego z pobranych postów pobierz użytkowników komentujących ten post
- dla każdego z pobranych użytkowników pobierz skomentowane przez niego posty
- itd

Jedną z możliwości, by zniwelować ten problem, jest ustawienie w konfiguracji serwera opcji, która mówi GraphQL, żeby odrzucił każde zapytanie, które zawiera większą głębokość rekurencyjną niż ustawiona.

Przykładowo, następujące zapytanie ma głębokość 2:

Query {
  user(id: $user_id) { 	# depth 1
    name 		# depth 2
  }
}

Ustawienie NODE_ENV w Apollo Server

Ostatnią, bardzo ważną rzeczą, o której chcę wspomnieć, jest wyłączenie możliwości podglądu schematu na produkcji. Ciekawą opcją serwera GraphQL jest to, że udostępnia tzw. sandbox (do wersji 3 był to playground), gdzie można poznać:

  • aktualnie dostępny schemat,
  • dokumentację dla typów,
  • dostępne zapytania,
  • subskrypcje,
  • mutacje itd.

Opcja ta od wersji 3 jest domyślnie wyłączona dla środowiska produkcyjnego, ale dobrze jest upewnić się, czy na pewno nikt niepożądany nie jest w stanie zobaczyć struktury udostępnianego schematu.

Podsumowanie

Mam nadzieję, że dobre praktyki, które Wam przybliżyłam, będą dla Was wsparciem podczas korzystania z GraphQL, a kierując się nimi, zachowacie najwyższą jakość produktu.

***

Jeśli interesuje Cię temat GraphQL, sprawdź inne artykuły naszych ekspertów, w których temat został wspomniany.

5/5 ( głosy: 2)
Ocena:
5/5 ( głosy: 2)
Autor
Avatar
Beata Możdżeń

Software developer z ponad 15-letnim doświadczeniem. Swoją przygodę z programowaniem rozpoczęła jako backend developer. Od kilku lat zajmuje się frontendem. W projekcie jest człowiekiem orkiestrą, a oprócz programowania lubi angażować się w organizację i koordynowanie pracy zespołu. Prywatnie uwielbia twórczość Agathy Christie, poznawanie tajników zdrowego odżywiania i grę na gitarze

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?