×

GraphQL: Das bessere REST?

GraphQL: Das bessere REST?

GraphQL ist eine Technologie, die in den letzten Jahren sehr populär geworden ist und in der Regel als REST-Ersatz zum Implementieren von Backend-APIs verwendet wird. Doch bevor wir uns mit GraphQL beschäftigen, müssen wir verstehen, warum in der Industrie der Wunsch nach einer modernen REST-Alternative entstanden ist.

REST (kurz für „Respresentational State Transfer“) ist seit langer Zeit der de-facto Standard für Webservice APIs. Eine API besteht dabei aus mehreren Ressourcen (auch Endpoints genannt), die jeweils eine bestimmte Art von Daten repräsentieren (z.B. Kontakte oder Adressen). Interaktionen mit den Ressourcen erfolgen über HTTP-Requests. Dabei sind folgende Parameter beteiligt:

  • Die genaue URL (z.B. /contacts für Kontakte)
  • Die HTTP-Methode (GET zum Lesen von Daten, POST zum Hinzufügen, …)
  • HTTP-Header (z.B. Definition des verwendeten Datenformats)
  • Der Body des Requests (z.B. die Daten zum Hinzufügen eines Kontakts)

Die Antwort besteht dann aus folgenden Komponenten:

  • Einem HTTP Statuscode (z.B. 200 für Ok, 401 für fehlende Berechtigung) der das Ergebnis der Operation angibt
  • HTTP-Header (z.B. Größe der Antwort)
  • Einem optionalen Body, der die angefragten Daten enthält

Dieser Ansatz ist zwar relativ einfach und effektiv, aber es gibt auch einige Probleme:

Dokumentation

Der REST-Standard macht keine Vorgaben über die Art wie Ressourcen dokumentiert werden sollen. Es gibt zwar aufbauende Standards die genau dies tun, aber man kann sich bei einer REST-API nicht darauf verlassen, dass diese genutzt werden. Und selbst wenn gute Dokumentation vorliegt: es gibt keine Garantie, dass diese mit der Implementierung übereinstimmt. Das Nutzen von REST-APIs, die man selbst nicht entwickelt hat, ist daher oft unsicher und kann theoretisch zu Laufzeitfehlern führen. Fehlerbehandlung kann dadurch je nach Situation sehr komplex werden.

Datenmenge

Es gibt bei REST-APIs keinen standardisierten Ansatz, um die Felder eines Objekts einzugrenzen. Interessieren einen Client beispielsweise nur die Namen aller Kontakte, bekommt er möglicherweise trotzdem alle Daten dieser Ressource geliefert und liest dann nur das Namensfeld aus. Je nach Anwendungsfall kann es durchaus einen großen Unterschied machen, ob die Antwort 1MB oder nur 1KB an Daten enthält. Man denke nur an das vielerorts noch relativ langsame mobile Internet. Natürlich kann man die API so anpassen, dass über bestimmte Parameter die Felder begrenzt werden, aber dies ist eben kein Standard und man kann sich nicht darauf verlassen, dass dies immer möglich ist.

Kombinierbarkeit

Selten sind Datentypen komplett voneinander isoliert. Ein Kontakt hat vielleicht auch eine Adresse, die über eine andere Ressource abrufbar ist. Wenn ein Client nun einen Kontakt mit Adresse abrufen möchte, gibt es zwei Möglichkeiten:

  1. Ein Kontakt enthält immer eine Adresse.
  2. Ein Kontakt enthält nur eine Referenz zu der Adresse (z.B. die ID) und der Client schickt dann einen zweiten Request ab, um die Adresse abzurufen.

Beide Wege sind nicht optimal. Bei der ersten Option sind zwar immer alle Daten vorhanden, aber auch hier werden dann wieder Daten verschickt, die eventuell nicht jeden Client interessieren. Bei Option Nummer zwei ist dies nicht so schlimm (die Adress-ID ist trotzdem noch vorhanden und braucht Speicher), aber dafür werden weitere HTTP-Requests nötig, was das Laden der Daten verzögert. Gerade bei langsameren mobilen Verbindungen kann die Latenz zum Server ein Flaschenhals beim Seitenaufbau sein, da sich solche voneinander abhängigen Requests nicht parallelisieren lassen.
Natürlich kann man hier die API wieder parametrisieren. Aber wenn z.B. die Adresse auch wieder eingebettete Objekte hat, wird dies zu einem sehr komplexen Problem und macht die Verwendung der API sehr kompliziert.

GraphQL

GraphQL verwendet einen komplett anderen Ansatz, um diese Probleme zu lösen. Eine GraphQL-API hat nur einen einzigen Endpoint. Anfragen an die API werden in einer speziellen Abfragesprache verfasst. Grundlage dieser Sprache ist ein GraphQL-Schema. In diesem Schema ist genau festgelegt, welche Datentypen in der API verwendet werden und welche Abfragen mit welchen Parametern möglich sind.

Besonders an der Abfragesprache ist, dass man explizit festlegen muss welche Daten man empfangen möchte. Ein Client bekommt somit immer genau die Daten, die ihn interessieren – nicht mehr und nicht weniger. Weiterhin lassen sich die Daten beliebig vieler Datentypen mit einer Abfrage verknüpfen. Damit sind Abfragen wie „Die Namen aller Kontakte mit der Stadt aus ihrem Wohnort und von der Stadt noch die Partei des Bürgermeisters“ problemlos in einem Aufruf möglich (ein entsprechendes Datenmodell vorausgesetzt). Alles in einem HTTP-Request und nur mit den Daten, die man wirklich braucht – schlanker geht es nicht. In einer REST-API ist eine solche Flexibilität ohne spezielle Endpoints nicht vernünftig zu bewerkstelligen. Mit GraphQL gibt es diese Flexibilität von Haus aus.

Ein GraphQL Schema kann beispielsweise folgendermaßen aussehen:

type Query {
 contacts: [Contact!]!
 contactsFromCity(city: String!): [Contact!]!
}

type Address {
 city: String!
 zipcode: String!
 street: String!
 number: String!
}

type Contact {
 name: String!
 address: Address!
}

Dieses Schema definiert zwei Datentypen: Kontakt und Adresse, wobei ein Kontakt immer eine Adresse besitzt. Ein Client kann entweder alle Kontakte oder nur die Kontakte einer bestimmten Stadt abfragen. Die Ausrufezeichen geben hier an, dass das jeweilige Feld nicht optional ist, d.h. immer gesetzt ist.

Abfragen sehen dann beispielsweise so aus:

# Namen und Stadt aller Kontakte
{
 contacts { 
  name
  address {
   city
  }
 }
}

# Namen, Straße und Hausnummer aller Kontakte aus Branschweig
{
 contactsFromCity(city: "Branschweig") {
  name
  address {
   street
   number
  }
 }
}

Die erste Abfrage fragt nur den Namen und Stadt aller Kontakte ab. Andere Felder wie Hausnummer werden dabei nicht übertragen. Die zweite Abfrage ruft alle Kontakte einer Stadt ab. Als Argument wird hier Brauschweig übergeben. Der GraphQL-Server kann damit seine Suche auf die Kontakte aus Braunschweig einschränken. Die Adresse wird hier dann auf Straße und Hausnummer beschränkt. Die Stadt, die in diesem Fall schon feststeht, wird somit nicht übertragen. Die Sprache bietet natürlich noch viele weitere Features wie Interfaces, Enumerations oder Union Types, um Daten effizient und ohne Redundanzen gut modellieren zu können.

Dieses Konzept ermöglicht eine ganze Reihe von Features, die sowohl den Benutzern einer fertigen Applikation, als auch den Entwicklern zu Gute kommen:

Garantiert korrekte Dokumentation

Das Schema einer GraphQL-API legt genau fest welche Abfragen möglich sind und welche Datentypen genutzt werden. Aus diesem Schema leitet sich direkt die Implementierung der einzelnen Abfragen ab. Somit stellt dieses Schema eine Dokumentation dar, die immer korrekt ist. Natürlich kann es nicht garantieren, dass es keine Bugs in der Implementierung gibt, aber dies kann kein Tool leisten.

Gängige GraphQL Serverimplementierungen bieten in der Regel auch eine Interaktive Oberfläche, mit der das Schema visuell aufbereitet und Abfragen live ausprobiert werden können. Als Entwickler kann man sich somit schnell mit einer API vertraut machen oder die eigene API schnell und einfach testen.

Typsicherheit in Clients

Da ein GraphQL-Schema komplett maschinenlesbar ist, kann es genutzt werden, um auf Clientseite passende Typen in der verwendeten Programmiersprache zu generieren. Alle gängigen GraphQL-Server Implementierungen ermöglichen das Abfragen des Schemas über einen speziellen Endpoint – mit einer sogenannten „Introspection Query“. Somit kann das Generieren von Typen ohne Probleme automatisiert werden. Dies ist in der Regel bei GraphQL-Client Implementierungen für statisch typisierte Programmiersprachen integriert. Als Entwickler kann man sich also zur Compile-Zeit sicher sein, dass die Client Anwendung mit dem GraphQL-Server kompatibel ist. Breaking Changes im Server würden dann auch entsprechend zu Kompilierfehlern im Client führen. Entsprechendes Editor-Tooling vorausgesetzt, bekommt man so auch Auto-Completion von Typen der GraphQL-API im Editor und viele weitere Annehmlichkeiten die modernes Editor-Tooling von statisch typisierten Sprachen bietet. Je nach verwendeter Programmiersprache kann dies die Wahrscheinlichkeit von Laufzeitfehlen stark verringern. Man ist damit dem Prinzip „If it compiles, it works“ wieder einen deutlichen Schritt nähergekommen.

Stark vereinfachte Client- und Serverentwicklung

In REST-basierten Anwendungen wird zum Teil eine Menge Code für die Serialisierung und Deserialisierung von Daten, das Senden der HTTP-Request an die unterschiedlichen URLs der REST-API und das Definieren und Implementieren der evtl. auch noch parametrisierten Endpoints benötigt. GraphQL-Frameworks können dies alles selbst erledigen, da der GraphQL-Standard dies genau vorschreibt. Gerade auf dem Server bedeutet dies eine große Ersparnis in zu schreibenden Code, im Vergleich zu einem REST basierten Ansatz.

Flexibilität

Ein Client kann in einer GraphQL-Abfrage genau spezifizieren welche Daten er erhalten möchte. Darüber hinaus kann aber auch das Format der Daten angepasst werden. Ein Client kann somit auch sehr einfach tief geschachtelte Objekte als flache Objekte empfangen und auch Felder umbenennen. Das Integrieren von GraphQL in einen Client mit bereits vorhandenen Typdefinitionen ist somit kein Problem.

Auch serverseitig lässt sich ein GraphQL-Endpoint ohne Probleme parallel zu einer eventuell schon bestehenden REST-API aufbauen. Graduelle Migrationen zu einer GraphQL-API sind daher sowohl auf dem Server als auch auf dem Client einfach möglich.

Wozu dann noch REST?

Dies klingt jetzt vielleicht alles stark danach, dass REST-APIs ausgedient hätten und man einfach immer GraphQL verwenden sollte. Aber wie immer in der Softwareentwicklung gibt es auch hier kein Allheilmittel. GraphQL spielt seine Stärken vor allem in mittleren bis großen Anwendungen voll aus. REST-API werden immer dann ineffizient und aufwändig zu pflegen, wenn die Daten dahinter komplexer sind und dies ist der Punkt an dem Entwickler und auch User von einer GraphQL getriebenen API profitieren. Bei kleineren Projekten mit einer überschaubaren Anzahl von Datentypen kann ein Standard REST Ansatz aber weiterhin vollkommen in Ordnung sein. Der Einsatz von GraphQL bringt eine gewisse Basiskomplexität in die Anwendung, die deutlich höher als bei Standard REST ist. Komplexe Datenmodelle können so gut beherrscht werden, in einfacheren Anwendungsfällen schießt man damit aber eventuell über das Ziel hinaus. Wie immer gilt es hier zu entscheiden, ab welchem Punkt sich die GraphQL-Komplexität im geplanten Projekt amortisiert.


https://graphql.org/

Von Jannis Isensee | 22.04.2021
Jannis Isensee

Softwareentwicklung