Fast jeder App-Entwickler wurde bereits mit einer REST API konfrontiert, die zur Datenabfrage und -manipulation mehrere Endpunkte mit unterschiedlichen URLs zur Verfügung stellt. Die Anfragen und der Aufbau der zugehörigen Antworten sind dabei vom Backend vordefiniert. Doch dieser Ansatz hat seine Grenzen. Wäre es nicht schön, die Anfragen selbst zu definieren und vom Backend nur die Daten zu erhalten, die man auch verlangt? Diese Art der Datenabfrage ist elementarer Bestandteil der Spezifikation der von Facebook entwickelten Anfragesprache GraphQL.
Einige Vorteile von GraphQL:
- kein Over- bzw. Underfetching
- Designanpassungen des Clients erfordern keine Anpassungen am Server
- Implementierung von Client und Server unabhängig voneinander, sobald das Schema definiert ist
- Die Anfragen der Clients können analysiert und ausgewertet werden, um dementsprechend Anpassungen am Server vorzunehmen
- Solange das Backend noch nicht verfügbar ist, kann die Datenstruktur des Schemas gemockt und anschließend einfach durch die Daten des Backends ausgetauscht werden
Schema & Typsystem
Damit der Client selbst Anfragen definieren kann, muss er zunächst wissen, welche Anfragen überhaupt möglich sind. Dazu muss jeder GraphQL-Service ein sogenanntes Schema bereitstellen. Zur Beschreibung des Schemas wird die sogenannte „GraphQL Schema Language” genutzt. Das Schema definiert das Typsystem und die Operationen, die auf den Datentypen angewendet werden können. Das Schema dient als Vertrag zwischen Client und Server. Sobald es definiert ist, kann die Implementierung von Client und Server unabhängig voneinander erfolgen. Die elementarsten Komponenten eines Schemas sind Typen. Folgendes Beispiel stellt die Definition der Typen Player und Club dar.
Beispiel 1:
type Player {
name: String!
shirt-number: Int
club: Club
}
type Club {
name: String!
players: [Player!]
}
type Query {
players(last: Int): [Player!]!
}
type Mutation {
createPlayer(name: String!, age: Int!): Player!
}
type Subscription {
newPlayer: Player!
}
Ein Typ besteht aus Feldern. Die Felder sind entweder skalar (z.B. Strings, Integer, Enumerationen) oder wiederum Typen mit Unterfeldern. Dadurch lassen sich Relationen zwischen verschiedenen Typen beschreiben. Im obigen Beispiel besteht eine one-to-many-Relation zwischen den Typen Player und Club. Der Typ Player besteht aus den Feldern name vom Typ String, shirt-number vom Typ Int und club vom Typ Club. Das ! hinter der Typbezeichnung drückt aus, dass das Feld nicht den Wert null annehmen darf. Bei dem Feld players des Typs Club handelt es sich um ein Array. Dies wird durch die eckigen Klammern um die Typbezeichnung ausgedrückt.
Neben den normalen Typen gibt es noch einige besondere Typen:
Querys
Eine Query ist die einfachste Form einer GraphQL-Anfrage. Dabei werden die Werte der Felder eines Typs abgefragt. Eine GraphQL-Anfrage kann man sich wie einen Baum aus der Graphentheorie vorstellen, da die Typen verschachtelt sein können. Die Typen stellen die Knoten des Baums dar und die skalaren Felder die Blätter.
Das heißt, eine gültige Anfrage darf keine Typen ohne zugehörige skalare Felder enthalten, da die Typen selbst keine textuell darstellbaren Daten sind. Die Struktur der Anfrage bestimmt zugleich die Struktur der zugehörigen Antwort. Sie unterscheiden sich nur darin, dass die Felder nun zusätzlich die entsprechenden Werte enthalten. Den Feldern können außerdem Argumente übergeben werden, falls diese im Schema spezifiziert sind. Dem Feld players kann somit in Beispiel 2 das Argument last übergeben werden, um die Zahl der Ergebnisse zu begrenzen.
Beispiel 2:
Anfrage:
query GetAllPlayers {
players(last: 3) {
name
}
}
Antwort:
{
"data": {
"players": [
{
"name": "Christiano Ronaldo"
},
{
"name": "Gareth Bale"
},
{
"name": "Sergio Ramos"
}
}
}
}
Mutations
Um Daten auf dem Server zu manipulieren, werden Mutations eingesetzt. Es wird zwischen drei Arten von Mutations unterschieden:
- Erstellen neuer Daten
- Verändern bestehender Daten
- Löschen bestehender Daten
Mutations sind aufgebaut wie Querys. Der einzige syntaktische Unterschied besteht darin, dass Mutations mit dem Keyword mutation beginnen müssen. Im folgenden Beispiel wird mithilfe einer Mutation ein neues Objekt vom Typ Player erzeugt.
Beispiel 3
Anfrage:
mutation CreateNewPlayer {
newPlayer(name: "Bob", shirt-number: 42)
}
Antwort:
{
"data": {
"newPlayer": {
"name": "Bob",
"shirt-number": 42
}
}
}
Subscriptions
Oft ist es notwendig, dass der Server die Clienten bei bestimmten Ereignissen in Echtzeit benachrichtigt. Subscriptions ermöglichen das. Der Client kann sich mithilfe von Subscriptions für Datenänderungen am Server registrieren. Dadurch wird eine stetige Verbindung zwischen Client und Server aufgebaut. Sobald das abgefragte Ereignis stattfindet, sendet der Server die entsprechenden Daten an den Client. Die Syntax von Subscriptions entspricht der Syntax von Querys und Mutations. Sie beginnen jedoch mit dem Keyword subscription. Im folgenden Beispiel wird eine Registrieung für das Ereignis newPlayer abgeschlossen. Der Client wird anschließend jedes Mal informiert, wenn ein neues Objekt vom Typ Player erstellt wird. Die Daten, die dabei übermittelt werden sollen, werden in der Subscription-Anfrage spezifiziert.
Anfrage:
subscription NewPlayer {
newPlayer {
name
}
}
Antwort (sobald ein Objekt vom Typ Player erstellt wird):
{
"newPlayer": {
"name": "Bob"
}
}
Fazit
GraphQL bietet mit seiner eleganten und flexiblen Anfragesprache eine tolle Alternative zu REST. Es existieren jedoch bisher nur wenige Implementierungen und die meisten sind noch nicht sehr ausgereift. Außerdem muss bei GraphQL auf einige Features, die bei REST zur Verfügung stehen verzichtet werden, wie z.B. HTTP-Caching oder Versioning. Dennoch sollte GraphQL beim Entwurf einer Server-Client-Architektur auf jeden Fall als Option in Betracht gezogen werden. Vor allem wenn die Vorteile von GraphQL voll ausgenutzt werden können.
Und wie nutzt man GraphQL jetzt am Besten in der App-Entwicklung? Das verraten wir in einem Folgebeitrag.