Rustとasync-graphqlによる高性能・型安全なGraphQLサーバーの構築
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
現在のWeb開発の状況において、APIはモダンなアプリケーションのバックボーンです。RESTが長らく支配的なパラダイムでしたが、GraphQLはその柔軟性、効率性、開発者フレンドリーさから急速に普及しました。GraphQLはクライアントが必要なデータを正確にリクエストできるため、過剰取得や過少取得を削減します。これは、特に複雑なアプリケーションやモバイルクライアントにとって有益です。高性能で回復力のあるバックエンドの構築に関しては、Rustはその比類なきメモリ安全性、並行性、速度で際立っています。GraphQLのパワーとRustの保証を組み合わせることで、次世代APIを構築するための説得力のあるソリューションが生まれます。この記事では、Rust向けの強力なGraphQLライブラリである async-graphql
を活用して、高性能であるだけでなく、本来型安全なサーバーサイドGraphQL APIを構築する方法を掘り下げ、より堅牢で保守性の高いシステムへの道を開きます。
コアコンセプトの理解
実践的な実装に入る前に、Rustと async-graphql
を使用してGraphQLサービスを構築する上で中心となるいくつかの重要な用語を簡単に定義しましょう。
- GraphQL: APIのためのオープンソースのデータクエリおよび操作言語であり、既存のデータでこれらのクエリを満たすためのランタイムです。RESTとは異なり、異なるデータ集約に複数のエンドポイントが必要になる場合があるのに対し、GraphQLは単一のエンドポイントを使用し、クライアントが要求する正確なデータ構造を指定できます。
- スキーマ定義言語 (SDL): GraphQL APIの構造(タイプ、フィールド、リレーションシップ、操作(クエリ、ミューテーション、サブスクリプション)を含む)を定義するために使用される言語に依存しない構文です。
- クエリ: サーバーからデータを読み取る操作です。
- ミューテーション: サーバー上のデータを書き込みまたは変更する操作です。
- サブスクリプション: Typically implemented using WebSockets. サーバーからのリアルタイム更新を受信する操作です。
- リゾルバ: GraphQLスキーマ内の特定のフィールドに対応するデータを取得する責任を負う、サーバー上の関数またはメソッドです。
- 型安全性: プログラミング言語のプロパティであり、タイプエラーを防ぎ、操作が正しいタイプのデータに対してのみ実行されることを保証します。Rustの強力な静的型システムは、コンパイル時に優れた型安全性を提供します。
async-graphql
: Rust向けの、人気があり、高性能で、機能豊富なGraphQLサーバーライブラリです。非同期操作、Rustの強力な型システムによる型安全性、および慣用的なRust APIを強調しています。async/await
: Rustの非同期コードを記述するための組み込みメカニズムであり、従来のthreadingモデルのオーバーヘッドなしで並行操作を可能にします。これは、ネットワークサーバーのようなI/Oバウンドな高性能アプリケーションにとって重要です。
型安全なGraphQLサーバーの構築
async-graphql
は、プロシージャルマクロで装飾されたRustの構造体と列挙型を使用して、GraphQLスキーマを直接定義することを可能にします。このアプローチは、本質的に型安全性を強制します。書籍を管理するためのシンプルなAPIを構築する例を見てみましょう。
プロジェクトセットアップ
まず、新しいRustプロジェクトを作成し、Cargo.toml
に必要な依存関係を追加します。
[package] name = "book_api" version = "0.1.0" edition = "2021" [dependencies] async-graphql = { version = "7.0", features = ["apollo_tracing", "tracing"] } # デバッグのためにトレーシングを追加 async-graphql-poem = "7.0" # Poem Webフレームワークのコネクタ poem = { version = "1.0", features = ["static-files", "rustls", "compression"] } # シンプルで高速なWebフレームワーク tokio = { version = "1.0", features = ["full"] } # 非同期ランタイム serde = { version = "1.0", features = ["derive"] } # (デ)シリアライズのため。しばしば便利 uuid = { version = "1.0", features = ["v4", "serde"] } # 一意なIDのため
データモデルの定義
まず、データを表すRust構造体を定義します。これらは自然にGraphQLタイプにマッピングされます。
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")] // 必要に応じて別のモジュールで列挙型を定義可能にする enum Genre { Fiction, NonFiction, ScienceFiction, Fantasy, Mystery, } // 実際のアプリケーションでは、データベースを使用する可能性が高いです。 // 簡単にするために、インメモリストアを使用します。 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() // 簡単にするためにクローン。実際のアプリケーションでは参照やArcを検討してください } }
#[derive(SimpleObject)]
および #[derive(Enum)]
マクロに注意してください。これらは async-graphql
によって提供され、Rustタイプから必要なGraphQLスキーマ定義を自動的に生成し、バックエンドロジックとGraphQL API間の型の一貫性を保証します。async-graphql
の ID
タイプは、GraphQLの ID
スカラーにマッピングされ、Stringとしてシリアライズされます。
クエリの定義
次に、ルート Query
オブジェクトを定義しましょう。この構造体には、データを取得するためのリゾルバメソッドが含まれます。
use async_graphql::{Context, Object, ID}; use std::sync::Arc; // BookStoreをスレッド間で安全に共有するため pub struct Query; #[Object] impl Query { /// すべての書籍のリストを返します。 async fn books(&self, ctx: &Context<'_>) -> Vec<Book> { let store = ctx.data::<Arc<BookStore>>().expect("BookStore not found in context"); store.get_all_books() } /// IDで単一の書籍を返します。 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() // 所有するBookを返すためクローン } }
#[Object]
マクロは、Query
構造体をGraphQLオブジェクトに変換します。impl
ブロック内の各 async fn
は、GraphQL Query
タイプ内のフィールドになります。ctx: &Context<'_>
パラメータは、共有アプリケーション状態へのアクセスを提供します。ここに BookStore
を配置します。
ミューテーションの定義
次に、クライアントが新しい書籍を追加できるようにミューテーションを追加しましょう。
use async_graphql::{Context, InputObject, Object, ID}; use uuid::Uuid; use std::sync::Arc; use tokio::sync::Mutex; // 並行環境のBookStoreへのミュータブルアクセス用 #[derive(InputObject)] struct NewBook { title: String, author: String, genre: Genre, published_year: i32, } pub struct Mutation; #[Object] impl Mutation { /// 新しい書籍を作成します。 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 } }
ここでは、#[derive(InputObject)]
がGraphQL入力タイプを定義し、ミューテーションの引数に使用されます。BookStore
を Arc<Mutex<T>>
でラップしていることに注意してください。Arc
(Atomic Reference Counted)は、スレッド間でデータの複数の所有権を許可し、Mutex
は、並行 async
環境で BookStore
への安全なミュータブルアクセスを提供します。
スキーマとサーバーのアセンブル
最後に、async_graphql::Schema
を使用してクエリとミューテーションをラン可能なGraphQLスキーマに結合し、poem
のようなWebフレームワークを介して公開します。
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; // BookStoreへのミュータブルアクセス用 mod models; // models.rsにBook, Genre, BookStoreが含まれていると仮定 mod queries; // queries.rsにQueryが含まれていると仮定 mod mutations; // mutations.rsに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> "#; #[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> { // インメモリブックストアを初期化 let book_store = Arc::new(Mutex::new(BookStore::new())); // GraphQLスキーマを作成 let schema = Schema::build(Query, Mutation, EmptySubscription) .data(book_store.clone()) // BookStoreをスキーマコンテキストに追加 .finish(); println!("GraphQL Playground: http://localhost:8000"); // Poemを使用してGraphQLエンドポイントとプレイグラウンドをサーブ Server::new(TcpListener::bind("127.0.0.1:8000")) .run( Route::new() .at("/graphql", get(graphql_playground).post(graphql_handler)) .data(schema) ) .await }
この main
関数では:
BookStore
を初期化し、Arc<Mutex<T>>
でラップして、異なるリクエストハンドラ間で安全に共有およびミュータブルなアクセスを可能にします。Schema::build
を使用してSchema
を構築し、Query
、Mutation
、EmptySubscription
タイプを渡します。async-graphql
は、プロシージャルマクロを使用してこれらのタイプを自動的に検査し、完全なGraphQLスキーマを構築します。.data(book_store.clone())
を使用してBookStore
をGraphQLコンテキストに注入し、リゾルバからアクセスできるようにします。localhost:8000
でリッスンするpoem
サーバーを設定します。/graphql
エンドポイントは、GETリクエスト(GraphiQLプレイグラウンドを表示)とPOSTリクエスト(GraphQLクエリ/ミューテーションを処理)の両方を処理します。async-graphql-poem
は便利な統合を提供します。
このアプローチの利点
- コンパイル時の型安全性: GraphQLスキーマはRustタイプから直接導出されるため、API定義とRust実装の間の不一致はコンパイル時エラーになります。これにより、ランタイムバグが大幅に削減され、保守性が向上します。
- 高性能: Rustのゼロコスト抽象化、効率的なメモリ管理、
async/await
ランタイムにより、GraphQLサーバーは最小限のオーバーヘッドで大量の同時リクエストを処理できます。async-graphql
は特にパフォーマンスのために設計されています。 - 並行性:
async-graphql
はRustの非同期エコシステムとシームレスに統合され、リゾルバがブロッキングせずにI/Oバウンド操作(データベース呼び出しや外部APIリクエストなど)を並行して実行できるようにします。 - 慣用的なRust: Rustに慣れている開発者は、APIを自然で直感的だと感じるでしょう。
- 豊富な機能:
async-graphql
は、サブスクリプション、ディレクティブ、インターフェース、ユニオンなど、幅広いGraphQL機能をサポートしており、複雑なアプリケーションに適しています。
アプリケーションシナリオ
このセットアップは、以下に最適です。
- マイクロサービス: 異種バックエンドサービスのための統合APIゲートウェイの提供。
- リアルタイムアプリケーション: チャットアプリケーション、ゲーム、または財務ダッシュボードでのライブ更新のためのサブスクリプションの活用。
- モバイルおよびWebバックエンド: 特定のフィールドのみを必要とするクライアントに効率的にデータをサーブし、ネットワーク使用率を最適化します。
- 内部ツール: データの一貫性が最優先される内部アプリケーションのための、堅牢で型安全なAPIの構築。
結論
Rustと async-graphql
を使用して高性能で型安全なGraphQLサーバーを構築することは、モダンなAPI開発のための強力で信頼性の高いソリューションを提供します。Rustの強力な型システムと async/await
の機能を活用することで、async-graphql
は開発者がRustコードから直接複雑なGraphQLスキーマを定義できるようにし、コンパイル時の型安全性を保証し、優れたランタイムパフォーマンスを提供します。この組み合わせにより、より堅牢で、保守性が高く、スケーラブルなバックエンドサービスが実現し、GraphQLとRustエコシステムの両方の利点を真に体現しています。これは、最先端の言語機能がどのように優れたアプリケーションアーキテクチャにつながるかの証です。