Was der neue API-Ansatz leistet und was nicht

Einführung in GraphQL

Alexander Kirschner, Gernot Pointner

© shutterstock.com | Hitdelight

Der Architekturstil REST ist aus der modernen Web-Services- und Backend-Entwicklung kaum mehr wegzudenken. Eine RESTful Architektur ist aber nicht zwangsläufig für jede Problemstellung automatisch die optimale Lösung. Motiviert von den Anforderungen datengetriebener, mobiler Clients hat Facebook GraphQL entwickelt, eine API-Beschreibung und -Anfragesprache, die in den letzten Jahren auch über die Grenzen von Facebook hinaus gewachsen ist.

Bereits im Jahr 2000 im Rahmen der Dissertation „Architectural Styles and the Design of Network-based Software Architectures“ von Roy Thomas Fielding vorgestellt, entwickelte sich REST über die Jahre zu einer echten Konkurrenz zu klassischen Web-Services-Technologien wie WSDL. Trotz der immensen Popularität hat der RESTful Ansatz seine Grenzen. Neu entstehende Anwendungsfälle bedingen oftmals neue Anforderungen: Im Jahr 2012 entstand bei Facebook der Bedarf nach einem neuen API-Ansatz, als im Rah- men der Neukonzeption der Smartphoneapplikationen die bestehenden Technologien als nicht ausreichend für die geänderten Anforderungen angesehen wurden. Mobile Clients haben oft mit limitierter und schwankender Verbindungsqualität zu kämpfen. Für Facebook war das die grundlegende Herausforderung und Motivation für die Entwicklung von GraphQL. Der Client erhält Zugriff auf ein als Graph dargestelltes Datenmodell des Servers, gegen das er Anfragen stellen kann. Die zu übertragende Datenmenge lässt sich je nach Anwendungsfall optimieren, indem nur der tatsächlich benötigte Teil der Daten des Servers angefordert wird. Facebook stellt neben der Spezifikation selbst (OWFa-1.0-Lizenz) auch eine passende Referenzimplementierung in JavaScript (MIT-Lizenz) zur Verfügung. Für die Weiterentwicklung der Spezifikation wurde ein RFC-Prozess etabliert.

Wie funktioniert GraphQL?

Das zentrale Element eines GraphQL-API ist das Schema, das mittels Interface Definition Language (IDL) beschrieben und von einem Server bereitgestellt wird. GraphQL folgt hier dem Schema-First-Ansatz: Dieses Schema legt den Vertrag zwischen Client und Server fest, die Daten stehen im Vordergrund. Das Schema ist streng typisiert. Neben den primitiven Datentypen IntFloat, String, Boolean und ID sind auch hierarchisch geschachtelte Objekttypen möglich, die selbst wiederum Felder enthalten können. Das Schema beschreibt außerdem die Operationen, die sich auf den Objektgraphen anwenden lassen. Man unterscheidet zwischen lesenden Operationen, den Queries, und schreibenden Operation, den Mutations. Die Beschreibung der Operationen ist ebenfalls durch die IDL möglich. So werden die verfügbaren Operationen einfach als zusätzliche Schemaelemente modelliert. Listing 1 zeigt ein GraphQL-Schema für einen beispielhaften Webshop.

type Product {
  id: ID!
  title: String!
  description: String!
  imageUrl: String
  ratings: [Rating]!
  averageRatingScore: Float
}
 
type Customer {
  id: ID!
  name: String!
  ratings: [Rating]!
}
 
 
type Rating {
  id: ID!
  product: Product!
  customer: Customer!
  score: Int!
  comment: String!
}
 
type Query {
  getProduct(productId: ID!) : Product
}
 
type Mutation {
rateProduct(productId: ID!, customerId: ID!, score: Int!, comment: String!) : Rating!
}

Abb. 1: Objektgraph für das Webshopschema

Das Schema konzentriert sich auf das Auslesen und die Bewertung von Produkten. Listen sind durch eckige Klammern gekennzeichnet, die Ausrufezeichen markieren Felder, die nicht Null sein dürfen. Im unteren Teil des Schemas definiert eine Query das Abrufen eines Produkts anhand seiner ID. Mithilfe einer Mutation kann ein bestehendes Produkt bewertet werden. Abbildung 1 zeigt den aus dem Schema resultierenden Objektgraphen.

Das Schema als Basis für Anfragen

Basierend auf einem Schema wie in Listing 1 kann ein Client seine gewünschten Anfragen formulieren. Die Einsprungspunkte geben die dort definierten Queries und Mutations vor, aus deren Ergebnissen im JSON-Format der Client dann die gewünschten Daten selektiert. GraphQL ist prinzipiell protokollagnostisch, nach Best Practice wird jedoch meist HTTPS als Trägerprotokoll verwendet. Der GraphQL-Server ist dann unter einem einzigen URL ansprechbar, der /graphql lautet. Der Client stellt seine Anfragen als HTTP POST oder HTTP GET Request.

Liegt das komplette Schema wie in Listing 1 in IDL-Form vor, kann der Client seine Anfragen direkt formulieren. GraphQL bietet als Variante die Möglichkeit, über Introspection das Metamodell des Schemas abzufragen (z. B. Typen und deren Felder, Queries, Mutations). So ist implizit bei jeder GraphQL-Query ein __schema-Feld verfügbar. GraphQL-Clients können somit über denselben Mechanismus sowohl konkrete Anfragen an den Server stellen als auch Schemainformationen für den Entwickler bereitstellen. Für den beispielhaften Webshop ist eine Clientanwendung denkbar, die für ein bestimmtes Produkt dessen Daten inklusive aller Bewertungen anzeigt. Neben dem Titel und der Beschreibung des Produkts müssten hier alle seine Bewertungen und auch deren Autoren geladen werden.

Listing 2 zeigt eine GraphQL-Anfrage, die alle dazu benötigten Daten mit einem einzigen Aufruf lädt. Mittels GraphQL kann aber auch mehrmals die gleiche parametrisierte Query mit unterschiedlichen Parametern in einer einzigen Anfrage abgesetzt werden. Zu diesem Zweck unterstützt GraphQL Aliasse, um die Ergebnisse einer Anfrage den Queries eindeutig zuordnen zu können. In Listing 2 ist ein solcher Alias, catfood, der Query vorangestellt.

{
  catfood: getProduct(productId: "P1") {
    title
    description
    imageUrl
    averageRatingScore
    ratings {
      customer {
        name
      }
      score
      comment
    }
  }
}

Listing 3 zeigt eine mögliche Antwort des Servers. Zu beachten ist, dass der Server exakt die angeforderten Felder zurückliefert und auch bei referenzierten Objekttypen, wie in diesem Fall Bewertungen und Kunden, nur die geforderten Felder lädt. Das gewählte Alias wurde ebenfalls mit in die Antwort übernommen.

{
  "data": {
    "catfood": {
      "title": "Katzenfutter Deluxe",
      "description": "Lorem ipsum dolor sit amet ...",
      "imageUrl": "/media/catfood.jpg",
      "averageRatingScore": 8.5,
      "ratings": [
        {
          "customer": {
            "name": "karl_kunde"
          },
          "score": 9,
          "comment": "Mein Kater Leroy hat alles weg geputzt."
        },
        {
          "customer": {
            "name": "Superfooder"
          },
          "score": 8,
          "comment": "Alles OK."
        }
      ]
    }
  }
}

Prototypische Implementierung einer GraphQL-Schnittstelle

Wir wissen nun, wie sich die Definition eines GraphQL-Schemas zusammensetzt und wie Anfragen prinzipiell gegen dieses ausgeführt werden. Doch welche Schritte sind für eine prototypische Implementierung einer GraphQL-Schnittstelle nötig? Wer sich nicht selbst dazu berufen fühlt, eine komplette GraphQL-Implementierung auf die Beine zu stellen, greift auf verfügbare Bibliotheken zurück. Für gängige Programmiersprachen stehen bereits zahlreiche Implementierungen zur Verfügung, unter anderem in Scala, Clojure und Python. Für Java bietet sich die Bibliothek graphql-java an, die sich an der Referenzimplementierung von Facebook orientiert.

Als nützliches Werkzeug für die Entwicklung von GraphQL-Anwendungen bietet sich GraphiQL an, eine Art GraphQL-Entwicklungsumgebung. Das Tool bietet ein Web-Frontend, in dem sich Queries und Mutations entwickeln und testen lassen. Mittels Introspection bringt es eine Autovervollständigung mit, die das spielerische Erkunden des Schemas ermöglicht. Schauen wir uns eine prototypische Implementierung für den Webshop auf Basis von graphql-java an. Um den benötigten Boilerplate-Code möglichst gering zu halten, verwenden wir zusätzlich die GraphQL Java Tools. Diese Bibliothek ist einer JavaScript-Bibliothek von Facebook nachempfunden und dient als Abstraktion zu graphql-java. Sie ermöglicht es, ein in der GraphQL-IDL geschriebenes Schema zu verwenden. Dazu existieren Hilfsklassen, um unter anderem den GraphQL-Server über das HTTP-Protokoll als Endpunkt verfügbar zu machen.

Ein GraphQL-Client soll folgende Anfrage an den GraphQL-Server absetzen und den Titel des Produkts P1 anfragen:

curl -X POST \
  localhost:8080/graphql \
  -d '{ "query": " query($queryProductId: ID!) {getProduct(productId: $queryProductId) { title } }" , "variables": { "queryProductId": "P1" }}'

Hierzu müssen jeder Objekttyp, jedes Feld und jede Operation aus dem in Listing 1 gezeigten Schema eine Entsprechung in Java erhalten. Gegenüber dem Request aus Listing 2 wird bei dem curl-Kommando im Beispiel die ID des Produkts als Variable getrennt übermittelt. Vor allem bei mehreren Parametern in einem Request trägt dies zur Übersichtlichkeit bei.

Abb. 2: Logische Bausteine einer GraphQL-Serverimplementierung mit den GraphQL Java Tools

Abbildung 2 zeigt die Zusammenhänge zwischen den logischen Bausteinen, die an der prototypischen Implementierung beteiligt sind. Die GraphQL-Implementierung als Kern der Anwendung basiert auf den GraphQL Java Tools. Das in IDL beschriebene Schema wird durch die GraphQL-Implementierung eingelesen und ausführbar gemacht. Dadurch wird ein programmatischer Zugriff auf die Schemaelemente wie Queries und Mutations möglich. Ein HTTP-Endpunkt nimmt Anfragen von GraphQL-Clients entgegen und verwendet die GraphQL-Implementierung zum Abarbeiten.

Damit die GraphQL-Implementierung die Operationen aus dem Schema ausführen kann, benötigt sie weitere Helfer wie (Property) Data Fetcher und Type Resolver. Wir müssen beispielsweise für jede Query einen sogenannten Query Resolver von Hand implementieren.

Listing 4 zeigt die Implementierung des Resolvers für die lesende Operation getProduct() aus Listing 1. Die Java-Klasse implementiert das Interface GraphQLQueryResolver. Die hier nicht gezeigte Implementierung der schreibenden Operation rateProduct() verhält sich analog, implementiert jedoch entsprechend das Interface GraphQLMutationResolver.

public class QueryResolver implements GraphQLQueryResolver {
 
  private ProductRepository productRepository;
 
  // Constructor omitted for brevity
 
  public Optional<Product> getProduct(String productId) {
    return productRepository.getById(productId);
  }
}

Je nachdem, ob es sich um die Implementierung einer Query oder einer Mutation handelt, finden sich in der entsprechenden Implementierung des Resolvers typischerweise lesende oder modifizierende Operationen. Diese könnten beispielsweise Interaktionen mit Datenbanken oder Third-Party-Services sein.

Listing 5 zeigt exemplarisch die Klasse Product, die als Plain Old Java Object (POJO) implementiert ist. Auffällig ist, dass nicht jedes Feld aus dem GraphQL-Objekttyp aus Listing 1 eine Entsprechung in der Java-Klasse besitzt. Beispielsweise kennt die Java-Klasse weder ihre durchschnittlichen Bewertungen (averageRatingScore) noch kann sie Zugriff auf ihre Bewertungen gewähren.

public class Product {
  private String id;
  private String title;
  private String description;
  private String imageUrl;
 
  // Getters and Setters omitted for brevity
}

Es handelt sich schlichtweg um eine Funktionalität, die über die eines typischen POJO hinausgeht. Falls für die Bearbeitung einer Query oder Mutation zusätzliche Funktionalität erforderlich ist, kann sie ausgelagert werden. In dem Fall tauchen erneut Resolver auf, diesmal in der Form des Interface GraphQLResolver<T>.

Listing 6 bildet exemplarisch die Klasse ProductResolver ab, in der die beiden gegenüber dem POJO zusätzlich benötigten Methoden implementiert sind. Die Methodensignaturen des Resolvers enthalten dabei jeweils als ersten Parameter die Objektinstanz, in deren Kontext die jeweilige Operation ausgeführt werden soll.

public class ProductResolver implements GraphQLResolver<Product> {
 
  private RatingRepository ratingRepository;
 
  // Constructor omitted for brevity
 
  public List<Rating> getRatings(Product product) {
      return ratingRepository.getByProductId(product.getId());
  }
 
  public double getAverageRatingScore(Product product) {
    return ratingRepository.getByProductId(
      product.getId()).stream()
        .mapToInt(Rating::getScore).average().getAsDouble();
  }
}

Ziel ist es letztendlich, dass bei einem Absetzen einer Query die Implementierung des passenden Resolvers verwendet wird. Ein Aufruf der Query getProduct() führt dann zu einem Aufruf der entsprechenden Java-Methode im ProductResolver. Listing 7 zeigt die Implementierung eines GraphQLEndpunkts als Ableitung der Klasse SimpleGraphQLServlet aus den GraphQL Java Tools. Die Klasse liest dabei das Schema aus Listing 1 als Datei ein, registriert alle benötigten Resolver und übergibt das eingelesene Schema an ihre Superklasse. Diese wenigen Zeilen bewirken, dass der Endpunkt unter dem Pfad /graphql in einem konfigurierten Webserver, beispielsweise Jetty oder Tomcat, erreichbar ist und GraphQL-Clients Anfragen an ihn stellen können.

@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {
 
  public GraphQLEndpoint() {
    super(createSchema());
  }
 
  private static GraphQLSchema createSchema() {
 
  // Creation of resolvers omitted for brevity
 
    return SchemaParser.newParser()
      .file("webshop-demo.graphqls")
      .resolvers(
        productResolver,
        ratingResolver,
        customerResolver,
        queryResolver,
        mutationResolver)
      .build()
      .makeExecutableSchema();
  }
}

Insgesamt fällt auf, dass bei keiner der gezeigten Codezeilen eine programmatische Zuordnung von Schemaobjekttypen und Feldern zu Java-Klassen, Attributen oder Methoden erfolgt. Die GraphQL Java Tools implementieren all dies mittels Reflection anhand einer priorisierten Zuordnung zwischen den GraphQL-Objekttypen und den Java-Klassen. Bei reiner Verwendung von graphql-java ohne die zusätzlichen GraphQL Java Tools hat der Entwickler einerseits mehr Einfluss auf die Abarbeitung des Requests, andererseits ist jedoch auch weitaus mehr Boilerplate-Code nötig. Abbildung 3 greift die Query aus Listing 2 auf und zeigt, von welchen Klassen die angeforderten Felder im GraphQL-Server aufgelöst werden.

Abb. 3: Zuordnung von Query-Feldern zu POJOs und Type Resolvers

Diejenigen Felder, die nicht im Produkt-POJO abgebildet sind, werden über die entsprechenden Resolver aufgelöst. Falls kein Mapping von den GraphQL Java Tools etabliert werden kann, bieten diese zur Laufzeit Hinweise, mit welchen Schreibweisen nach passenden Methoden gesucht wird, beispielsweise getRatings() oder ratings().

Zusammenfassend implementiert GraphQL-Java in Verbindung mit den GraphQL Java Tools die Auswertung der GraphQL-Anfragen, indem die Schemaelemente auf entsprechende Methoden oder Member-Variablen von Java-Klassen gemappt werden. Für den Entwickler bleibt dann noch das Laden beziehungsweise das Berechnen der entsprechenden Daten, um die angefragten Query-Felder zu befüllen.

Einbinden von GraphQL in eine bestehende Architektur

In bestehende Architekturen kann GraphQL auf vielfältige Weise eingebunden werden. Einerseits wird GraphQL als API für Services genutzt, die selbst Businesslogik implementieren. Dieser Ansatz ist im Anwendungsbeispiel skizziert. Andererseits wird GraphQL parallel zu bestehenden REST-APIs als eine Art Backend for Frontend (BFF) eingesetzt. Darüber hinaus ist ein Einsatz von GraphQL als API-Gateway denkbar, das einem oder mehreren bestehenden Backend-Systemen vorgeschaltet wird. Bei dieser Variante muss jedoch sichergestellt werden, dass die benötigten Daten aus den verschiedenen Quellen performant geladen werden.

Abb. 4: GraphQL-Server als API-Gateway

Abbildung 4 zeigt eine grobe Architektur, bei der verschiedene Clients einen gemeinsam
genutzten GraphQL-Server als API-Gateway ansprechen. Der GraphQL-Server wiederum ruft dann, für den Client transparent, weitere Backend-Systeme auf, um die entsprechenden Daten zu lesen oder zu schreiben.

Performanceoptimierungen im GraphQL-Server

Der GraphQL-Client kann im Rahmen des definierten Schemas den Umfang der Antwort frei bestimmen. Die Abarbeitung dieser Anfragen kann im Server schnell komplex werden, besonders wenn viele geschachtelte Felder angefragt werden. Wie zuvor erwähnt, stellen Zugriffe auf nachgeschaltete Backend-Systeme ebenfalls eine Herausforderung hinsichtlich der Performance dar.

Im Java-Bereich kann die Bibliothek GraphQL java-dataloader dazu genutzt werden, um all diese Zugriffe zu optimieren. Sie ist wie die GraphQL Java Tools einer JavaScript-Loader-Bibliothek von Facebook nachempfunden. Mit ihrer Hilfe können Zugriffe, beispielsweise Anfragen an eine Datenbank oder einen Web Service, zusammengefasst, parallelisiert und redundante Zugriffe eliminiert werden. Taucht ein Objekt beispielsweise in der Antwort des GraphQL-Servers mehrmals auf, so muss es nur ein einziges Mal geladen werden. Diese Optimierungen arbeiten immer innerhalb einer einzelnen Anfrage und nicht über mehrere hinweg.

Allerdings kann es auch bei performance-optimierten Implementierungen vorkommen, dass komplexe Anfragen zu einem Time-out bei der Verarbeitung führen. Ein möglicher Ansatz ist hier, die Anfragen vor deren Ausführung hinsichtlich ihrer Komplexität zu bewerten und gegebenenfalls mit der Rückgabe einer entsprechenden Fehlermeldung abzuweisen.

Abgrenzung zwischen GraphQL und REST

GraphQL bietet eine ganze Reihe interessanter Möglichkeiten. Für das eine oder andere Projekt im Web-Services-Umfeld wird die Entscheidung über den zu verwendenden API-Ansatz künftig zwischen REST und GraphQL fallen. Im Folgenden stellen wir die beiden Ansätze einander gegenüber.

API-Design

REST definiert einen kompletten Architekturstil, der grundsätzliche Konzepte wie Ressourcen, deren Repräsentationen und Zustandsänderungen adressiert. Aufgrund der abstrakten Definition herrschen diverse Meinungen, wie REST mittels HTTP korrekt umzusetzen ist.

Bei der Betrachtung des API-Designs verfügt ein RESTful API in der Regel über mehrere Endpunkte mit jeweils unterschiedlichen URLs: Bezogen auf das Webshop-Beispiel ist bei einem RESTful Ansatz ein Endpunkt /products/<ID> für die Query getProduct() möglich, der mittels GET abgefragt werden kann. Die Mutation rateProduct() kann hingegen über ein POST auf den URL / products/< productId>/ratings modelliert werden, wobei sich Kundennummer, Kommentar und Bewertung im Body des HTTP Requests befinden würden. Die Endpunkte eines RESTful API können mittels eines definierten Subsets der verfügbaren HTTP-Methoden angesprochen werden. Somit lassen sich bereits anhand der verwendeten HTTP-Methode und des URL Rückschlüsse auf die Semantik der Operation ziehen.

Bei einem RESTful API besteht nach dem HATEOAS-Prinzip die Möglichkeit, das gesamte API von einem Anfangspunkt aus verfügbar und mittels MIME Types und Links navigierbar zu machen. Weitaus mehr verbreitet ist jedoch der Einsatz von Tools wie Swagger und RAML, mit denen URLs, erlaubte HTTP-Methoden und Schemata eines RESTful API ebenfalls beschrieben werden können. Bei GraphQL hingegen ermöglicht das Schema die Definition des API an einer zentralen Stelle. Das erleichtert den Entwicklern die Arbeit, da alle benötigten Informationen zentral verfügbar sind, ohne dass hierfür zusätzliche Tools eingesetzt werden müssen.

Darüber hinaus ist die klare und umfassende Spezifikation von GraphQL ein nicht zu vernachlässigender Faktor. Teams mit wenig Erfahrung im Bereich RESTful API-Design ermöglicht sie, häufig auftretende Fehler zu vermeiden und schneller stabile, produktionstaugliche Lösungen zu entwickeln. Für das Datenmodell und die Operationen ist natürlich immer noch der Entwickler verantwortlich.

Da bei GraphQL nur ein einziger Endpunkt verwendet wird (nach Best Practice/graphql) und sowohl lesende als auch verändernde Operationen mit der potenziell gleichen HTTP-Methode ausgelöst werden können, gehen jedoch die Ausdrucksmöglichkeiten der HTTP-Semantik für einzelne Operationen verloren. Alle Queries und Mutations müssen in Form des Methodennamens und optional zusätzlicher Kommentare dokumentiert werden. Dies birgt neue Herausforderungen: So können bei GraphQL beispielsweise Methoden zum Löschen von Entitäten in unterschiedlichen APIs mit unterschiedlichen Präfixen beginnen (beispielsweise delete, remove …), da keine Konvention hierfür existiert.

Optimierung der zu übertragenden Datenmengen

Stellen wir uns folgendes Szenario vor: Während ein Client in seinem Request für ein Produkt sämtliche dem Produkt zugeordneten Bewertungen und deren Ersteller anfordert, ist ein anderer Client nur an der durchschnittlichen Bewertung des Produkts interessiert.

Bei REST ist das auf mehreren Wegen umsetzbar: Einerseits können Bewertungen und Kunden als eigenständige Ressourcen über jeweils eigene Endpunkte angeboten und dann per ID in der Response des Produkts referenziert werden. Für Clients, die auch an den Attributen der referenzierten Ressourcen interessiert sind, resultiert dies jedoch in mehreren HTTP-Aufrufen
(Underfetching).

Andererseits besteht die Möglichkeit, referenzierte Ressourcen mit in die Response aufzunehmen. Das führt jedoch zu einer pauschalen Erhöhung der zu übertragenden Datenmenge (Overfetching) und Latenz, Letzteres insbesondere, wenn Ergebnisse on the fly aggregiert werden müssen.

Eine weitere Möglichkeit ist es, den Umfang der Antwort über Query-Strings oder HTTP-Header zu steuern, um dem Client mehr Bestimmungsmöglichkeiten zu geben. Im Gegensatz zu GraphQL mit einer definierten Query-Syntax existiert bei REST jedoch keine einheitliche Herangehensweise.

Wie gezeigt, ist es bei einer GraphQL-Abfrage möglich, die benötigten Daten gemäß des Schemas zu spezifizieren. Dieser Mechanismus ist elementarer Bestandteil der Spezifikation. Gegenüber REST reduziert sich für den Client auch die Anzahl der benötigten Anfragen. Anstatt mehrere Ressourcen in Form verschiedener Endpunkte abrufen zu müssen, werden die Daten mit einem einzelnen Request abgefragt. Die Datenübertragung wird dadurch effizienter.

Caching von Anfragen

Ein RESTful API benötigt eigene Endpunkte für das Abfragen von Produkten, Bewertungen oder Kunden in Form von URLs wie /products/. Der Vorteil dabei ist, dass für jede adressierte Ressource individuell ein HTTP-Caching-Header gesetzt werden kann (Totty, Brian; Gourley, David; Sayer, Marjorie; Aggarwal, Anshu; Reddy, Sailu: „HTTP: The Definitive Guide“, O‘Reilly, 2009). Da die Semantik von HTTP erhalten bleibt, funktioniert das Caching der Responses bereits über existierende Mechanismen wie HTTP-Proxy-Caches auf Netzwerkebene oder Browsercaches im Client. Im Gegensatz dazu wird eine GraphQL-Schnittstelle nur über einen einzigen URL angeboten, hier kann das HTTP Caching de facto nicht genutzt werden. Bei GraphQL muss daher das Caching im Client selbst implementiert werden, was prinzipiell aufwendiger und fehleranfälliger als bei REST ist.

Eine GraphQL Best Practice rät dazu, für jede in einer Response zurückgegebenen Entität ein ID-Attribut mit anzugeben [15]. Es soll dem Client zumindest die Identifizierung gleicher Entitäten ermöglichen. Das Problem der zeitlich begrenzten Cachegültigkeit oder der Cacheinvalidierung wird dadurch jedoch nicht gelöst.

Konformität zur HTTP-Semantik

Eine HTTP-Anfrage mit der Methode GET ist eigentlich als Safe Method definiert. Daher sollte sie von den HTTP-Clients oder Proxies gefahrlos mehrfach gesendet werden können. Dies ist beispielsweise erforderlich, wenn die Verbindung währen der Client-Server-Kommunikation abbricht. REST und GraphQL unterscheiden sich zudem in ihrer Konformität zur HTTP-Semantik und so auch in der Nutzung der HTTP-Methoden. Betrachten wir zum Beispiel den lesenden Zugriff auf ein Produkt: Bei einem korrekt implementierten REST-API sollte ein Aufruf mittels HTTP GET, beispielsweise auf den Endpunkt / products/<ID>, keine verändernden Effekte haben. Bei GraphQL hingegen kann die Mutation für das Bewerten eines Produkts ebenfalls über HTTP GET abgesetzt werden, da Requests unabhängig von deren Semantik sowohl über GET als auch mittels POST übertragen werden dürfen.

Am Beispiel des Webshops erklärt: Falls der HTTP-Server Anfragen zum Bewerten von Produkten an einem GET-Endpunkt annimmt, könnten HTTPS-Clients oder Proxies GET Requests irrtümlich mehrfach absetzen. Sie nehmen fälschlicherweise an, es handele sich um eine Safe Method.

Fehlerbehandlung

Bei der Fehlerbehandlung kommt die Nichtkonformität von GraphQL zur HTTP-Semantik erneut zum Tragen: Während ein RESTful API die definierten HTTP-Statuscodes nutzt, werden bei GraphQL die bei einer Anfrage aufgetretenen Fehler im HTTP-Body der Antwort zurückgegeben.

Die GraphQL-Spezifikation legt fest, wie die Fehlerinformation zu strukturieren ist: Ein zusätzliches Element errors enthält eine Liste von Fehlern, deren Inhalt jeweils in Form von Schlüssel-/Wert-Paaren übermittelt wird. Zwingend erforderlich ist dabei lediglich der Schlüssel message, dessen Wert die eigentliche Fehlermeldung beinhaltet.

Einheitliche Statuscodes werden von GraphQL hingegen nicht spezifiziert. Dies erschwert den Einsatz von HTTP-Client Bibliotheken und HTTP-Clients im Allgemeinen. Außerdem impliziert es zusätzlichen Implementierungsaufwand auf der Clientseite, da die Behandlung der Fehler bei jeder Schnittstelle eigens umgesetzt werden muss.

Versionierbarkeit der Schnittstelle

Ein oft unterschätzter Aspekt beim Design von Web-Services-Schnittstellen ist die Versionierbarkeit. Bei einem RESTful API sind mehrere Ansätze weit verbreitet: Entweder wird die Version mit in den URL aufgenommen (beispielsweise /v1/products/) oder es werden Custom-HTTP-Header genutzt (beispielsweise X-API-VERSION). Über den Content Type des Accept-Headers kann ebenso auf eine API-Version Bezug genommen werden.

GraphQL besitzt ebenfalls keinen eigenen Mechanismus, um mehrere Versionen eines API gleichzeitig anzubieten. Prinzipiell möglich ist es aber, die zuvor für REST erwähnten Ansätze auch für GraphQL einzusetzen. Zusätzlich können bei GraphQL Felder als deprecated markiert werden. Diese Markierung lässt sich auch mit den bereits erwähnten Technologien Swagger und RAML bewerkstelligen.

Der GraphQL-Server besitzt im Vergleich zu einem REST-Server mehr Wissen darüber, welche Informationen von seinen Clients benötigt werden, da jedes Feld explizit angefordert wird. Anhand der Statistik über alle GraphQL-Anfragen kann bestimmt werden, ob und von welchen Clients ein bestimmtes Feld benutzt wird. Wird ein Feld nicht benutzt, kann es mit geringerem Risiko entfernt werden.

Zusammenfassung

Wie wir gesehen haben, bietet GraphQL ein zentrales Schema. Es beinhaltet sowohl die Definition des Objektgraphen als auch die auf ihm verfügbaren Operationen. Das Schema beschreibt den Vertrag zwischen Client und Server, davon ausgehend kann der Client dann flexibel Anfragen stellen und genau die Daten anfordern, die er benötigt.

Es gibt nicht den einen API-Ansatz, der für alle Projekte die optimale Lösung darstellt. Diese Regel gilt auch für REST und GraphQL. Die Vorteile von GraphQL kommen besonders bei solchen Projekten zum Tragen, bei denen mehrere Clients unterschiedliche Teile des Datenmodells anfordern oder in denen die zu übermittelnde Datenmenge über das API ein kritischer Faktor ist. Durch die Tatsache, dass zusammenhängende Daten mit einer einzigen Anfrage abgerufen werden können, lassen sich bei GraphQL Clients eleganter schreiben.

Hinsichtlich der Versionierung des API nehmen sich die beiden Ansätze nicht viel, auch wenn der GraphQL-Server über mehr Wissen verfügt, welche Felder von den Clients tatsächlich verwendet werden. Im Gegensatz zu einem RESTful API lassen sich bei GraphQL HTTPCaches im Client oder auf Netzwerkebene nicht nutzen. Auch die Fehlerbehandlung fällt im Vergleich zu REST schwieriger, da aufgrund der fehlenden Konformität zur HTTP-Semantik die Fehlerbehandlung von Standard-HTTP-Clients nicht verwendet werden kann.

Ausblick

Neben Facebook setzen bereits heute weitere Unternehmen wie GitHub oder XING auf GraphQL. Viele weitere Unternehmen arbeiten an entsprechenden APIs. Aktuelle Themen beschäftigen sich unter anderem mit einer flexibleren Übermittlung der GraphQL-Serverantwort zum Client. Beispielsweise wird damit experimentiert, die Antwort in mehreren Schritten zu übermitteln. Ein Client kann dann die ersten Ergebnisse bereits verarbeiten, während im Hintergrund weitere übermittelt werden.

Das neue GraphQL-Feature der Subscriptions steht hingegen kurz vor der Veröffentlichung: Mit seiner Hilfe lassen sich GraphQL-Clients in Form eines Publish/Subscribe-Mechanismus für Modelländerungen am GraphQL-Server registrieren. Ebenso sollte beobachtet werden, ob und in welcher Form RESTful APIs von Neuerungen des HTTP-Protokolls profitieren. Insbesondere durch den Streamingmechanismus von HTTP2 könnten bereits bestehende RESTful APIs deutlich performanter angesprochen werden. Eines ist auch sicher: Sowohl mit REST als auch mit GraphQL lassen sich schon heute gute und schlechte APIs bauen.

Verwandte Themen:

Geschrieben von
Alexander Kirschner
Alexander Kirschner
Alexander Kirschner entwickelt Software bei der jambit GmbH im Herzen Münchens. Nach mehreren Jahren im Bereich C-/C++-Automotive-Infotainment-Entwicklung beschäftigt er sich in letzter Zeit im Bereich Java wieder vermehrt mit verteilten Systemen und insbesondere Microservice-Architekturen. Seine weiteren Schwerpunkte liegen im Bereich agiler Entwicklungsprozesse sowie Continuous Integration and Delivery.
Gernot Pointner
Gernot Pointner
Gernot Pointner ist als Entwickler bei der jambit GmbH beschäftigt. Nachdem er in den letzten Jahren Erfahrung mit verschiedensten Technologien sammeln durfte, beschäftigt er sich zurzeit mit verteilten Webservice-Architekturen im Java-Umfeld. Zu seinen weiteren Interessen zählen Concurrency und funktionale Programmierung.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: