Um GraphQL-APIs und die gleichnamige Abfragesprache ranken sich viele Mythen und Vorurteile. So wird sie von einigen als „SQL für das Frontend“ bezeichnet, die eine nicht zu bändigende Komplexität im Backend schaffe. Andere hingegen sehen sie als ernstzunehmende REST-Alternative und sind von Flexibilität und Typsicherheit begeistert. Dieser Artikel beantwortet die häufigsten Fragen der API-Technologie.
Was ist GraphQL?
GraphQL ist eine Sprache zur Abfrage von Daten. Ursprünglich wurde GraphQL von Facebook zum internen Einsatz in den eigenen Anwendungen konzipiert, dann als Open-Source-Lösung veröffentlicht, und seit Ende 2018 wird GraphQL von einem Konsortium – der GraphQL Foundation – weiterentwickelt.
Wichtig ist, dass GraphQL kein fertiges Produkt ist. Vielmehr handelt es sich dabei um eine Spezifikation, in der Syntax und Semantik der Abfragesprache beschrieben sind. Außerdem enthält die Spezifikation eine allgemeine Beschreibung, wie Server GraphQL-Anfragen bearbeiten und beantworten müssen.
Welches Problem will GraphQL lösen?
Mit einer GraphQL-Abfrage sind Clients grundsätzlich in der Lage, in einem Request genau die Daten abzufragen, die sie für einen Use-Case benötigen. Eine fiktive Kochrezepte-Anwendung könnte zum Beispiel Kochrezepte und dazugehörige Benutzer-Kommentare (Feedbacks) verwalten und per API zur Verfügung stellen.
Je nach Use-Case ist ein Client nun an unterschiedlichen Ausschnitten dieser Daten interessiert. Für die Landing-Page werden vielleicht nur Titel und Anzahl der Bewertungen der Rezepte benötigt, für die Rezept-Detailseite alle Informationen des Rezepts (Titel, Beschreibung, Zutaten, Kochzeit, etc.) und für die Feedback-Seite nur der Titel des Rezepts und die Kommentare aus den Feedbacks.
Für diese Ansichten könnte der Client jeweils eine GraphQL-Abfrage schicken und würde genau die dafür benötigten Daten bekommen. Bei Ressourcen-orientierten APIs wie REST könnte es sein, dass der Client zu viele Daten bekommt („overfetching“), also zum Beispiel alle Informationen eines Rezepts, auch wenn er nur dessen Titel benötigt. Oder er bekommt zu wenig (zum Beispiel keine Feedbacks), die dann in einem zweiten Request nachgeladen werden müssen („underfetching“). Diese Probleme lassen sich natürlich auch in anderen Technologien lösen (und sind auch in GraphQL nicht immer optimal gelöst), aber das ist die Grundidee von GraphQL.
Darüber hinaus zeichnen sich GraphQL-APIs durch ihre Typsicherheit aus. GraphQL-APIs werden mit einem Schema beschrieben, in dem genau festgelegt ist, welche Objekte es gibt und wie diese aussehen. Diese Informationen können Tools auswerten und damit Hilfe während der Entwicklung geben – auch ohne umfangreiche Dokumentation ist eine GraphQL-API daher gut zu verstehen bzw. zu erlernen. Zur Laufzeit nutzt GraphQL die Typinformationen, um sicherzustellen, dass Abfragen und Daten gemäß dem Schema gültig sind. Das macht die Arbeit mit GraphQL-APIs sehr komfortabel und auch sehr sicher.
Wie kann ich GraphQL einsetzen?
Wenn man eine GraphQL-API bereitstellen möchte, gibt es dazu zwei Wege. Zum einen gibt es einige Tools und Frameworks, die zum Beispiel für bestehende Datenbanken eine GraphQL generieren und bereitstellen können (wie z.B. PostGraphile). Diese Ansätze können insbesondere zum Kennenlernen von GraphQL verwendet werden.
Üblicherweise wird man eine GraphQL-API aber für seine eigene Anwendung selbst implementieren. Für fast alle populären Programmiersprachen gibt es Frameworks, die einem bei der Implementierung und Bereitstellung der API helfen. Und auch wenn diese sich natürlich in der Verwendung unterscheiden, sind deren grundlegende Konzepte immer identisch, da diese in der GraphQL-Bibliothek zumindest grob beschrieben sind.
GraphQL macht keine Aussage über den zu verwendenden Transportmechanismus, also auf welchem Weg ein Client seine Anfragen zum Server schickt. In vielen Fällen wird dazu HTTP verwendet, das ist aber nicht zwingend notwendig.
Welche Operationen kann eine GraphQL-API anbieten?
Abfragen, die ein Client an eine GraphQL-API sendet, werden in GraphQL Operationen genannt. Unterschieden werden dabei drei verschiedene Typen von Operation, die eine API anbieten kann:
- Query: Der sicherlich am häufigsten verwendete Operationstyp, mit dem Daten von einer Schnittstelle gelesen werden (vergleichbar mit HTTP GET Requests in einer REST-API).
- Mutation: Mit diesem Operationstyp können Clients Schreibzugriffe an den Server senden, um Daten anzulegen, zu verändern oder zu löschen (vorsichtig vergleichbar mit HTTP POST, PUT, PATCH und DELETE Operationen in einer REST API).
- Subscription: Hiermit können Clients auf Ereignisse vom Server lauschen, etwa wenn ein neues Objekt im Datenbestand angelegt oder verändert wurde.
Welche Operationen bzw. Operationstypen angeboten werden und wie diese konkret aussehen, legt die eine API jeweils individuell fest. Genau wie bei anderen API-Technologien auch, diktieren dabei die fachlichen Anforderungen das Aussehen und den Umfang der jeweiligen Schnittstelle.
Im Gegensatz zu manch anderslautender Aussage stellt GraphQL nicht pauschal alle Daten in „irgendeiner“ Form (zum Beispiel den kompletten Inhalt einer Datenbank) dem Client zur Verfügung.
Wie sieht eine GraphQL-API aus?
Der Name GraphQL leitet sich davon ab, dass mit der Abfragesprache Daten aus einem Objektgraphen abgefragt werden können. Folglich stellt eine GraphQL-API Objekte zur Verfügung, die in einem Schema beschrieben sind. Das Schema ist zwingend erforderlich bei einer GraphQL-API und legt fest, welche Objekte es gibt und wie diese aussehen, also welche Felder sie haben.
Für jedes Feld wird dabei nicht nur dessen Name, sondern auch dessen Typ (z.B. String, Boolean oder Int) festgelegt, denn GraphQL verfügt über ein eigenes Typsystem. Für jedes Feld kann außerdem angegeben werden, ob das Feld „null“ sein darf oder nicht. Außerdem kann ein Feld Argumente haben, die ebenfalls samt Typangabe im Schema beschrieben werden müssen. Felder mit Argumenten werden auch Methoden genannt, weil sie den klassischen Methoden in Programmiersprachen sehr ähnlich sehen.
Neben den fachlichen Objekten muss ein GraphQL-Schema die Beschreibung der Root-Typen enthalten. Dabei handelt es sich um spezielle Objekte, die den Einstieg in den Objektgraphen darstellen. Pro angebotenem Operationstyp (Query, Mutation, Subscription) gibt es jeweils einen eigenen Root-Typen. Eine GraphQL-Abfrage beginnt dann immer an einem der Root-Typen.
https://gist.github.com/nilshartmann/b47ea8290c7bdf92e97c493866780a3e
Das obige Listing zeigt eine einfache Schemabeschreibung mit der Schema Definition Language (SDL), die ebenfalls Teil der GraphQL-Spezifikation ist. Die beschriebene API stellt Queries und Mutations zur Verfügung, sowie die beiden Objekte „Recipe“ und „Feedback“. In dem Schema ist außerdem Dokumentation im Markdown-Format an Typen und Feldern hinterlegt.
Wichtig ist, dass die Beschreibung des Schemas nur eine Aufgabe bei der Umsetzung einer GraphQL-API ist. Im Backend muss danach implementiert werden, wie die Daten für die Anfrage eines Clients ermittelt und zurückgeliefert werden. Dazu bieten die GraphQL-Frameworks zwar Unterstützung, aber keine „Magie“.
Wie sieht die GraphQL-Abfragesprache aus?
Mit der Sprache lassen sich Abfragen formulieren, die aus dem bereitgestellten Objektgraphen eine Untermenge auswählen. Dabei muss eine Abfrage immer den vorgegebenen Pfaden durch den Graphen folgen und bei einem der Root-Typen beginnen. Dazu schreibt man die Felder aus einem Objekt hin, die man abfragen möchte. Handelt es sich bei einem Feld um ein Objekt bzw. eine Liste von Objekten (z.B. das Feld feedbacks), dann muss man wiederum hinschreiben, welche Felder man aus diesem Objekt selektieren möchte.
https://gist.github.com/nilshartmann/b05c8911317d6d4959e060c2c4b12f26
Das obige Listing zeigt, wie von der Recipe API ein Kochrezept über dessen Id abgefragt wird. Aus dem Kochrezept wird dann dessen Titel, die Kochzeit, sowie alle Feedbacks abgefragt und von denen jeweils der Kommentar. Anhand des query-Schlüsselwortes wird festgelegt, um welchen Operationstypen es sich handelt. Hier wäre alternativ auch mutation bzw. subscription möglich. Die Syntax bei diesen beiden Operationen wäre aber identisch, nur das Einstiegsobjekt ein anderes.
Die Antwort auf den Query ist in der Regel ein (JSON-)Objekt, das die ermittelten Daten enthält, wie im nächsten Listing. Die Struktur des Objektes entspricht dabei der Struktur der Abfrage. Der Client weiß also beim Formulieren seiner Operation, wie die Antwort strukturell aussieht.
https://gist.github.com/nilshartmann/e8f7c2a7472982ee85088f8ee6498f94
Welche Limitierung gibt es in der Abfragesprache?
GraphQL, bzw. dessen Abfragesprache, hat (insbesondere auch im Vergleich zu SQL) eine ganze Reihe von Einschränkungen. Wie erwähnt, können Clients nur den Pfaden durch den Objektgraphen folgen, also praktisch eine Untermenge an Daten auswählen. Andere Abfragen oder Verknüpfungen (wie zum Beispiel JOINs in SQL) sind nicht möglich.
Ein Client kann in der Recipe API zwar ein Rezept und dann dessen Feedbacks abfragen, aber zum Beispiel nicht direkt alle Feedbacks. Wären Recipe und Feedback jeweils Datenbank-Tabellen in einer SQL-Datenbank, könnte ein Client mittels SQL zwischen diesen beiden Tabellen beliebige Verknüpfungen mit JOINs erstellen.
Ebenso sieht GraphQL keine Möglichkeit von Haus aus vor, mit dem Daten gefiltert oder sortiert werden könnten. Konzepte wie „ORDER BY“, „OFFSET“ oder „LIMIT“ gibt es genausowenig wie ein Pendant zur „WHERE“-Klausel in SQL-Statements. Möchte man solche Features in der eigenen API anbieten, muss man dazu entsprechende Felder und Argumente im Schema definieren und dann im Backend natürlich auch implementieren.
Das Feld Query.recipes zeigt dazu ein sehr einfaches Beispiel. Hier wäre es dem Client möglich, zu bestimmen, wie viele Rezepte zurückgeliefert werden sollen. Allerdings gilt auch hier: Wie die entsprechenden Argumente heißen und wie diese backend-seitig verarbeitet werden, liegt in der Verantwortung des API-Anbieters und geschieht nicht durch eine GraphQL-„Magie“.
Auch wenn diese Einschränkungen zunächst irritieren mögen (auch weil sich aufgrund der Namen „GraphQL“ und „SQL“ Assoziationen zwischen beiden Technologien aufdrängen), haben sie auch Vorteile. Eine Anwendung bzw. eine API muss nur die Features anbieten (und implementieren!), die sie aus fachlicher Sicht auch benötigt und umsetzen kann.
Eine oft geäußerte Befürchtung, dass GraphQL-APIs in der Umsetzung langsam und kompliziert wären, ist somit zumindest nicht pauschal richtig, denn es hängt stark davon ab, wie komplex eine konkrete API ist. Außerdem kann die Art und Weise, wie z.B. Filtern und Sortieren angeboten wird, individuell und passend zur jeweiligen Fachlichkeit erfolgen, was sowohl Entwicklung als auch Verwendung der API vereinfachen kann.
Wie funktionieren GraphQL-Frameworks?
In der Spezifikation gibt es eine Empfehlung, wie Frameworks konzeptionell arbeiten sollten. Und dieser Empfehlung folgen auch alle bekannten Frameworks, so dass der grundsätzliche Ablauf der Abfrage-Verarbeitung in allen Frameworks gleich aussieht.
Zunächst erhält das Framework ein „GraphQL-Dokument“, in dem eine oder mehrere Operationen beschrieben sind. Auf welchem Transportweg das geschieht, ist in GraphQL nicht festgelegt, aber häufig kommt HTTP zum Einsatz. Das GraphQL-Framework validiert das Dokument hinsichtlich der Syntax und des Schemas. Bei Fehlern wird die Verarbeitung abgelehnt und der Client bekommt eine Fehlermeldung. Gültige Operationen werden dann zur Verarbeitung weitergegeben.
Dabei ruft das GraphQL-Framework für jedes Feld, das in einer Abfrage vorkommt, eine Funktion auf. Diese Funktionen werden häufig als „Resolver“- oder „Data-Fetcher“-Funktionen bezeichnet und müssen von der Anwendung implementiert werden. Sie sind dazu da, die eigentlichen Daten zu ermitteln. Für den Beispiel-Query würde also eine Funktion aufgerufen, die die Daten für das recipe-Feld mit dem Argument „id: R1“ ermittelt.
Wie die Funktion implementiert wird, ist Aufgabe des Entwicklers. Wichtig für das GraphQL-Framework ist nur, dass die korrekten Daten zurückgeliefert werden. Typischerweise würde eine Resolver-Funktion zum Beispiel Daten aus einer Datenbank oder einem Microservice abfragen und zurückliefern.
Konzeptionell kann man sich einen GraphQL-Request also wie eine Menge von Funktionsaufrufen im Backend vorstellen. Natürlich gibt es Framework-spezifische Möglichkeiten, die die Implementierung der Funktionen vereinfachen und deren Ausführung optimieren.
Sind alle Resolver-Funktionen ausgeführt, validiert das GraphQL-Framework die zurückgelieferten Daten. Passen diese zum Schema, erhält der Client das Ergebnis, ansonsten eine Fehlermeldung.
Wie sieht Tooling für GraphQL aus?
Das Schema einer GraphQL kann mittels eines eigenen GraphQL-Queries abgefragt werden. Darüber kann ein Client dann zum Beispiel auslesen, welche Objekte mit welchen Feldern es gibt. Auch die Dokumentation ist darüber abfragbar. Dieses Introspection-Feature machen sich eine Reihe von Tools zunutze, um eine sehr gute Unterstützung bei der Entwicklung anzubieten.
Ein bekanntes Tool ist z.B. GraphiQL. Dabei handelt es sich um einen Web-basierten Explorer, mit dem man Queries formulieren und ausführen kann. Da GraphiQL das Schema einer API auslesen kann, gibt es beim Formulieren der Abfragen Code-Completion, Syntax Highlighting und Fehlerüberprüfung, wie man das aus IDEs von Programmiersprachen gewohnt ist. Ähnlichen Support bieten auch Plug-ins z.B. für JetBrains IDEs oder Visual Studio Code. Auch hier kann man direkt in der IDE seine Abfragen nicht nur formulieren, sondern auch überprüfen und ausführen lassen.
Darüber hinaus gibt es aber auch zahlreiche Code-Generatoren (wie z.B. den GraphQL Code Generator und das DGS Code Generation Plugin ), die zum Beispiel für einen konkreten Query-Typen die jeweilige Antwort generieren können. Das funktioniert insbesondere für TypeScript sehr gut, aber auch für andere Programmiersprachen gibt es Ansätze.
Auf diese Weise ist es möglich, mit GraphQL eine Art „Ende-zu-Ende-Typsicherheit“ zu bekommen. Beim Entwickeln parst der Code-Generator den geschriebenen Code, um darin enthaltene GraphQL-Queries zu finden. Für die gefundenen Queries wird nun Code generiert (z.B. ein Typ für die Antwort des Queries).
Wenn der Query ungültig ist, erzeugt der Code-Generator direkt eine Fehlermeldung. Wenn der generierte Typ wiederum nicht korrekt verwendet wird (weil zum Beispiel aus dem Query ein Feld entfernt wurde, das aber in der Anwendung noch verwendet wird), gibt es einen Compile-Fehler. Auch wenn die API selbst geändert wird, und – zum Beispiel – im CI-Build der Code-Generator ausgeführt wird, würde es sofort Fehler geben, wenn zum Beispiel Felder verwendet werden, die in der API entfernt wurden.
Können GraphQL-Ergebnisse gecacht werden?
Ein häufig formulierter Kritikpunkt gegenüber GraphQL-APIs ist, das GraphQL-Abfragen bzw. deren Ergebnisse nicht gecacht werden können. Dieser Einwand ist jedoch nur teilweise berechtigt.
Richtig ist, dass sich – im Vergleich zu GraphQL – Ergebnisse von REST APIs einfacher cachen lassen, da hier für jede Ressource ein individueller Endpunkt aufgerufen wird, dessen Ergebnis bereits vom Browser oder auch von einem Content Delivery Network (CDN) gecacht werden kann. GraphQL-Abfragen werden immer an denselben Endpunkt (üblicherweise /graphql) gesendet, so dass der Browser das Ergebnis nur bedingt cachen kann. Dazu kommt, dass in einer GraphQL-Antwort mehrere Objekte enthalten sein können, die eine unterschiedliche Cache-Dauer haben können.
Das bedeutet aber nicht, dass GraphQL-Operationen überhaupt nicht gecacht werden können, denn neben den Browser- und CDN-Caches gibt es eine Reihe weiterer Stellen, an denen Daten eines Requests vorgehalten werden können (zum Beispiel im Anwendungs- oder Datenbank-Cache). Unabhängig davon hängt die Möglichkeit des Cachings auch stark von fachlichen Faktoren ab. Daten, die sich eher schnell ändern, sind Technologie-unabhängig für Caching nicht geeignet.
Ist GraphQL SQL fürs Frontend?
Nein.
Warum sollte ich GraphQL verwenden?
Die Stärke von GraphQL ist sicherlich die Möglichkeit für Clients, passgenaue Abfragen zu stellen. Das zählt sich insbesondere aus, wenn die Anforderungen des Clients im Vorweg nicht bekannt sind, oder sogar die Clients selbst gar nicht bekannt sind, zum Beispiel bei öffentlichen APIs, für die Kunden ihre eigenen Clients schreiben.
Daneben ist aber auch die Typsicherheit ein wichtiges Argument für den Einsatz von GraphQL, denn dadurch können zahlreiche typische Fehler vermieden werden. Auch das Kennenlernen einer API wird dadurch erheblich vereinfacht.
Wann sollte ich GraphQL nicht verwenden?
Die Flexibilität (auch wenn sie, wie gesehen, enge Grenzen hat) ist aus Client-Sicht eine große Stärke, kann aber für die Implementierung im Backend eine große Herausforderung sein. Je nach Komplexität der API bzw. Komplexität eines konkreten Queries kann die performante und sichere Bereitstellung der Daten eine Hürde bei der Entwicklung sein.
Dadurch, dass prinzipiell jede Anfrage anders aussehen kann, ist es zum Beispiel schwer, im Vorfeld Optimierung (etwa durch optimierte DB-Zugriffe oder Caching) umzusetzen. Ob und inwieweit das ein echtes Problem darstellt, kann nur im konkreten Projekt-Kontext beantwortet werden.
Auch ob das Thema Over- und Underfetching für oder gegen GraphQL spricht, hängt von API und Kontext ab. Dem entgegen steht zum Beispiel, dass die Möglichen des Cachings (bereits im Browser) sehr viel größer und einfacher in klassischen REST-Anwendungen sind. Der Nachteil des Over- bzw. Underfetchings kann unter Umständen dadurch kompensiert werden.
Außerdem sollte man überlegen, wer die GraphQL-API konsumieren soll. In einer internen API zum Beispiel können sich alle Beteiligten auf die Verwendung von GraphQL festlegen. In einer öffentlichen API sollte man sich die Frage stellen, ob potenzielle Kunden bereit sind, GraphQL-Clients zu entwickeln, zumal das Know-How hier bei weitem nicht so weit verbreitet ist wie bei REST APIs und diese deshalb sicherlich eine geringere Einstiegshürde darstellen.
GraphQL – Fazit
GraphQL hat, wie andere Technologien auch, Stärken und Schwächen. Vor dem Einsatz von GraphQL sollte man prüfen, ob die gebotenen Features für die eigene Entwicklung einen Vorteil zum Beispiel gegenüber REST APIs bieten. Dabei sollte man sich auf die Gedankenwelt und Philosophie von GraphQL einlassen und nicht versuchen, die Konzepte aus REST APIs 1:1 auf GraphQL zu übertragen.
Für die Entwicklung von GraphQL-APIs stehen viele Frameworks zur Verfügung, die es einem leicht machen, zumindest eine kleine API testweise schnell umzusetzen. Beim Design einer GraphQL-API sollte man, fachlich getrieben, Schritt-für-Schritt evolutionär vorgehen. Dadurch hat man gute Chancen, eine API zu bauen, die sowohl für Konsumenten verständlich als auch in der Entwicklung beherrschbar bleibt.
Titelmotiv: Bild von Gerd Altmann auf Pixabay
- GraphQL: 13 Antworten auf häufig gestellte Fragen - 4. Juni 2024
- React 2024 – SPA oder Fullstack-Anwendung? - 30. November 2023
- React-Anwendungen mit TypeScript entwickeln - 11. Februar 2021