LEAN-CODERS Logo

Blog

LEAN-CODERS

GraphQL: Warum auf den modernen API-Standard wechseln?

GraphQL - A new view on your data

Warum GraphQL?

Ich bin davon überzeugt, und ich möchte auch dich überzeugen: Jede halbwegs umfangreiche REST-API wird zu einem schlechten Nachbau einer GraphQL-API.

Any sufficiently complicated RESTful API contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of GraphQL.

Schreib' einfach mal ne API zum Anzeigen von Posts in einem Blog. Du wirst vermutlich folgende Methoden designen:

GET allPosts()
GET post(postId)
GET comments(postId)

Es wird wohl nicht lange dauern, und aus allPosts() wird so etwas entstehen:

GET allPosts(skip, take, filterFields[], filterValues[], fieldsToInclude[])

Damit du keinen eigenen Roundtrip fürs Laden der Kommentare brauchst, wirst du vielleicht auch die post()-Methode etwas abändern:

GET post(postId, commentsSkip, commentsTake)

So ähnlich sehen die meisten APIs im Enterpriseumfeld aus. Eher individuell, selten einheitlich. Gibt es nun einen Weg, wie man diese immer wiederkehrenden Anforderungen an die Flexibilität von APIs nicht gleich von Haus aus in einem Standard integrieren kann? Findige Entiwckler:innen bei Facebook haben sich überlegt, wie man API-Designs streamlinen und vereinfachen kann. Herausgekommen dabei ist GraphQL - sozusagen der nächste Evolutionsschritt in Sachen APIs.

Was ist GraphQL?

Dabei ist festzuhalten, dass GraphQL nur ein Standard ist. Zwar gibt es entsprechende Libraries wie zB Apollo in denen Teile davon angenehmerweise schon ausprogrammiert sind, aber das ist natürlich Technologieabhängig.

GraphQL ist eine Sprache, um APIs mit einer "Query Language" anzusprechen. Clientseitig sieht eine API-Abfrage somit eher wie eine "Datenbankabfrage" aus, während ein klassischer REST-Aufruf wie der Aufruf einer Methode aussieht. Das klingt vielleichterst mal marginal, ist aber eine große Veränderung im Mindset, wie wir mit APIs umgehen können.

Eine GraphQL-Query könnte zB so aussehen:

{posts (author:"alice", skip: 10, take: 10) {
    id,
    author,
    content,
    headline,
    tags,
    comments {
      author,
      comment,
    }
  }
}

Also wir fetchen hier die Posts 11 bis 20 von "alice" und selektieren die Felder "id", "author", "content", "headline", und "tags". Gleichzeitig laden wir Kommentare zu den Posts, und von den Kommentaren laden wir die Felder "Author" und "comment".

Dabei kann GraphQL als generische "Datenzugriffs-Schicht" gesehen werden. Woher die Comments kommen, ist völlig belanglos und wird im Backend ausprogrammiert. Haben wir eine Dokumentenbasierte Datenbank, werden die Comments vielleicht direkt im Post-Dokument mitgespeichert. Haben wir eine relationale DB, stehen die Comments vielleicht in einer separaten Tabelle. Theoretisch können die Comments aber auch in einem Textfile abgespeichert sein, oder über eine andere API von einem Drittsystem abgerufen werden - aus Sicht des abfragenden Codes ist es jedenfalls völlig gleich- hier sehen wir nur, dass es Comments gibt, und dass die Comments offenbar eine Beziehung zu den Posts haben.

Vor- und Nachteile gegenüber REST-APIs

Optimierte Datenabfragen aus Client-Sicht

Nie wieder overfetching und underfetching.

In der GraphQL-Abfrage geben wir explizit an, welche Felder, Subentitäten, und Felder der Subentitäten wir abfragen wollen. In jedem halbwegs vernünftig implementierten Backend werden dann tatsächlich nur die requesteten Felder ausgeliefert. Damit ist es leicht möglich, dieselbe Schnittstelle für mehrere Abfragen verschiedenster Granularität zu verwenden.

zB benötigt eine Post-Übersichtsseite vielleicht nur eine Headline und die Tags, während die Detailseite eines Posts den vollen Inhalt samt Comments abfragen muss. Aus GraphQL-Sicht kann man in beiden Fällen "posts" abfragen - nur die Liste der abgefragten Felder ändert sich.

Weniger Aufwand im Backend durch stärkere Entkopplung

Dasselbe Konzept hat auch große, oftmals nicht bedachte Auswirkungen im Backend: Als API-Entwickler:in kann ich nämlich als Rückgabewerte in meiner API stets eine möglichst große Anzahl von Feldern definieren. Dabei brauche ich keine Angst zu haben, dass es inperformant wird, denn es werden letzendlich nur die Felder zurückgegeben, und auch nur jene Subentitäten abgefragt, die das Frontend auch wirklich benötigt. (Richtige Programmierung im Backend vorausgesetzt ...)

Das hat ganz simple, aber mächtige Implikationen: Werden in der ursprünglichen Anforderung zB die "Tags" in der Übersichts-Seite nicht benötigt, aber später von den User:innen trotzdem gefordert, so reicht eine rein frontendseitige Anpassung. Das Backend muss nicht erweitert und neu deployed werden. Das spart auf Dauer potenziell massiv Aufwand und Komplexität.

Parallelism built in

Nehmen wir an, wir schreiben eine komplexe GraphQL-Abfrage, die mehrere Subentitäten abfragt, wobei einzelne Subentitäten verschiedene Datenquellen haben. Posts und Comments liegen vielleicht in einer Datenbank. Andere Informationen müssen aber vielleicht von einem anderen Server abgerufen werden, wieder als API, oder liegen in Form von Dateien irgendwo in einem Netzwerklaufwerk, das mit dem Server verbunden ist.

Gute GraphQL-Frameworks bieten komfortable Mechanismen zum Handling von Subentitäts-Abfragen. In Apollo unter NestJS kann das Laden eines Posts mitsamt Comments und Revisions zB so aussehen:

  @Query(returns => Post)
  async post (@Args('id', { type: () => Int }) id: number): Promise<Post> {
    return // Lade Post aus der DB
  }

  @ResolveField()
  async comments(@Parent() post: Post) {
    return // Lade Comments mit comment.postId == post.Id
  }

  @ResolveField()
  async revisions(@Parent() post: Post) {
    return // Lade Revisions mit revision.postId == post.Id
  }

Apollo prüft nun automatisch, ob auch Comments und/oder Revisions im GraphQL-Query abgefragt werden, und wenn ja, dann werden diese Subentitäten mit der jeweiligen async Methode resolved. Und das passiert von Haus aus parallel.

Dies ist nur eine Möglichkeit, wie man die Sache angehen kann. In einer relationalen Datenbank möchte man vermutlich ohne ResolveField arbeiten und direkt einen JOIN absetzen. In anderen Szenarien, insbesondere wenn Daten aus verschiedenen Datenquellen geladen werden sollen, ist ein Daten-Join über ResolveField aber eine sehr bequeme/einfache und performante Lösung, weil die Datenquellen hier out-of-the-box parallel abgefragt werden.

Bequemerweise werden die Results der Abfragen dann durch Apollo automatisch in das übergeordnete Modell (hier: Post) eingefügt.

Deprecation-Feature

Besonders bei großen, langlebigen APIs wird ein guter Umgang mit Deprecation und Weiterentwicklung unterschätzt. IT-Systeme entwickeln sich ständig weiter, und ich kenne keine genügend komplexe und langlebige API, die nicht Probleme mit Deprecation hätte. Meist ist der Ausweg, dass mehrere verschiedene Versionen von APIs (zumindest übergangsweise) parallel betrieben werden müssen.

Der GraphQL-Standard bietet hier eine eigene @deprecated-directive, mit der man leicht anzeigen kann, welche Felder veraltet sind (mit einem optionalen Reason dazu).

Die allermeisten GraphQL-Clients lesen diese Direktive aus und geben zB ein Warning aus, dass man ein depricated Field verwendet.

Damit lässt sich im Idealfall sogar verhindern, mehrer (verschiedene) Versionen von APIs parallel zu betreiben. Es bleibt bei einer einzigen API, die mit der Zeit immer weiter verändert/erweitert wird.

Subscription-Feature

Der GraphQL-Standard definiert auch ein Subscription-Feature für Push-Notifications. Damit ist aus Clientsicht für eine solche Funktionalität keine weitere Technologie nötig.

GraphQL als zentrale Schaltstelle / Datenabfrageschicht

Vorher wurde es schon ein bisschen angedeutet, aber hier nochmal auf einem Blick: GraphQL ändert, wie wir eine API interpretieren. Wir sehen eine API nicht mehr (nur) als Sammlung von Methoden, die irgendetwas zurückliefern. Vielmehr sehen wir eine GraphQL-API als "Datenzugriffs-Schicht":

  • Syntax mit starkem Fokus auf Datenobjekte, deren Felder, und deren Beziehungen zueinander

  • Einheitliche / vereinigte Sicht auf Daten aus verschiedensten Datenquellen / Systemen

  • Performant durch Parallelität der Abfrage und einfache Kontrolle darüber, welche Daten übers Netz tatsächlich übertragen werden

  • Das Frontend/Client muss nicht wissen, aus welchen Datenquellen einzelne Felder oder Subentitäten stammen. Das Frontend fragt einfach ab, was es braucht - alleine das Backend kümmert sich dann darum, die entsprechenden Quellen abzufragen

  • Generell spart man sich Roundtrips zum Server bzw. kann man Abfragen leichter als mit REST so gestalten, dass man insgesamt weniger Calls benötigt, und trotzdem ein gutes API-Design erhält

Typing für angenehme & sichere Entwicklung

Types sind "First Class Citizens" bei GraphQL. Dies macht die Entwicklung nicht nur sicherer, sondern auch angenehmer. In den meisten GraphQL-IDEs werden die Types natürlich auch mitberücksichtigt und validiert. Außerdem ermöglicht das Typing (insbesondere von komplexen Objekten/Models) sehr angenehme AutoCompletion-Featurs in den IDEs

Ein paar Nachteile & Challenges

  • Wie auch bei REST-APIs muss man auch bei GraphQL auf entsprechenden Authorization/Authentication sowie Parametervalidierung achten. Nichts hindert einen Client davor, eine Abfrage a la
    ...(skip:0, take: 1000000)
    zu erstellen. Dies muss backendseitig gehandhabt werden

  • "Zu" komplexe Abfragen möglich: Obwohl gerade die Performance bei "sachgerechter" Verwendung von GrapQL-Abfragen eine Stärke ist, gibt es hier auch eine Pitfall: Clients haben nun die Möglichkeit, entsprechend komplexe Queries abzusetzen, welche in langen Ladezeiten resultieren.

  • Die Lernkurve von GraphQL gilt derzeit als schwieriger als die von REST-APIs

  • Die GraphQL-Community (und entsprechend auch die Anzahl an Beispielen, Tutorials, etc.) ist kleiner als die von REST-Pendants

  • "Overkill für simple Apps": In der Recherche zu diesem Artikel bin ich auf Aussagen gestoßen, dass ein Einsatz von GraphQL sich eher für größere APIs lohnt. Ich persönlich teile diese Meinung nicht. Mit dem aktuellen Tooling und einem grundlegenden Training ist die Implementierung einer GraphQL-API nicht komplexer oder langwieriger als die Entwicklung einer REST-API. Aber man genießt dafür einige Vorteile, die sich vor Allem dann zeigen, wenn die API länger benutzt, gewartet und erweitert wird.

Training & Unterstützung bei der Implementierung

Benötigst du Unterstützung bei der Konzeption oder Implementierung einer GraphQL-Schnittstelle? Oder würdest du dein Knowhow und um dieses Thema auf schnellstem Wege im Rahmen eines Workshops, Trainings oder durch eine gemeinsame Implementierung eines Prototyps upgraden? Kontaktiere uns jetzt über das folgende Kontaktformular und lass uns losstarten :)

Zurück zur Übersicht

Get inTouch

Diese Seite wird durch reCAPTCHA geschützt. Es gelten Googles Datenschutzbestimmungen und Nutzungsbedingungen.
Adresse:
Hainburger Straße 33, 1030 Wien