GraphQL vs. REST — samouczek GraphQL
Opublikowany: 2022-03-11Być może słyszałeś o nowym dzieciaku w okolicy: GraphQL. Jeśli nie, to GraphQL jest jednym słowem nowym sposobem pobierania API, alternatywą dla REST. Zaczęło się jako wewnętrzny projekt na Facebooku, a ponieważ był open source, zyskał dużą popularność.
Celem tego artykułu jest pomoc w łatwym przejściu z REST na GraphQL, niezależnie od tego, czy już zdecydowałeś się na GraphQL, czy po prostu chcesz go wypróbować. Nie jest wymagana żadna wcześniejsza znajomość GraphQL, ale wymagana jest pewna znajomość interfejsów API REST, aby zrozumieć artykuł.
Pierwsza część artykułu rozpocznie się od podania trzech powodów, dla których osobiście uważam, że GraphQL jest lepszy od REST. Druga część to samouczek dotyczący dodawania punktu końcowego GraphQL na swoim zapleczu.
Graphql vs. REST: Po co odrzucać REST?
Jeśli nadal wahasz się, czy GraphQL jest odpowiedni dla Twoich potrzeb, tutaj znajduje się dość obszerny i obiektywny przegląd „REST vs. GraphQL”. Jednak z trzech głównych powodów, dla których warto korzystać z GraphQL, czytaj dalej.
Powód 1: Wydajność sieci
Załóżmy, że masz na zapleczu zasób użytkownika z imieniem, nazwiskiem, adresem e-mail i 10 innymi polami. Na kliencie zazwyczaj potrzebujesz tylko kilku z nich.
Wykonanie wywołania REST na punkcie końcowym /users
przywraca wszystkie pola użytkownika, a klient używa tylko tych, których potrzebuje. Oczywiście istnieje pewna strata transferu danych, która może być brana pod uwagę w przypadku klientów mobilnych.
GraphQL domyślnie pobiera najmniejsze możliwe dane. Jeśli potrzebujesz tylko imion i nazwisk użytkowników, określ to w zapytaniu.
Poniższy interfejs nazywa się GraphiQL, co jest jak eksplorator API dla GraphQL. Na potrzeby tego artykułu stworzyłem mały projekt. Kod jest hostowany na GitHub i zajmiemy się nim w drugiej części.
W lewym okienku interfejsu znajduje się zapytanie. Tutaj pobieramy wszystkich użytkowników — zrobilibyśmy GET /users
z REST — i pobieramy tylko ich imiona i nazwiska.
Zapytanie
query { users { firstname lastname } }
Wynik
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
Gdybyśmy chcieli również otrzymywać e-maile, dodanie wiersza „e-mail” poniżej „nazwiska” załatwiłoby sprawę.
Niektóre zaplecza REST oferują opcje, takie jak /users?fields=firstname,lastname
aby zwrócić częściowe zasoby. Za to, co jest warte, Google to poleca. Jednak nie jest to domyślnie zaimplementowane i sprawia, że żądanie jest ledwo czytelne, zwłaszcza gdy dodasz inne parametry zapytania:
-
&status=active
, aby filtrować aktywnych użytkowników -
&sort=createdAat
, aby posortować użytkowników według daty ich utworzenia -
&sortDirection=desc
, ponieważ oczywiście tego potrzebujesz -
&include=projects
aby uwzględnić projekty użytkowników
Te parametry zapytania są poprawkami dodanymi do interfejsu API REST w celu imitowania języka zapytań. GraphQL to przede wszystkim język zapytań, który sprawia, że zapytania są zwięzłe i precyzyjne od samego początku.
Powód 2: Wybór projektu „Uwzględnij a punkt końcowy”
Wyobraźmy sobie, że chcemy zbudować proste narzędzie do zarządzania projektami. Mamy trzy zasoby: użytkowników, projekty i zadania. Definiujemy również następujące relacje między zasobami:
Oto niektóre z punktów końcowych, które udostępniamy światu:
Punkt końcowy | Opis |
---|---|
GET /users | Lista wszystkich użytkowników |
GET /users/:id | Zdobądź pojedynczego użytkownika o identyfikatorze :id |
GET /users/:id/projects | Pobierz wszystkie projekty jednego użytkownika |
Punkty końcowe są proste, łatwe do odczytania i dobrze zorganizowane.
Sprawy stają się trudniejsze, gdy nasze żądania stają się bardziej złożone. Weźmy punkt końcowy GET /users/:id/projects
: powiedzmy, że chcę wyświetlać tylko tytuły projektów na stronie głównej, ale projekty+zadania na pulpicie nawigacyjnym, bez wykonywania wielu wywołań REST. Zadzwonię:
-
GET /users/:id/projects
dla strony głównej. -
GET /users/:id/projects?include=tasks
(na przykład) na stronie pulpitu nawigacyjnego, aby zaplecze dołączało wszystkie powiązane zadania.
Powszechną praktyką jest dodawanie parametrów zapytania ?include=...
, aby to zadziałało, i jest to nawet zalecane przez specyfikację interfejsu API JSON. Parametry zapytania, takie jak ?include=tasks
są nadal czytelne, ale niedługo otrzymamy ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
.
Czy w takim przypadku rozsądniej byłoby utworzyć punkt końcowy /projects
aby to zrobić? Coś w stylu /projects?userId=:id&include=tasks
, ponieważ mielibyśmy o jeden poziom relacji mniej do uwzględnienia? A właściwie punkt końcowy /tasks?userId=:id
też może działać. Może to być trudny wybór projektowy, jeszcze bardziej skomplikowany, jeśli mamy relację wiele do wielu.
GraphQL wszędzie używa podejścia include
. Dzięki temu składnia do pobierania relacji jest potężna i spójna.
Oto przykład pobierania wszystkich projektów i zadań od użytkownika o identyfikatorze 1.
Zapytanie
{ user(id: 1) { projects { name tasks { description } } } }
Wynik
{ "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }
Jak widać, składnia zapytania jest łatwa do odczytania. Gdybyśmy chcieli zagłębić się w zadania, komentarze, zdjęcia i autorów, nie zastanawialibyśmy się dwa razy nad tym, jak zorganizować nasze API. GraphQL ułatwia pobieranie złożonych obiektów.
Powód 3: Zarządzanie różnymi typami klientów
Tworząc back-end, zawsze zaczynamy od tego, aby API było jak najszerzej wykorzystywane przez wszystkich klientów. Jednak klienci zawsze chcą dzwonić mniej i brać więcej. Dzięki głębokim dołączeniom, częściowym zasobom i filtrowaniu żądania wysyłane przez klientów internetowych i mobilnych mogą się znacznie różnić od siebie.
W przypadku REST istnieje kilka rozwiązań. Możemy utworzyć niestandardowy punkt końcowy (tj. alias punktu końcowego, np. /mobile_user
), niestandardową reprezentację ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
), a nawet klienta -specyficzne API (jak kiedyś Netflix). Wszystkie trzy wymagają dodatkowego wysiłku ze strony zespołu programistów zaplecza.
GraphQL daje większe możliwości klientowi. Jeśli klient potrzebuje złożonych żądań, sam zbuduje odpowiednie zapytania. Każdy klient może inaczej korzystać z tego samego interfejsu API.
Jak zacząć z GraphQL
W większości dzisiejszych debat na temat „GraphQL vs. REST” ludzie myślą, że muszą wybrać jedną z dwóch. To po prostu nieprawda.
Nowoczesne aplikacje zazwyczaj korzystają z kilku różnych usług, które udostępniają kilka interfejsów API. Moglibyśmy właściwie myśleć o GraphQL jako o bramie lub opakowaniu do wszystkich tych usług. Wszyscy klienci trafiliby na punkt końcowy GraphQL, a ten punkt końcowy trafiłby do warstwy bazy danych, usługi zewnętrznej, takiej jak ElasticSearch lub Sendgrid, lub innych punktów końcowych REST.
Drugim sposobem użycia obu jest posiadanie oddzielnego punktu końcowego /graphql
w interfejsie API REST. Jest to szczególnie przydatne, jeśli masz już wielu klientów korzystających z interfejsu API REST, ale chcesz wypróbować GraphQL bez naruszania istniejącej infrastruktury. I to jest rozwiązanie, które dzisiaj badamy.
Jak wspomniano wcześniej, zilustruję ten samouczek małym przykładowym projektem dostępnym na GitHub. Jest to uproszczone narzędzie do zarządzania projektami, zawierające użytkowników, projekty i zadania.
Technologie użyte w tym projekcie to Node.js i Express dla serwera WWW, SQLite jako relacyjna baza danych oraz Sequelize jako ORM. Trzy modele — użytkownik, projekt i zadanie — są zdefiniowane w folderze models
. Punkty końcowe REST /api/users
, /api/projects
i /api/tasks
są udostępniane światu i są zdefiniowane w folderze rest
.

Należy pamiętać, że GraphQL można zainstalować na dowolnym typie zaplecza i bazy danych, przy użyciu dowolnego języka programowania. Zastosowane tu technologie zostały wybrane ze względu na prostotę i czytelność.
Naszym celem jest utworzenie punktu końcowego /graphql
bez usuwania punktów końcowych REST. Punkt końcowy GraphQL trafi bezpośrednio do ORM bazy danych w celu pobrania danych, dzięki czemu będzie całkowicie niezależny od logiki REST.
Rodzaje
Model danych jest reprezentowany w GraphQL przez typy , które są silnie typizowane. Powinno istnieć mapowanie 1 do 1 między Twoimi modelami a typami GraphQL. Nasz typ User
to:
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
Zapytania
Zapytania definiują, jakie zapytania możesz uruchomić w swoim GraphQL API. Zgodnie z konwencją powinien istnieć RootQuery
, który zawiera wszystkie istniejące zapytania. Wskazałem również na odpowiednik REST każdego zapytania:
type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }
Mutacje
Jeśli zapytania są żądaniami GET
, mutacje mogą być postrzegane jako POST
/ PATCH
/ PUT
/ DELETE
(choć tak naprawdę są to zsynchronizowane wersje zapytań).
Zgodnie z konwencją, wszystkie nasze mutacje umieszczamy w RootMutation
:
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }
Zauważ, że wprowadziliśmy tutaj nowe typy o UserInput
, ProjectInput
i TaskInput
. Jest to również powszechna praktyka w przypadku REST, aby utworzyć model danych wejściowych do tworzenia i aktualizowania zasobów. Tutaj nasz typ UserInput
jest naszym typem User
bez pól id
i projects
i zwróć uwagę na słowo kluczowe input
zamiast type
:
input UserInput { firstname: String lastname: String email: String }
Schemat
Za pomocą typów, zapytań i mutacji definiujemy schemat GraphQL , który jest tym, co punkt końcowy GraphQL uwidacznia światu:
schema { query: RootQuery mutation: RootMutation }
Ten schemat jest mocno typowany i to pozwoliło nam mieć te przydatne autouzupełnienia w GraphiQL.
Resolwery
Teraz, gdy mamy już publiczny schemat, nadszedł czas, aby powiedzieć GraphQL, co zrobić, gdy wymagane jest każde z tych zapytań/mutacji. Resolvery wykonują ciężką pracę; mogą na przykład:
- Uderz w wewnętrzny punkt końcowy REST
- Zadzwoń do mikroserwisu
- Uderz w warstwę bazy danych, aby wykonać operacje CRUD
W naszej przykładowej aplikacji wybieramy trzecią opcję. Rzućmy okiem na nasz plik przeliczników:
const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }
Oznacza to, że jeśli zapytanie user(id: ID!)
jest żądane na GraphQL, zwracamy z bazy danych User.findById()
, który jest funkcją Sequelize ORM.
A co z dołączeniem do wniosku innych modeli? Cóż, musimy zdefiniować więcej przeliczników:
User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */
Więc kiedy zażądamy pola projects
w typie User
w GraphQL, to złącze zostanie dołączone do zapytania bazy danych.
I wreszcie, resolwery dla mutacji:
RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }
Możesz się z tym pobawić tutaj. W trosce o utrzymanie danych na serwerze w czystości wyłączyłem resolwery dla mutacji, co oznacza, że mutacje nie wykonają żadnych operacji tworzenia, aktualizowania ani usuwania w bazie danych (a tym samym zwrócą null
na interfejsie).
Zapytanie
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
Wynik
{ "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }
Przepisanie wszystkich typów, zapytań i tłumaczeń dla istniejącej aplikacji może zająć trochę czasu. Istnieje jednak wiele narzędzi, które mogą Ci pomóc. Na przykład istnieją narzędzia, które tłumaczą schemat SQL na schemat GraphQL, w tym resolwery!
Składanie wszystkiego razem
Przy dobrze zdefiniowanym schemacie i programach rozpoznawania nazw dotyczących tego, co zrobić z każdym zapytaniem schematu, możemy zamontować punkt końcowy /graphql
na naszym zapleczu:
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
I możemy mieć ładnie wyglądający interfejs GraphiQL na naszym zapleczu. Aby złożyć żądanie bez GraphiQL, po prostu skopiuj adres URL żądania i uruchom go za pomocą cURL, AJAX lub bezpośrednio w przeglądarce. Oczywiście istnieje kilka klientów GraphQL, które pomogą Ci zbudować te zapytania. Zobacz poniżej kilka przykładów.
Co dalej?
Celem tego artykułu jest pokazanie, jak wygląda GraphQL i pokazanie, że zdecydowanie można wypróbować GraphQL bez wyrzucania infrastruktury REST. Najlepszym sposobem sprawdzenia, czy GraphQL odpowiada Twoim potrzebom, jest wypróbowanie go samodzielnie. Mam nadzieję, że ten artykuł skłoni Cię do nurkowania.
Istnieje wiele funkcji, o których nie omówiliśmy w tym artykule, takich jak aktualizacje w czasie rzeczywistym, przetwarzanie wsadowe po stronie serwera, uwierzytelnianie, autoryzacja, buforowanie po stronie klienta, przesyłanie plików itp. Doskonałe źródło informacji o tych funkcjach jest Jak GraphQL.
Poniżej znajdują się inne przydatne zasoby:
Narzędzie po stronie serwera | Opis |
---|---|
graphql-js | Referencyjna implementacja GraphQL. Możesz go użyć z express-graphql do stworzenia serwera. |
graphql-server | Wszechstronny serwer GraphQL stworzony przez zespół Apollo. |
Wdrożenia na inne platformy | Rubin, PHP itp. |
Narzędzie po stronie klienta | Opis |
---|---|
Przekaźnik | Framework do łączenia Reacta z GraphQL. |
apollo-klient. | Klient GraphQL z powiązaniami dla React, Angular 2 i innych frameworków front-end. |
Podsumowując, uważam, że GraphQL to coś więcej niż szum. Nie zastąpi ona jeszcze jutro REST, ale oferuje wydajne rozwiązanie prawdziwego problemu. Jest stosunkowo nowa, a najlepsze praktyki wciąż się rozwijają, ale z pewnością jest to technologia, o której usłyszymy w ciągu najbliższych kilku lat.