Przykładowa implementacja GraphQL z wykorzystaniem nodejs, expressjs i mongodb

2
388
views

Po artykułach mocno teoretycznych dotyczących GraphQL (wstęp i definicja schematu) czas przejść do czegoś konkretniejszego i przedstawić jakąś przykładową implementację z wykorzystaniem tego rozwiązania. Co prawda na tym blogu pojawiały się już opisy użycia GraphQLa w aplikacji PHPowej w „zapiskach z pola bitwy” podczas powstawania projektu Krauza, ale chciałbym uporządkować temat i przedstawić implementację z wykorzystaniem najlepszej biblioteki do GraphQL, którą stworzył sam Facebook

Na tapet zabierzemy przykłady zawarte w poprzednich wpisach teoretycznych czyli pozostaniemy w tematyce aplikacji przechowującej informacje o książkach i ich autorach. Ten post będzie pierwszym z serii opisujących implementację API z wykorzystaniem GraphQL. Dlatego dzisiaj można spodziewać się podstaw związanych z rozstawieniem projektu.

Wybór technologii

GraphQL to tylko specyfikacja i istnieje do niej pełno implementacji w praktycznie każdej technologii, która tego wymaga. Ale większość z nich to biblioteki opensource tworzone przez społeczność, czasem tylko przez jedną osobę i nie są to na tyle pewne rozwiązania aby brać je pod uwagę podczas użycia w projekcie. Moje pierwsze zetknięcie z biblioteką do PHP trochę mnie do niej zniechęciło bo często zmieniały się kluczowe elementy API (wiem, że to wersja rozwojowa, ale mimo wszystko trochę demotywujące).

Dlatego postanowiłem stworzyć testowy projekt (sandbox) wykorzystujący GraphQL w oparciu o najlepszą bibliotekę, która powstała dla nodejs i została stworzona przez samego Facebooka jako implementacja referencyjna do specyfikacji.

Do przechowywania danych wybrałem MongoDB ze względu na to, że bardzo dobrze współdziała z nodejs i można znaleźć dobre wsparcie chociażby w heroku jeśli chciałbym opublikować ten sandbox. Dodatkowo dla łatwiejszego modelowania dokumentów wybrałem bibliotekę Mongoose.

Oczywiście do serwowania HTTP bez niespodzianek – expressjs, który jest wciąż najpopularniejszy w świecie nodejs.

Implementacja

Po stronie bazy danych

Zacznijmy od zadeklarowania struktury dokumentów jakie będą przechowywane w bazie danych. W pierwszej iteracji nie będzie to nic skomplikowanego bo w API spodziewamy się tylko autorów i książek z relacją jeden do wielu (w celu uproszczenia na tę chwilę nie wnikamy, że jakaś książka może mieć wielu autorów i zakładamy, że książka ma jednego autora, za to autor może posiadać wiele książek).

models/author.js

models/book.js

Schemat GraphQL

Dobra, mamy jakieś założenie co do przechowywanej struktury dokumentów w bazie danych, to teraz można odwzorować ją w naszym API oraz zastanowić się na jakie akcje chcemy pozwolić użytkownikom. W tym podrozdziale nie będę się zagłębiać w tłumaczenie poszczególnych elementów bo jak już wspominałem – wstęp teoretyczny pojawił się na blogu wcześniej i jeszcze raz zachęcam do zobaczenia posta związanego z definiowaniem schematu.

Type

Na początek zadeklarujmy najważniejsze byty w GraphQL – ObjectType, które będą definiować pola encji Book i Author.

graphql/type/bookType.js

graphql/type/authorType.js

Query

Mając już zadeklarowane pierwsze typy można od razu zdefiniować QueryType, który jest obowiązkowym elementem w schemacie GraphQL.

graphql/root/queryType.js

Powyżej zadeklarowane zostały 4 możliwe akcje do wykonania w naszym API:

  • pobranie listy wszystkich autorów
  • pobranie informacji o jednym autorze na podstawie ID
  • pobranie wszystkich książek
  • pobranie informacji o książce na podstawie ID

Jak można zauważyć, w polach author i book zdefiniowane są argumenty, w obu wypadkach oczekiwany jest tylko jeden argument: id (args: { id: { type: GraphQLID } }).

Mutation + Input

Powyżej zadeklarowane akcje jako Query pozwalają tylko na odczyt danych, w aplikacjach jednak bardzo często trzeba dodawać lub modyfikować dane. Tutaj z pomocą przychodzi typ Mutation, który wygląda i działa bardzo podobnie do Query. Na początek zróbmy najprostszy przykład i zezwólmy na dodawanie nowych książek i autorów (bez ich edytowania).

graphql/root/mutationType.js

Jak widać w powyższym przykładzie w obu przypadkach oczekujemy argumentów, na podstawie których będzie można utworzyć nowe encje w naszej bazie danych. Pojawiły się nowe elementy o nazwie authorInput i bookInput – są to definicje typu Input.

graphql/input/authorInput.js

graphql/input/bookInput.js

Schemat

Ostatni element, który pozwala spiąć całe API do kupy to definicja schematu, którą ostatecznie będzie trzeba wpiąć w nasz serwer HTTP (w tym wypadku z frameworkiem expressjs).

graphql/schema.js

W 17 linii można ustawić czy chcemy uruchomić dla naszego API aplikację graphiql, która pozwala na przyjemne zarządzanie i eksplorowanie API.

Ostatnim krokiem jest tylko ustawienie definicji schematu jako middleware w aplikacji expressowej.

index.js

Akcje – resolver

Wróćmy jeszcze do definiowania typów Query i Mutation, podczas definiowania elementów API pojawiały się tam takie słowa kluczowe jak resolve i kierowały one do innych funkcji. To tak naprawdę powołuje do prawdziwego życia API oparte o GraphQL. Co nam po zdefiniowaniu schematu, jeśli nic się nie dzieje? To właśnie w miejscu resolve należy wykonać operacje, które zwrócą odpowiednie wyniki dla naszego API.

Na tę chwilę postanowiłem stworzyć kontrolery, które grupują akcje danego typu (booksController i authorsController) i wygląda to mniej więcej tak:

controllers/authorsController.js

controllers/booksController.js

Podsumowanie

Chciałbym aby dzisiejszy post był wstępem do ciekawszych omówień pracy ze standardem GraphQL, które mam w planach napisać w przyszłości. Mam nadzieję, że ten post mimo iż nie zawiera skomplikowanych rozwiązań okaże się pomocny i będzie traktowany jako podstawa do późniejszych postów. Cały kod źródłowy dostępny jest na repozytorium graphql-nodejs-sandbox w serwisie GitHub.

Dzisiejszy post jest pierwszą iteracją projekciku, który powstaje przy okazji aby lepiej obrazować działanie GraphQLa dlatego też nie wiem jeszcze w jakim kierunku pójdą zmiany, na dzisiaj jest to bardzo proste i minimalistyczne rozwiązanie, ale być może w przyszłości zostaną wprowadzone zmiany, które będą się znacząco różniły od stanu bieżącego, dlatego postanowiłem wprowadzić TAGa 1.0.0 w repo aby zachować aktualny stan.