GraphQL in der Praxis
25.06.2024, Stefanie Erne

Bei der Implementierung der neuen Buchungsplattform für SimpleTrain haben wir uns für GraphQL für die Client-Server-Kommunikation entschieden. In diesem Beitrag tauche ich tiefer in die Welt von GraphQL ein. Ich erläutere, was GraphQL ist und warum es für unser Projekt die ideale Wahl war.

Was ist GraphQL?

GraphQL ist eine Technologie, die es ermöglicht, Daten effizient zwischen Client und Server auszutauschen. Es handelt sich dabei um eine Abfragesprache, die es dem Client erlaubt, genau die Daten anzufordern, die er benötigt. Zudem können mehrere Ressourcen in einer einzigen Anfrage kombiniert werden, was die Anzahl der Netzwerkaufrufe reduziert und die Performance verbessert. Das ist meines Erachtens ein klarer Vorteil gegenüber REST, wo die Datenstruktur meist vom Server vorgegeben ist und ressourcenübergreifende Abfragen schwierig sind.

Ein konkretes Beispiel

Eine Kund:in möchte ihre Bestellung auf der SimpleTrain Buchungsplattform ansehen. Mit REST müsste das Frontend mehrere Anfragen an den Server senden, um alle benötigten Informationen zu erhalten. Es bräuchte separate Anfragen für die Bestellung (order), die Reisen (journeys) und die Reisenden (travellers). Mit GraphQL kann das Frontend eine einzige Anfrage senden, die alle benötigten - und nur die benötigten - Informationen enthält.

Die GraphQL-Query könnte etwa so aussehen:

query GetOrder {
  order(id: "123"){
    id
    description
    journeys {
      from
      to
      date
    }
    travellers {
      id
      name
      age
      reduction
    }
  }
}

Hier definieren wir eine Query namens GetOrder (der Name ist frei wählbar) und fragen die Bestellung (order) mit der ID 123 ab. Wir interessieren uns für die ID, die Beschreibung, die Reisen (journeys) und die Reisenden (travellers).

Warum GraphQL?

Die Query-Sprache von GraphQL ist sehr mächtig und ermöglicht komplexe Abfragen, die genau die benötigten Daten liefern. Das macht es performanter, da nur die benötigten Daten übertragen werden und sie einer einzigen Anfrage an den Server kombiniert werden können.

Ein weiterer Vorteil von GraphQL ist die Definition eines stark typisierten Schemas. Das Schema definiert, welche Objekte welche Felder haben, wie sie miteinander verbunden sind, und was für Typen die Felder haben. Dank des Schemas ist zu Entwicklungszeit bekannt, welche Daten der Client anfordern kann.

Sortieren, Filtern und Paginieren von Daten ist mit GraphQL ebenfalls vorgesehen. Im Schema ist festgelegt, welche Objekte sortiert, gefiltert oder paginiert werden können. Der Client kann dann in der Query angeben, wie die Daten konkret sortiert, gefiltert oder paginiert werden sollen, wenn überhaupt. Die Umsetzung erfolgt auf dem Server und ist abhängig von der gewählten Technologie und GraphQL-Bibliothek. Mehr dazu später.

Supergraphen sind ein weiterer Vorteil von GraphQL. Mit Supergraphen können mehrere GraphQL-Services zu einem einzigen Schema kombiniert werden. So können etwa Daten aus vielen verschiedenen Microservices in einer einzigen Anfrage kombiniert werden, ohne dass der Client wissen muss, welche Daten aus welchem Service kommen.

Query, Mutation, Subscription

Bei REST-Schnittstellen wird die Art der Anfrage durch die HTTP-Action bestimmt. Beispielsweise holt GET Informationen vom Server und POST erstellt üblicherweise einen neuen Eintrag auf dem Server. Das ist allerdings eher eine Konvention und nicht alle REST-APIs halten sich daran.

Bei GraphQL werden alle Anfragen an denselben Endpunkt beim Server gesendet. Die Art der Aktion ist im GraphQL Statement hinterlegt. Das ist vergleichbar mit SQL, wo beispielsweise SELECT Daten abruft und INSERT Einträge erstellt.

Die drei Operationen in der GraphQL-Welt sind Query, Mutation und Subscription.

  • Eine Query ist eine Leseoperation, die Daten vom Server anfordert
  • Eine Mutation ist eine Schreiboperation, die Daten auf dem Server ändert
  • Eine Subscription ist eine Operation, die es dem Client ermöglicht, auf Änderungen zu reagieren, die auf dem Server auftreten

Eine Query beschreibt, welche Daten der Client erhalten möchte. Sie kann optional weitere Parameter enthalten, um die Daten zu filtern oder zu sortieren. So ein Beispiel haben wir weiter oben schon gesehen.

Eine Mutation beschreibt, welche Daten der Client ändern möchte. Beispielsweise wird so eine Bestellung aufgegeben oder ein Benutzerkonto erstellt. Eine Mutation hat optionale Parameter, welche die Änderungen beschreiben (etwa den Namen des Benutzers). Sie definiert – wie bei einer Query – an welchen Feldern der Antwort der Client interessiert ist (z.B. die vom Server erstellte Benutzer ID).

Das folgende Beispiel einer Mutation zeigt an einem hypothetischen Beispiel, wie Konten erstellt werden. Es wird der Name und die E-Mail-Adresse gesetzt und der Client erwartet vom Server die ID des Benutzers zurück. Die ID wird in diesem Beispiel durch den Server erstellt.

mutation CreateNewUser {
  createUser(input: {
    name: "John Doe",
   email: "[email protected]"
  }) {
    id
  }
}

Nachteile

Natürlich hat auch GraphQL Nachteile. Ein Nachteil ist, dass die Abfragen komplexer sind als bei REST. Man muss sich genauer mit der Query-Sprache auseinandersetzen, sich mit dem Schema vertraut machen und wissen, welche Daten der Client benötigt. Das kann anfangs etwas aufwändiger sein, als einfach eine REST-Schnittstelle zu verwenden.

Ohne geeignete Optimierungen können GraphQL-Abfragen, die auf verschachtelte Daten zugreifen, das N+1-Abfrageproblem verursachen. Das heisst, dass für jede verschachtelte Beziehung eine zusätzliche Datenbankabfrage ausgeführt wird. Das kann die Leistung beeinträchtigen, da es zu sehr vielen Datenbankabfragen kommen kann. Oder es führt zu einer grossen Datenmenge, die über das Netzwerk übertragen werden muss.

Umsetzung

Nach der kurzen Einführung zu GraphQL gehe ich nachfolgend auf die konkrete Umsetzung im Projekt mit SimpleTrain ein.

Verwendete Technologien

Für die Server-Seite verwenden wir Hot Chocolate von ChilliCream, eine GraphQL-Server-Bibliothek für .NET. Hot Chocolate ist sehr leistungsfähig und bietet viele Funktionen, welche die Entwicklung von GraphQL-Services erleichtern. Das Schema wird in C# definiert: normale Datenstrukturen wie Klassen oder Records können zur Typendefinition verwendet werden, und auch Mutationen und Queries werden in C# implementiert. Die Implementierung des GraphQL-Servers mit Hot Chocolate fand ich sehr angenehm und intuitiv. Dank der guten Dokumentation mit vielen Beispielen war die Einarbeitungszeit kurz.

Für die Client-Seite verwenden wir Apollo Client (bzw. Apollo Angular), eine weitverbreitete GraphQL-Client-Bibliothek für JavaScript/ TypeScript. Zusätzlich verwenden wir GraphQL-Codegen, um TypeScript-Typen und Angular-Services aus dem GraphQL-Schema und den Client-seitigen Queries zu generieren.

Filtern, Sortieren und Paginieren von Daten

Ein integraler Bestandteil von GraphQL ist die Möglichkeit, Daten zu filtern, zu sortieren und zu paginieren. Mit Hot Chocolate ist es einfach, diese Funktionalitäten zu implementieren. Dazu genügt es, beim gewünschten Feld in der Schema-Definition die entsprechenden Annotationen hinzuzufügen. Hot Chocolate wendet dann die Filter, Sortier- und Paginierungs-Operationen automatisch auf das IQueryable an.

In der Query, welche der Client sendet, kann er dann die konkret gewünschten Filter und Sortier-Parameter angeben.

Ein Client holt alle Schweizer Bahnhöfe und erwartet die Resultate nach Namen sortiert:

query GetSwissStations {
  stations(where: {country: {eq: "CH"}}, order: { name: ASC}) {
    items {
      id
      name
    }
  }
}

Die Implementierung auf der Server-Seite könnte etwa so aussehen:

public class StationQuery {
    private MyDbContext DbContext { get; }

    public StationQuery(IDbContextFactory<MyDbContext> dbContextFactory)
    {
        DbContext = dbContextFactory.CreateDbContext();
    }
 
    [UseOffsetPaging]
    [UseFiltering<StationFilterType>]
    [UseSorting]
    public IQueryable<Station> Stations(CancellationToken ctx = default) => DbContext.Stations;
  
}

In diesem Beispiel wird die Stations-Query definiert, welche alle Bahnhöfe zurückgibt. Die Annotationen UseOffsetPaging, UseFiltering und UseSorting aktivieren die entsprechenden Funktionen. Die Filter- und Sortier-Parameter werden automatisch auf das IQueryable angewendet, wir müssen also im Normalfall nicht explizit die Filter- und Sortier-Logik implementieren.

Berechtigung und Identifizierung

Ein wichtiger Bestandteil jeder API ist die Informations-Sicherheit. Nur Berechtigte sollten auf bestimmte Daten zugreifen können.

Hot Chocolate bietet, ähnlich wie beim Sortieren und Filtern, auch für die Berechtigung eine Annotation. Diese wird auf Feldern oder Mutationen angewendet.

Optional kann eine Liste von Benutzerrollen angegeben werden. Nur diese sind dann berechtigt, auf das entsprechende Feld zuzugreifen. Werden keine Rollen angegeben, wird nur verlangt, dass der Zugriff authentifiziert ist.

Die Identifizierung selbst ist nicht Teil von Hot Chocolate bzw. GraphQL. Dafür greift Hot Chocolate auf ASP.NET zurück.

Im konkreten Projekt erfolgt die Identifizierung mit OpenID Connect (OIDC). Die Angular Applikation holt sich beim Identifizierungs-Server – wir nutzen dafür Zitadel – ein JSON Web Token (JWT). Dieses Token wird mit jeder GraphQL-Anfrage an den Server mitgeschickt. Auf dem Server ist ASP.NET so konfiguriert, dass es dieses Token prüft und vom Identifizierung-Server die Rollen abgleicht. So können wir sicherstellen, dass gewisse Daten nur berechtigten Personen zugestellt werden.

Abschluss

GraphQL ist eine mächtige Technologie, die es ermöglicht, Daten effizient zwischen Client und Server auszutauschen. Es bietet viele Vorteile gegenüber REST. Die wichtigsten sind die Möglichkeit, genau die benötigten Daten in einer einzigen Anfrage zu erhalten, die starke Typisierung und die einfache Implementierung von Filtern, Sortieren und Paginieren.

Für die Implementierung der neuen Buchungsplattform für SimpleTrain war GraphQL die ideale Wahl. Es ermöglicht uns, die Daten effizient und performant zwischen Client und Server auszutauschen, und bietet viele Funktionen, welche die Entwicklung erleichtern. Wir generieren etwa die TypeScript-Typen und Angular-Services automatisch aus dem GraphQL-Schema und den Client-seitigen Queries. So können wir sicherstellen, dass die Typen auf der Client-Seite immer aktuell sind.

Möchtest du mehr über GraphQL erfahren? Dann findest du in der offizielle GraphQL-Dokumentation viele weitere nützliche Informationen.