GraphQL vs. REST – Ein GraphQL-Tutorial
Veröffentlicht: 2022-03-11Sie haben vielleicht schon von dem neuen Kind um den Block gehört: GraphQL. Wenn nicht, ist GraphQL, kurz gesagt, eine neue Möglichkeit, APIs abzurufen, eine Alternative zu REST. Es begann als internes Projekt bei Facebook, und seit es Open Source ist, hat es viel Anklang gefunden.
Das Ziel dieses Artikels ist es, Ihnen bei einem einfachen Übergang von REST zu GraphQL zu helfen, unabhängig davon, ob Sie sich bereits für GraphQL entschieden haben oder es einfach nur ausprobieren möchten. Es sind keine Vorkenntnisse in GraphQL erforderlich, aber eine gewisse Vertrautheit mit REST-APIs ist erforderlich, um den Artikel zu verstehen.
Der erste Teil des Artikels beginnt mit der Nennung von drei Gründen, warum ich persönlich denke, dass GraphQL REST überlegen ist. Der zweite Teil ist ein Tutorial zum Hinzufügen eines GraphQL-Endpunkts zu Ihrem Back-End.
Graphql vs. REST: Warum REST fallen lassen?
Wenn Sie noch zögern, ob GraphQL für Ihre Bedürfnisse geeignet ist, finden Sie hier einen recht ausführlichen und objektiven Überblick über „REST vs. GraphQL“. Lesen Sie jedoch weiter, um meine drei wichtigsten Gründe für die Verwendung von GraphQL zu erfahren.
Grund 1: Netzwerkleistung
Angenommen, Sie haben eine Benutzerressource im Back-End mit Vorname, Nachname, E-Mail und 10 weiteren Feldern. Auf dem Client benötigen Sie im Allgemeinen nur ein paar davon.
Durch einen REST-Aufruf am /users
-Endpunkt erhalten Sie alle Felder des Benutzers zurück, und der Client verwendet nur die, die er benötigt. Es gibt eindeutig eine gewisse Datenübertragungsverschwendung, was bei mobilen Clients eine Überlegung sein könnte.
GraphQL ruft standardmäßig die kleinstmöglichen Daten ab. Wenn Sie nur den Vor- und Nachnamen Ihrer Benutzer benötigen, geben Sie dies in Ihrer Abfrage an.
Die folgende Schnittstelle heißt GraphiQL, was wie ein API-Explorer für GraphQL ist. Ich habe ein kleines Projekt für den Zweck dieses Artikels erstellt. Der Code wird auf GitHub gehostet, und wir werden im zweiten Teil darauf eingehen.
Im linken Bereich der Benutzeroberfläche befindet sich die Abfrage. Hier rufen wir alle Benutzer ab – wir würden GET /users
mit REST ausführen – und erhalten nur ihre Vor- und Nachnamen.
Anfrage
query { users { firstname lastname } }
Ergebnis
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
Wenn wir auch die E-Mails erhalten wollten, würde das Hinzufügen einer „E-Mail“-Zeile unter „Nachname“ ausreichen.
Einige REST-Backends bieten Optionen wie /users?fields=firstname,lastname
an, um Teilressourcen zurückzugeben. Für das, was es wert ist, empfiehlt Google es. Es ist jedoch nicht standardmäßig implementiert und macht die Anfrage kaum lesbar, insbesondere wenn Sie andere Abfrageparameter einwerfen:
-
&status=active
, um aktive Benutzer zu filtern -
&sort=createdAat
, um die Benutzer nach ihrem Erstellungsdatum zu sortieren -
&sortDirection=desc
, weil Sie es offensichtlich brauchen -
&include=projects
, um die Projekte der Benutzer einzuschließen
Diese Abfrageparameter sind Patches, die der REST-API hinzugefügt werden, um eine Abfragesprache zu imitieren. GraphQL ist vor allem eine Abfragesprache, die Anfragen von Anfang an prägnant und präzise macht.
Grund 2: Die Designentscheidung „Include vs. Endpoint“.
Stellen wir uns vor, wir wollen ein einfaches Projektmanagement-Tool bauen. Wir haben drei Ressourcen: Benutzer, Projekte und Aufgaben. Wir definieren auch die folgenden Beziehungen zwischen den Ressourcen:
Hier sind einige der Endpunkte, die wir der Welt präsentieren:
Endpunkt | Beschreibung |
---|---|
GET /users | Alle Benutzer auflisten |
GET /users/:id | Holen Sie sich den einzelnen Benutzer mit id :id |
GET /users/:id/projects | Holen Sie sich alle Projekte eines Benutzers |
Die Endpunkte sind einfach, leicht lesbar und gut organisiert.
Die Dinge werden kniffliger, wenn unsere Anfragen komplexer werden. Nehmen wir den Endpunkt GET /users/:id/projects
: Angenommen, ich möchte nur die Titel der Projekte auf der Startseite anzeigen, aber Projekte + Aufgaben auf dem Dashboard, ohne mehrere REST-Aufrufe durchzuführen. Ich würde anrufen:
-
GET /users/:id/projects
für die Homepage. -
GET /users/:id/projects?include=tasks
(zum Beispiel) auf der Dashboard-Seite, damit das Back-End alle zugehörigen Aufgaben anhängt.
Es ist üblich, Abfrageparameter ?include=...
hinzuzufügen, damit dies funktioniert, und wird sogar von der JSON-API-Spezifikation empfohlen. Abfrageparameter wie ?include=tasks
sind immer noch lesbar, aber bald werden wir bei ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
.
Wäre es in diesem Fall klüger, dafür einen /projects
-Endpunkt zu erstellen? Etwas wie /projects?userId=:id&include=tasks
, da wir eine Beziehungsebene weniger einbeziehen müssten? Oder ein /tasks?userId=:id
Endpunkt könnte auch funktionieren. Dies kann eine schwierige Designentscheidung sein, die noch komplizierter wird, wenn wir eine Viele-zu-Viele-Beziehung haben.
GraphQL verwendet überall den include
-Ansatz. Dies macht die Syntax zum Abrufen von Beziehungen leistungsstark und konsistent.
Hier ist ein Beispiel für das Abrufen aller Projekte und Aufgaben vom Benutzer mit der ID 1.
Anfrage
{ user(id: 1) { projects { name tasks { description } } } }
Ergebnis
{ "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" } ] } ] } } }
Wie Sie sehen können, ist die Abfragesyntax leicht lesbar. Wenn wir tiefer gehen und Aufgaben, Kommentare, Bilder und Autoren einbeziehen wollten, würden wir nicht zweimal darüber nachdenken, wie wir unsere API organisieren. GraphQL erleichtert das Abrufen komplexer Objekte.
Grund 3: Verwalten verschiedener Arten von Clients
Beim Aufbau eines Backends beginnen wir immer damit, die API so weit wie möglich für alle Kunden nutzbar zu machen. Dennoch möchten Kunden immer weniger anrufen und mehr abholen. Durch tiefe Einbeziehungen, teilweise Ressourcen und Filterung können sich Anfragen von Web- und mobilen Clients stark voneinander unterscheiden.
Bei REST gibt es mehrere Lösungen. Wir können einen benutzerdefinierten Endpunkt (d. h. einen Alias-Endpunkt, z. B. /mobile_user
), eine benutzerdefinierte Darstellung ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
) oder sogar einen Client erstellen -spezifische API (wie einst Netflix). Alle drei erfordern zusätzliche Anstrengungen des Back-End-Entwicklungsteams.
GraphQL gibt dem Client mehr Leistung. Wenn der Client komplexe Anforderungen benötigt, baut er die entsprechenden Abfragen selbst auf. Jeder Client kann dieselbe API unterschiedlich nutzen.
So starten Sie mit GraphQL
In den meisten Debatten über „GraphQL vs. REST“ denken die Leute heute, dass sie sich für eines der beiden entscheiden müssen. Das stimmt einfach nicht.
Moderne Anwendungen verwenden im Allgemeinen mehrere verschiedene Dienste, die mehrere APIs verfügbar machen. Wir könnten uns GraphQL tatsächlich als Gateway oder Wrapper für all diese Dienste vorstellen. Alle Clients würden auf den GraphQL-Endpunkt treffen, und dieser Endpunkt würde auf die Datenbankebene, einen externen Dienst wie ElasticSearch oder Sendgrid oder andere REST-Endpunkte treffen.
Eine zweite Möglichkeit, beide zu verwenden, besteht darin, einen separaten /graphql
Endpunkt auf Ihrer REST-API zu haben. Dies ist besonders nützlich, wenn Sie bereits zahlreiche Clients haben, die auf Ihre REST-API zugreifen, aber Sie GraphQL ausprobieren möchten, ohne die vorhandene Infrastruktur zu gefährden. Und das ist die Lösung, die wir heute erforschen.
Wie bereits erwähnt, werde ich dieses Tutorial mit einem kleinen Beispielprojekt veranschaulichen, das auf GitHub verfügbar ist. Es ist ein vereinfachtes Projektmanagement-Tool mit Benutzern, Projekten und Aufgaben.
Die für dieses Projekt verwendeten Technologien sind Node.js und Express für den Webserver, SQLite als relationale Datenbank und Sequelize als ORM. Die drei Modelle – Benutzer, Projekt und Aufgabe – werden im Ordner „ models
“ definiert. Die REST-Endpunkte /api/users
, /api/projects
und /api/tasks
sind für die Welt verfügbar und im Ordner rest
definiert.

Beachten Sie, dass GraphQL auf jeder Art von Back-End und Datenbank mit jeder Programmiersprache installiert werden kann. Die hier verwendeten Technologien sind aus Gründen der Einfachheit und Lesbarkeit gewählt.
Unser Ziel ist es, einen /graphql
Endpunkt zu erstellen, ohne die REST-Endpunkte zu entfernen. Der GraphQL-Endpunkt trifft direkt auf das Datenbank-ORM, um Daten abzurufen, sodass er völlig unabhängig von der REST-Logik ist.
Typen
Das Datenmodell wird in GraphQL durch Typen repräsentiert, die stark typisiert sind. Es sollte eine 1-zu-1-Zuordnung zwischen Ihren Modellen und GraphQL-Typen geben. Unser User
wäre:
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
Abfragen
Abfragen definieren, welche Abfragen Sie auf Ihrer GraphQL-API ausführen können. Per Konvention sollte es eine RootQuery
, die alle vorhandenen Abfragen enthält. Ich habe auch auf das REST-Äquivalent jeder Abfrage hingewiesen:
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 }
Mutationen
Wenn Abfragen GET
-Anforderungen sind, können Mutationen als POST
/ PATCH
/ PUT
/ DELETE
-Anforderungen angesehen werden (obwohl sie eigentlich synchronisierte Versionen von Abfragen sind).
Per Konvention fügen wir alle unsere Mutationen in eine RootMutation
ein:
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 }
Beachten Sie, dass wir hier neue Typen namens UserInput
, ProjectInput
und TaskInput
. Dies ist auch bei REST üblich, um ein Eingabedatenmodell zum Erstellen und Aktualisieren von Ressourcen zu erstellen. Hier ist unser UserInput
-Typ unser User
ohne die Felder id
und projects
, und beachten Sie das Schlüsselwort input
anstelle von type
:
input UserInput { firstname: String lastname: String email: String }
Schema
Mit Typen, Abfragen und Mutationen definieren wir das GraphQL-Schema , das der GraphQL-Endpunkt der Welt aussetzt:
schema { query: RootQuery mutation: RootMutation }
Dieses Schema ist stark typisiert und hat es uns ermöglicht, diese praktischen Autovervollständigungen in GraphiQL zu haben.
Resolver
Jetzt, da wir das öffentliche Schema haben, ist es an der Zeit, GraphQL mitzuteilen, was zu tun ist, wenn jede dieser Abfragen/Mutationen angefordert wird. Resolver erledigen die harte Arbeit; sie können zum beispiel:
- Treffen Sie einen internen REST-Endpunkt
- Rufen Sie einen Microservice an
- Greifen Sie auf die Datenbankebene zu, um CRUD-Operationen auszuführen
In unserer Beispiel-App wählen wir die dritte Option. Werfen wir einen Blick auf unsere Resolver-Datei:
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 }
Das heißt, wenn die user(id: ID!)
Abfrage auf GraphQL angefordert wird, geben wir User.findById()
, eine Sequelize-ORM-Funktion, aus der Datenbank zurück.
Was ist mit dem Beitritt zu anderen Modellen in der Anfrage? Nun, wir müssen mehr Resolver definieren:
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 } */
Wenn wir also das projects
in einem User
in GraphQL anfordern, wird dieser Join an die Datenbankabfrage angehängt.
Und schließlich Resolver für Mutationen:
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 }
Hier können Sie damit herumspielen. Um die Daten auf dem Server sauber zu halten, habe ich die Resolver für Mutationen deaktiviert, was bedeutet, dass die Mutationen keine Erstellungs-, Aktualisierungs- oder Löschvorgänge in der Datenbank ausführen (und daher null
auf der Schnittstelle zurückgeben).
Anfrage
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
Ergebnis
{ "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" } ] } ] } } }
Es kann einige Zeit dauern, alle Typen, Abfragen und Resolver für Ihre vorhandene App neu zu schreiben. Es gibt jedoch viele Tools, die Ihnen dabei helfen. Beispielsweise gibt es Tools, die ein SQL-Schema in ein GraphQL-Schema übersetzen, einschließlich Resolver!
Alles zusammenfügen
Mit einem gut definierten Schema und Resolvern, was bei jeder Abfrage des Schemas zu tun ist, können wir einen /graphql
-Endpunkt in unserem Back-End bereitstellen:
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
Und wir können eine gut aussehende GraphiQL-Oberfläche in unserem Back-End haben. Um eine Anfrage ohne GraphiQL zu stellen, kopieren Sie einfach die URL der Anfrage und führen Sie sie mit cURL, AJAX oder direkt im Browser aus. Natürlich gibt es einige GraphQL-Clients, die Ihnen beim Erstellen dieser Abfragen helfen. Siehe unten für einige Beispiele.
Was kommt als nächstes?
Das Ziel dieses Artikels ist es, Ihnen einen Vorgeschmack darauf zu geben, wie GraphQL aussieht, und Ihnen zu zeigen, dass es definitiv möglich ist, GraphQL auszuprobieren, ohne Ihre REST-Infrastruktur wegzuwerfen. Der beste Weg, um herauszufinden, ob GraphQL Ihren Anforderungen entspricht, ist, es selbst auszuprobieren. Ich hoffe, dass dieser Artikel Sie dazu bringt, den Sprung zu wagen.
Es gibt viele Funktionen, die wir in diesem Artikel nicht besprochen haben, wie z. B. Echtzeit-Updates, serverseitiges Batching, Authentifizierung, Autorisierung, clientseitiges Caching, Hochladen von Dateien usw. Eine hervorragende Ressource, um mehr über diese Funktionen zu erfahren ist How to GraphQL.
Nachfolgend finden Sie einige weitere nützliche Ressourcen:
Serverseitiges Tool | Beschreibung |
---|---|
graphql-js | Die Referenzimplementierung von GraphQL. Sie können es mit express-graphql , um einen Server zu erstellen. |
graphql-server | Ein vom Apollo-Team erstellter All-in-One-GraphQL-Server. |
Implementierungen für andere Plattformen | Ruby, PHP usw. |
Clientseitiges Tool | Beschreibung |
---|---|
Relais | Ein Framework zum Verbinden von React mit GraphQL. |
apollo-client. | Ein GraphQL-Client mit Bindungen für React, Angular 2 und andere Front-End-Frameworks. |
Zusammenfassend glaube ich, dass GraphQL mehr als ein Hype ist. Es wird REST morgen noch nicht ersetzen, aber es bietet eine performante Lösung für ein echtes Problem. Es ist relativ neu und Best Practices entwickeln sich noch, aber es ist definitiv eine Technologie, von der wir in den nächsten Jahren hören werden.