これは はてなエンジニア - Qiita Advent Calendar 2024 - Qiita 15日目の記事です。昨日は id:utgwkk さんの「ISUCONの感想戦を支えるEC2の自動開始・停止、そしてAWS Step Functions」でした。
はてなでフロントエンドエキスパートをしている id:mizdra です。この記事では、JavaScript で GraphQL サーバーの技術選定をする際に、どのようなツールやライブラリがあるのかを紹介します。
というのも、JavaScript で GraphQL サーバーを作ろうと思って検索してみると、「Next.js + Apollo Server + graphql-codegen で GraphQL サーバーを作ろう!」だとか、そういう記事が多数出てきます。ただ、複数のライブラリやツールを組み合わせると作れることは分かるのですが、どのライブラリ・ツールが何を担っていて、どういう代替ライブラリ・ツールに差し替え可能なのかが、正直調べても分かりづらいです。「Apollo Server」ってやつは「Pothos」ってやつに差し替えられるの? とか、そういった疑問が出てきがちです *1。
そうした疑問に答えられるように、GraphQL サーバーを構成するライブラリやツールの整理をする、というのがこの記事の目的です。
はじめに要点だけ
- GraphQL サーバーを作る際には、以下のようなレイヤーごとに技術選定すると良い
- GraphQL Server
- 「GraphQL クエリ」を受け取って、「GraphQL レスポンス」を返すことが仕事
- スキーマ・Resolver は外で定義されたものを受け取って、それを使って GraphQL クエリを処理する
- スキーマ・Resolver はユーザが好きなようなライブラリをツールを使って書ける
- HTTP Server
- 「GraphQL Server」には直接 HTTP リクエストを受け取って、HTTP レスポンスを返す機能がないので、そこを担う
- スキーマ定義
- Resolver
- GraphQL Server
- このレイヤーごとであれば、ライブラリやツールを差し替えできる
- コードファーストでは、スキーマと Resolver が同時に定義される
- Resolver の定義方法をどうするかを別個に検討しなくて良い
- GraphQL Code Generator はなくてもやっていける
GraphQL Server
「GraphQL クエリ」を受け取って、「GraphQL レスポンス」を返すもののこと。有名どころだと以下のようなライブラリが存在する。
- @apollo/server
- graphql-yoga
- The Guild が作っている GraphQL Server ライブラリ
- GraphQL Code Generator 作ってるのと同じ団体
- initial commit は 2017/11
- @apollo/server よりパフォーマンスが良くて、code splitting に対応しているらしい
- The Guild が作っている GraphQL Server ライブラリ
- @hono/graphql-server
- Hono が出しているやつ
- @apollo/server や graphql-yoga に乗っからず、Hono のために専用のものを用意しているのは何故なんでしょうね
- graphql-yoga も Hono で動くので、わざわざ専用のライブラリ要らないと思うのだけど...
- パフォーマンスは良さそう
- 何か事情を知ってる方が居たら教えてください
例えば @apollo/server は以下のようにして使います。ApolloServer というクラスに GraphQL スキーマと resolver を渡して初期化するだけです。
// https://www.apollographql.com/docs/apollo-server/getting-started より引用・一部改変 import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; const typeDefs = ` type Book { title: String author: String } type Query { books: [Book] } `; const books = [ { title: 'ゆゆ式 1', author: '三上小又' }, { title: 'ゆゆ式 2', author: '三上小又' }, ]; const resolvers = { Query: { books: () => books, }, }; const server = new ApolloServer({ typeDefs, resolvers, });
この辺は graphql-yoga も全く同じインターフェイスになってます。
HTTP Server
@apollo/server や graphql-yoga は「GraphQL クエリを受け取って GraphQL レスポンスを返す」ことはできますが、直接 HTTP リクエストを受け取って、HTTP レスポンスを返したりといったことはできません。そのため、実際に機能する GraphQL サーバーを実装するには、HTTP Server / Router と組み合わせないといけません。
有名どころだと、以下のようなものが使われることが多いです。
- node:http の createServer
- router がなくて素朴すぎて、これを直接使うの稀なはず
- @apollo/server/standalone の startStandaloneServer
- こちらも router がなくて素朴すぎて、使うの稀なはず
- express, fastify
- Hono
- https://www.npmjs.com/package/@hono/graphql-server とセットで使う想定っぽい?
- graphql-yoga とかも使えるかはよくわかってない
- Next.js の Route Handler (App Router only)
- Next.js の API Routes (Pages Router only)
- Edge Runtime (AWS Lambda など)
- HTTP Server というよりはランタイムレベルの話だけど、一応ここに乗せておく
- https://github.com/apollo-server-integrations/apollo-server-integration-aws-lambda
スキーマ定義
さっき見たように、@apollo/server や graphql-yoga はスキーマを外から受け取るだけで、そのスキーマがどう定義されているかは関知しません。ここはユーザが自由に定義できます。
スキーマの定義方法は、以下のようなものがあります。
- 文字列
- 「GraphQL Server」で紹介したやり方
- サンプルコードに使うくらいで、プロダクションではまず採用されない
- スキーマファイル
- schema.graphql などで定義するやつ
- fs.readFile で読み込んで、ApolloServer に渡したら良い
- コードファーストライブラリ
- TypeScript などプログラミング言語のコードでスキーマを定義するアプローチ
- Nexus
- もうメンテナンスされてない
- https://github.com/graphql-nexus/nexus/issues/1139
- pothos
- 最近よく使われているのを見る
- Plugin が豊富: https://pothos-graphql.dev/docs/plugins
コードファーストライブラリの場合はちょっと特殊で、大抵スキーマと resolver を一緒に定義する作りになってます。
// https://pothos-graphql.dev/docs より引用 import { createYoga } from 'graphql-yoga'; import { createServer } from 'node:http'; import SchemaBuilder from '@pothos/core'; const builder = new SchemaBuilder({}); builder.queryType({ fields: (t) => ({ hello: t.string({ args: { name: t.arg.string(), }, resolve: (parent, { name }) => `hello, ${name || 'World'}`, }), }), }); const yoga = createYoga({ schema: builder.toSchema(), });
builder.queryType
の中でスキーマと Resolver を一緒に定義しているのが分かると思います。そしてそのスキーマと resolver の情報を builder.toSchema()
で抽出して、yoga に渡しています。yoga の schema オプションは、typeDefs
オプション・resolvers
オプションに相当する情報をまとめて渡せるオプションで、これで一度にスキーマと Resolver の情報を yoga に渡しています。
Resolver
スキーマと同様、Resolver についても @apollo/server や graphql-yoga はそれが定義されているか関知しません。よってスキーマファーストで開発する場合、resolver をどうやって定義するかも決めなければなりません。こちらについてもアプローチが色々あります。
- 手で頑張って書く
const resolvers = { ... }
の中を気合で書く- 型チェックも何もない (本来 String しか返せない field に TypeScript の number を返せてしまう)
- 基本的にこれを採用することない
- GraphQL Code Generator で resolver の型定義を生成して、型が付いた状態で書く
- @graphql-codegen/typescript-resolvers という plugin があり、これを使うとスキーマファイルから TypeScript 向けの resolver の型定義を生成できる
Resolvers
という型が生成されるので、const resolvers: Resolvers = { ... }
のようにして使う- 後は
{ ... }
の中を好きに書く
- 後は
- コードファースト開発では大体これが採用される (という認識。他にナウい手段があるなら教えてください!)
コードファーストアプローチでは、スキーマをコードで定義する時に一緒に resolver も定義することになるので、この部分の技術選定はスキップされます。そのため、GraphQL Code Generator は基本的に導入しなくても問題ありません。ただ、Resolvers 以外の型定義 (例えば で生成できる GraphQL Object の型など) を生成したくなったら GraphQL Code Generator を導入することになりますが、そうしたものが欲しくなるまでは、導入しなくて良いでしょう。
おわりに
この記事では JavaScript における GraphQL サーバーを構成するライブラリやツールの整理をしました。GraphQL サーバーの技術選定をする際の参考になれば嬉しいです。
はてなエンジニア - Qiita Advent Calendar 2024 - Qiita の明日の担当は id:bps_tomoya さんです。
*1:ちなみに答えは No です
*2:もともと Meteor Development Group という団体が作ったライブラリだった: https://zenn.dev/username/articles/2023-07-17-944941f57183b6#%E6%AD%B4%E5%8F%B2