Aufbau von Hochleistungs-GraphQL-Servern in Rust mit async-graphql, die Typsicherheit gewährleisten
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einführung
In der aktuellen Landschaft der Webentwicklung bilden APIs das Rückgrat moderner Anwendungen. Während REST lange Zeit das dominierende Paradigma war, hat GraphQL aufgrund seiner Flexibilität, Effizienz und Benutzerfreundlichkeit schnell an Bedeutung gewonnen. GraphQL ermöglicht es Clients, genau die Daten anzufordern, die sie benötigen, wodurch Over-Fetching und Under-Fetching reduziert werden, was besonders für komplexe Anwendungen und mobile Clients von Vorteil ist. Wenn es um den Aufbau von hochleistungsfähigen, resilienten Backends geht, sticht Rust mit seiner unübertroffenen Speichersicherheit, Nebenläufigkeit und Geschwindigkeit hervor. Die Kombination der Leistungsfähigkeit von GraphQL mit den Garantien von Rust schafft eine überzeugende Lösung für den Aufbau von APIs der nächsten Generation. Dieser Artikel befasst sich damit, wie wir async-graphql
, eine leistungsstarke GraphQL-Bibliothek für Rust, nutzen können, um serverseitige GraphQL-APIs zu erstellen, die nicht nur hochperformant, sondern auch inhärent typsicher sind und somit den Weg für robustere und besser wartbare Systeme ebnen.
Kernkonzepte verstehen
Bevor wir uns in die praktische Implementierung vertiefen, definieren wir kurz einige Schlüsselbegriffe, die für den Aufbau von GraphQL-Diensten mit Rust und async-graphql
zentral sind.
- GraphQL: Eine Open-Source-Abfrage- und Datenmanipulationssprache für APIs und eine Laufzeitumgebung zur Erfüllung dieser Abfragen mit vorhandenen Daten. Im Gegensatz zu REST, bei dem möglicherweise mehrere Endpunkte für verschiedene Datenaggregationen benötigt werden, verwendet GraphQL einen einzigen Endpunkt und ermöglicht es Clients, die genaue Datenstruktur zu spezifizieren, die sie benötigen.
- Schema Definition Language (SDL): Eine sprachunabhängige Syntax, die zur Definition der Struktur einer GraphQL-API verwendet wird, einschließlich Typen, Feldern, Beziehungen und Operationen (Queries, Mutations, Subscriptions).
- Query: Eine Operation zum Lesen von Daten vom Server.
- Mutation: Eine Operation zum Schreiben oder Modifizieren von Daten auf dem Server.
- Subscription: Eine Operation zum Empfangen von Echtzeitaktualisierungen vom Server, die typischerweise über WebSockets implementiert wird.
- Resolvers: Funktionen oder Methoden auf dem Server, die für das Abrufen der Daten verantwortlich sind, die einem bestimmten Feld im GraphQL-Schema entsprechen.
- Typsicherheit: Die Eigenschaft einer Programmiersprache, Typfehler zu verhindern und sicherzustellen, dass Operationen nur mit Daten vom richtigen Typ ausgeführt werden. Rusts starkes statisches Typsystem bietet ausgezeichnete Typsicherheit zur Kompilierzeit.
async-graphql
: Eine beliebte, performante und funktionsreiche GraphQL-Serverbibliothek für Rust. Sie legt Wert auf asynchrone Operationen, Typsicherheit durch das starke Typsystem von Rust und eine idiomatische Rust-API.async/await
: Rusts integrierter Mechanismus zum Schreiben von asynchronem Code, der gleichzeitige Operationen ohne den Overhead traditioneller Threading-Modelle ermöglicht. Dies ist entscheidend für hochperformante I/O-gebundene Anwendungen wie Netzwerkspeicher.
Aufbau eines typsicheren GraphQL-Servers
async-graphql
ermöglicht es uns, unser GraphQL-Schema direkt anhand von Rust-Strukturen und Enums zu definieren, die mit prozeduralen Makros versehen sind. Dieser Ansatz erzwingt von Natur aus Typsicherheit. Lassen Sie uns ein Beispiel für den Aufbau einer einfachen API zur Verwaltung von Büchern durchgehen.
Projekteinrichtung
Erstellen Sie zunächst ein neues Rust-Projekt und fügen Sie die erforderlichen Abhängigkeiten in Ihre Cargo.toml
hinzu:
[package] name = "book_api" version = "0.1.0" edition = "2021" [dependencies] async-graphql = { version = "7.0", features = ["apollo_tracing", "tracing"] } # Fügen Sie Tracing für bessere Debugging hinzu async-graphql-poem = "7.0" # Connector for Poem web framework poem = { version = "1.0", features = ["static-files", "rustls", "compression"] } # Ein einfaches und schnelles Web-Framework tokio = { version = "1.0", features = ["full"] } # Asynchrone Laufzeit serde = { version = "1.0", features = ["derive"] } # Für (De)Serialisierung, oft nützlich uuid = { version = "1.0", features = ["v4", "serde"] } # Für eindeutige IDs
Definieren unserer Datenmodelle
Wir beginnen mit der Definition der Rust-Strukturen, die unsere Daten repräsentieren. Diese werden natürlich unseren GraphQL-Typen zugeordnet.
use async_graphql::{Enum, Object, ID, SimpleObject}; use uuid::Uuid; #[derive(SimpleObject, Debug, Clone)] struct Book { id: ID, title: String, author: String, genre: Genre, published_year: i32, } #[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] #[graphql(remote = "Genre")] // Ermöglicht die Definition des Enums in einem separaten Modul, falls erforderlich enum Genre { Fiction, NonFiction, ScienceFiction, Fantasy, Mystery, } // In einer realen Anwendung würden Sie wahrscheinlich eine Datenbank verwenden. // Zur Vereinfachung verwenden wir einen In-Memory-Speicher. struct BookStore { books: Vec<Book>, } impl BookStore { fn new() -> Self { BookStore { books: Vec::new() } } fn add_book(&mut self, book: Book) { self.books.push(book); } fn get_book(&self, id: &ID) -> Option<&Book> { self.books.iter().find(|b| &b.id == id) } fn get_all_books(&self) -> Vec<Book> { self.books.clone() // Klonen zur Vereinfachung; Referenzen oder Arc in einer realen Anwendung in Betracht ziehen } }
Beachten Sie die Makros #[derive(SimpleObject)]
und #[derive(Enum)]
. Diese Makros werden von async-graphql
bereitgestellt und generieren automatisch die erforderlichen GraphQL-Schema-Definitionen aus unseren Rust-Typen, wodurch die Typenkonsistenz zwischen unserer Backend-Logik und der GraphQL-API gewährleistet wird. Der Typ ID
von async-graphql
wird dem GraphQL-Skalar ID
zugeordnet, der als String serialisiert wird.
Definieren von Queries
Als Nächstes definieren wir unser Root-Query
-Objekt. Diese Struktur enthält die Resolver-Methoden zum Abrufen von Daten.
use async_graphql::{Context, Object, ID}; use std::sync::Arc; // Zum sicheren Teilen von BookStore über Threads hinweg pub struct Query; #[Object] impl Query { /// Gibt eine Liste aller Bücher zurück. async fn books(&self, ctx: &Context<'_>) -> Vec<Book> { let store = ctx.data::<Arc<BookStore>>().expect("BookStore not found in context"); store.get_all_books() } /// Gibt ein einzelnes Buch nach seiner ID zurück. async fn book(&self, ctx: &Context<'_>, id: ID) -> Option<Book> { let store = ctx.data::<Arc<BookStore>>().expect("BookStore not found in context"); store.get_book(&id).cloned() // Geklont, da wir ein signiertes Book zurückgeben } }
Das Makro #[Object]
wandelt unsere Query
-Struktur in ein GraphQL-Objekt um. Jede async fn
innerhalb des impl
-Blocks wird zu einem Feld im GraphQL Query
-Typ. Der Parameter ctx: &Context<'_>
bietet Zugriff auf den gemeinsam genutzten Anwendungsstatus, wo wir unseren BookStore
platzieren werden.
Definieren von Mutations
Fügen wir nun Mutationen hinzu, um Clients das Hinzufügen neuer Bücher zu ermöglichen.
use async_graphql::{Context, InputObject, Object, ID}; use uuid::Uuid; use std::sync::Arc; use tokio::sync::Mutex; // Für den schreibbaren Zugriff auf BookStore in einer gleichzeitigen Umgebung #[derive(InputObject)] struct NewBook { title: String, author: String, genre: Genre, published_year: i32, } pub struct Mutation; #[Object] impl Mutation { /// Erstellt ein neues Buch. async fn add_book(&self, ctx: &Context<'_>, input: NewBook) -> Book { let store_arc = ctx.data::<Arc<Mutex<BookStore>>>().expect("BookStore Mutex not found in context"); let mut store = store_arc.lock().await; let new_book = Book { id: ID(Uuid::new_v4().to_string()), title: input.title, author: input.author, genre: input.genre, published_year: input.published_year, }; store.add_book(new_book.clone()); new_book } }
Hier definiert #[derive(InputObject)]
einen GraphQL-Eingabetyp, der für Argumente für Mutationen verwendet wird. Beachten Sie, dass wir unseren BookStore
in Arc<Mutex<T>>
einpacken. Arc
(Atomic Reference Counted) ermöglicht mehrere Besitzrechte an Daten über Threads hinweg, und Mutex
bietet sicheren schreibbaren Zugriff auf den BookStore
in einer gleichzeitigen async
-Umgebung.
Zusammenstellen des Schemas und Servers
Schließlich kombinieren wir unsere Queries und Mutationen zu einem ausführbaren GraphQL-Schema mit async_graphql::Schema
und stellen es über ein Web-Framework wie poem
bereit.
use async_graphql::{EmptySubscription, Schema}; use async_graphql_poem::{GraphQLResponse, GraphQLRequest}; use poem::{ get, handler, listener::TcpListener, web::{Html, Data}, EndpointExt, IntoResponse, Route, Server, }; use std::sync::Arc; use tokio::sync::Mutex; // Für den schreibbaren Zugriff auf BookStore mod models; // angenommen, models.rs enthält Book, Genre, BookStore mod queries; // angenommen, queries.rs enthält Query mod mutations; // angenommen, mutations.rs enthält Mutation use models::{BookStore}; use queries::Query; use mutations::Mutation; // GraphiQL Playground HTML const GRAPHIQL_HTML: &str = r#"<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>GraphiQL</title> <link href="https://unpkg.com/graphiql/graphiql.min.css" rel="stylesheet" /> </head> <body> <div id="graphiql" style="height: 100vh;"></div> <script crossorigin src="https://unpkg.com/react/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/graphiql/graphiql.min.js"></script> <script> window.onload = function() { ReactDOM.render( React.createElement(GraphiQL, { fetcher: GraphiQL.createFetcher({ url: "/graphql" }), defaultQuery: ` query { books { id title author genre publishedYear } } mutation AddBook { addBook(input: { title: "The Rust Programming Language", author: "Steve Klabnik & Carol Nichols", genre: ScienceFiction, publishedYear: 2018 }) { id title author genre } } `, }), document.getElementById('graphiql'), ); }; </script> </body> </html>" #gt; #[handler] async fn graphql_playground() -> impl IntoResponse { Html(GRAPHIQL_HTML) } #[handler] async fn graphql_handler( schema: Data<&Schema<Query, Mutation, EmptySubscription>>, req: GraphQLRequest, ) -> GraphQLResponse { schema.execute(req.0).await.into() } #[tokio::main] async fn main() -> Result<(), std::io::Error> { // Initialisieren des In-Memory-Buchspeichers let book_store = Arc::new(Mutex::new(BookStore::new())); // Erstellen des GraphQL-Schemas let schema = Schema::build(Query, Mutation, EmptySubscription) .data(book_store.clone()) // Hinzufügen des BookStore in den Schema-Kontext .finish(); println!("GraphQL Playground: http://localhost:8000"); // Bereitstellen des GraphQL-Endpunkts und des Spielplatzes mit Poem Server::new(TcpListener::bind("127.0.0.1:8000")) .run( Route::new() .at("/graphql", get(graphql_playground).post(graphql_handler)) .data(schema) ) .await }
In dieser main
-Funktion:
- Initialisieren wir unseren
BookStore
und packen ihn inArc<Mutex<T>>
, um sicherzustellen, dass er über verschiedene Request-Handler hinweg sicher, gemeinsam genutzt und schreibbar zugänglich ist. - Erstellen wir das
Schema
mitSchema::build
und übergeben ihm unsereQuery
-,Mutation
- undEmptySubscription
-Typen.async-graphql
analysiert diese Typen automatisch anhand der prozeduralen Makros, um das vollständige GraphQL-Schema zu konstruieren. - Verwenden wir
.data(book_store.clone())
, um unserenBookStore
in den GraphQL-Kontext einzufügen und ihn für unsere Resolver zugänglich zu machen. - Richten wir einen
poem
-Server ein, der auflocalhost:8000
lauscht. - Der Endpunkt
/graphql
verarbeitet sowohl GET-Anfragen (zur Anzeige des GraphiQL-Spielplatzes) als auch POST-Anfragen (zur Verarbeitung von GraphQL-Queries/Mutationen).async-graphql-poem
bietet eine praktische Integration.
Vorteile dieses Ansatzes
- Typsicherheit zur Kompilierzeit: Da unser GraphQL-Schema direkt aus Rust-Typen abgeleitet wird, führt jede Diskrepanz zwischen der API-Definition und der Rust-Implementierung zu einem Kompilierzeitfehler. Dies reduziert Laufzeitfehler erheblich und verbessert die Wartbarkeit.
- Hohe Leistung: Rusts Null-Kosten-Abstraktionen, effizientes Speichermanagement und die
async/await
-Laufzeit ermöglichen es unserem GraphQL-Server, ein hohes Volumen an gleichzeitigen Anfragen mit minimalem Overhead zu bewältigen.async-graphql
ist speziell auf Leistung ausgelegt. - Nebenläufigkeit:
async-graphql
integriert sich nahtlos in Rusts asynchrones Ökosystem und ermöglicht es Resolvern, E/A-gebundene Operationen (wie Datenbankabfragen oder externe API-Aufrufe) gleichzeitig auszuführen, ohne den Server zu blockieren. - Idiomatische Rust: Entwickler, die mit Rust vertraut sind, werden die API als natürlich und intuitiv empfinden.
- Umfangreiche Funktionen:
async-graphql
unterstützt eine breite Palette von GraphQL-Funktionen, darunter Subscriptions, Direktiven, Interfaces, Unions und mehr, was es für komplexe Anwendungen geeignet macht.
Anwendungszenarien
Diese Einrichtung eignet sich ideal für:
- Microservices: Bereitstellung eines einheitlichen API-Gateways für verschiedene Backend-Dienste.
- Echtzeitanwendungen: Nutzung von Subscriptions für Live-Updates in Chat-Anwendungen, Spielen oder Finanz-Dashboards.
- Mobile und Web-Backends: Effiziente Bereitstellung von Daten für Clients, die nur bestimmte Felder benötigen, wodurch die Netzwerknutzung optimiert wird.
- Interne Tools: Erstellung robuster und typsicherer APIs für interne Anwendungen, bei denen Datenkonsistenz oberste Priorität hat.
Fazit
Der Aufbau eines hochleistungsfähigen, typsicheren GraphQL-Servers in Rust mit async-graphql
bietet eine leistungsstarke und zuverlässige Lösung für die moderne API-Entwicklung. Durch die Nutzung von Rusts starkem Typsystem und seinen async/await
-Fähigkeiten ermöglicht async-graphql
Entwicklern, komplexe GraphQL-Schemata direkt aus Rust-Code zu definieren, was eine Typsicherheit zur Kompilierzeit gewährleistet und eine außergewöhnliche Laufzeitleistung liefert. Diese Kombination führt zu robusteren, wartbareren und skalierbareren Backend-Diensten und verkörpert wahrhaftig das Beste aus den Ökosystemen von GraphQL und Rust. Dies ist ein Beweis dafür, wie modernste Sprachfunktionen zu einer überlegenen Anwendungsarchitektur führen können.