GraphQLをさわったとことがなかったので、Rustで実装してみることにしました。
RustでGraphQLサーバを立てる場合、調べた感じ代表的なクレートとしては、juniperかasync-graphqlのどちらかっぽい。
axumを使ったexampleがどちらもあったので、試したところ、async-graphqlのほうがシンプルで使いやすかったので、こちらで実装してみる
github.com
github.com
概要
アプリっぽい感じで使ってみたかったので、適当に銀行口座を作るようなAPIを元にGraphQLサーバを立ててみた
クリーンアーキテクチャにして、REST APIとの両立やDynamoDBを使いたかったので、結局、いろいろやってるAPIになりました。
github.com
環境
GraphQL
実装としてはRouterとSchemaとでファイルを分けました。今回は、検索と登録だけなので、QueryとMutationだけになってます。(ちなみにGraphQLもasync-graphqlもほぼ知識がない状態なので、あしからず)
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(modules.clone())
.finish();
let app = Router::new()
.route("/", get(graphql_playground).post(graphql_handler))
.layer(Extension(schema))
.fallback(not_found_handler);
async-graphqlのbookやasync-graphqlを実装したzennの記事を参考にして、実装できました。
感想としては、GraphQLの知識がほぼゼロでもなんとかなったなという感じです
続いて肝心のSchemaは、resolverファイルを作ってこっちに定義しました。
unwrap()使ってたりとエラー処理に関してはかなり雑ですが、目的はGraphQLを使ってみる、なので、そこはご愛嬌。。
クリーンアーキテクチャ+DIしているので、依存関係はModules構造体に定義していて、それを先程のdata()関数に渡して、各関数で、Contextから取得できるようにしてます。
#[Object]
impl QueryRoot {
async fn ping(&self, ctx: &Context<'_>) -> Ping {
ctx.data::<Arc<Modules>>().unwrap().bank_query_use_case().ping().await
}
async fn find(&self, ctx: &Context<'_>, id: String) -> BankQueryAccount {
let res = ctx.data::<Arc<Modules>>()
.unwrap()
.bank_query_use_case()
.view_account(id)
.await;
match res {
Ok(account) => account.unwrap(),
Err(e) => panic!("error: {}", e),
}
}
}
#[Object]
impl MutationRoot {
async fn create(&self, ctx: &Context<'_>, create_account: CreateAccount) -> BankQueryAccount {
let res = ctx.data::<Arc<Modules>>()
.unwrap()
.bank_query_use_case()
.add_account(create_account.into())
.await;
match res {
Ok(new_account) => new_account.unwrap(),
Err(e) => panic!("error: {}", e),
}
}
}
リクエストされたSchemaやレスポンスの定義ですが、これは、別ファイルの構造体でそれぞれ定義してます。
SimpleObjectのアトリビュートがあるのが、Query
InputObjectのアトリビュートがあるのが、Mutationです。
#[derive(SimpleObject)]
pub struct Ping {
pub status: String,
pub code: i32,
}
#[derive(SimpleObject)]
pub struct BankQueryAccount {
pub bank_id: String,
pub branch_office_id: String,
pub name: String,
pub money: i32,
}
#[derive(InputObject)]
pub struct CreateAccount {
pub bank_id: String,
pub branch_office_id: String,
pub name: String,
pub money: i32,
}
実際にplaygroundでリクエストするときのがクエリこれです
{
find(id: "1") {
bankId,
branchOfficeId,
name,
money
}
}
mutation {
create(createAccount: { bankId: "test", branchOfficeId: "test", name: "graphql", money: 100 }) {
bankId
branchOfficeId
name
money
}
}
感想
クリーンアーキテクチャとREST APIも実装したあとにGraphQLも実装したので、ごちゃごちゃした感じになってしまいましたが、こうやって使うんだな、というのはよくわかりました。
やることたくさんあって、わからん!というわけではなく、GraphQLがある程度、理解できたら、実装にはそんな苦労しないのかな?という印象。
実装観点でREST APIとの違いでいえば、Schemaを定義し、RouterにSchemaをビルドしたものを渡す部分が異なるだけでそれ以外はやることは同じなので、両立もできそうだし、REST APIから移行もできそう、とは思いました。
まとめ
async-graphqlを使えば、GraphQLの実装はそんなに難しくないよ
後は仕事で導入するときは、以下の本を読んでメリデメを理解してからのほうがよいなと思いました。
https://www.amazon.co.jp/%E5%88%9D%E3%82%81%E3%81%A6%E3%81%AEGraphQL-%E2%80%95Web%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B6%E6%96%B0%E4%B8%96%E4%BB%A3API-Eve-Porcello/dp/487311893X