GraphQL でモダン API を実装する #reinvent

2020.07.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

reInvent:2019 のセッション、Building modern APIs with GraphQLのまとめです。

Demo

セッションはサンプル APIから全く同じデータをそれぞれ REST API から取得する場合と GraphQL API で取得する場合のランタイムでの実行の違いについての解説から始まります。

デモに利用されたサンプルデータ、films.jsonpeople.jsonはすべてこちらのレポジトリから確認できます。

REST API

サンプルデータから、Luke Skywalkerが登場するスターウォーズ映画に登場する全てのキャラクターのリストをそれぞれ REST API と GraphQL API から取得します。まずは RESTAPI の実装をみてみます。

main.js

async function runREST() {
  const LukeURI = "http://34.219.118.93/api/people/1";
  const character = await fetchObject(LukeURI);
  const films = await fetchAll(character.films);
  const allCharacterURIs = flatten(films.map(film => film.characters));
  const uniqueCharacterURIs = unique(allCharacterURIs);
  const uniqueCharacters = await fetchAll(uniqueCharacterURIs);
  const allCharNames = uniqueCharacters
    .map(c => c.name)
    .sort()
    .join(", ");
  document.getElementById("names").innerText = allCharNames;
}

REST API で取得する場合、以下の流れでデータの取得が行われます。

1. `Luke Skywalker`のデータを取得する API を呼び出す
2. ⑴ で返却されたデータから、`Luke Skywalker`の出演する映画のデータを取得する。
3. 映画データからそれぞれのキャラクターの URL を抽出する。重複チェックをして重複するデータはリストから削除する
4. 取得したキャラクターの URL リストから全てのキャラクターのデータを取得する

データの形式と処理を読み解くと、⑵ で映画データを取得するには映画の数だけ、⑷ でキャラクターのデータを取得するにはキャラクターの数だけ API 呼び出しが発生することがわかります。

実際にブラウザの Network タブから API 呼び出しの際に何が起こるかを観察してみましょう。 100 もの API コールがパラレルに行われているのがわかります。必要なデータを取得するのに要した時間は440msでした。

GraphQL API

一方、GraphQL API の場合はどうでしょうか。

REST API と同じデータを GraphQL で取得する場合の処理が以下です。

main.js

async function runGraphQL() {
  // This value will be rewritten by demo.ts
  const GraphQLEndpoint = REPLACEME;
  const { data } = await fetchGraphQL(
    GraphQLEndpoint,
    `{
      person(name: "Luke Skywalker") {
        name
        films {
          characters {
            name
          }
        }
      }
    }`
  );

  const films = data.person.films;
  const allCharacters = flatten(films.map(film => film.characters));
  const allCharacterNames = allCharacters.map(c => c.name);
  const names = unique(allCharacterNames)
    .sort()
    .join(", ");
  document.getElementById("names").innerText = names;
}

GraphQL のスキーマをみて見ると、それぞれのデータに型が指定されていることがわかります。GraphQL ではこれらの型をクエリで指定して取得することができます。

取得したデータから必要なデータを取り出したり、ソートしたりなどの処理を挟むことは REST API の時と変わりませんが、ブラウザの Network タブを確認するとリクエストは一度しかコールされていないことがわかります。必要なデータを取得するのに要した時間は73msでした。

GraphQL の特徴

  • リクエスト:GraphQL クエリを実行し、JSON 形式のレスポンスを受け取ります。GraphQL のクエリリクエスト形式は JSON のフォーマットに近しいですが、これは返却されるレスポンスに含まれるデータを予測しやすくするという意図があります。

  • スキーマ:スキーマはドメイン別に定義します。GraphQL は定義されたスキーマと GraphQL のクエリシンタックスの両方が有効である時のみ動作します。

  • サーバー:クエリの処理を行うサーバーです。送信されたリクエストを読み取り、JSON 形式のレスポンスを返却します。

GraphQL is NOT

GraphQL は・・・

  • A graph database query language ではない
  • State を管理するものではない
  • 音声や動画などのバイナリデータを扱うのには向かない
  • Facebook Graph API ではない
  • データベースの種類を限定しないし、データベースは使わなくてもいい
  • Javascript/Node.js 環境に限定されない
  • React/Relay/web 開発にのみ使われるものではない
  • HTTP に限定されない

クエリの形式

  • Operation名:query, mutation, subscription から指定します
  • [Optional]メソッド名:オプショナル項目ですが、クエリが何をしているのかわかりやすくしたりデバッグの際に Log がみやすくなるので付けることが推奨されています
  • Input 変数:$id:ID!の様にクエリに変数を渡すことでプロジェクト全体で汎用的に利用できます
  • 要素の指定:取得したい要素を定義します。レスポンスの JSON はこれとほぼ同じ形式で返却されます
  • ルート要素: authorなど、取得したいデータのルート要素を指定します
  • サブセレクション:booksなどの入れ子要素を指定する場合はtitle, isbnなどの中身の要素を指定する必要があります

REST vs. GraphQL

REST GraphQL 備考
Shared Definition No Yes
Conceptual Model Resource Graphs
Organization Federated Centralized
Related Ops Yes No
Introspection No Yes GraphQL の__schema__typeクエリを利用してスキーマ情報を取得できます
Data Typing Weak Strong GraphQL の場合型情報はスキーマで定義されます
リアルタイム性 No Yes GraphQL の Subscription を利用することでリアルタイムでデータの送受信ができる

REST API と GraphQL API をざっと比較したのが上記のテーブルです。 ここで注意したいのは、これらの要素はトレードオフであり、どちらかがより優れているかどうかというものではありません。

例えば、リアルタイム性に関しては Subscription 特性を持つ GraphQL が圧倒的に優れていますが、スケーラビリティに関してはロードバランサーの背後に設置してスケールする REST API の方が容易であると言えます。

モダン API 開発の課題

モダン API の開発にはたくさんの課題がありますが、ここではこの中から 3 つ、課題を解決する上で GraphQL がもっとも優れているものをご紹介します。

効率性の高さ

  • Overfetching: レスポンスが不要なデータも多く含んでしまうケース
  • Underfetching: 一度のリクエストで全ての必要なデータを取得することができないため、追加でリクエストを送信する必要があるケース

ネットワークへのリクエストは少なければ少ない方が良いです。 もうすぐ G5 も来るのにどうしてネットワークについて考える必要があるのでしょう? それはユーザーがいつも最新のデバイスを使い、良好なネットワーク環境にいるとは限らないからです。

型安全

GraphQL スキーマ定義によって提供される Typesafety はクライアントサイドでの複雑性を軽減します。

ドメインモデリング

GraphQL is Domain Driven Design:ビジネスロジックをそのままコードに落としやすいという利点があります。

認証&認可

  • 認証(Authentification):
  • 認可(Authorization):

認証、認可は GraphQL の役割ではありません。

例えば、同じプロジェクト内で GraphQL API と REST API、RPC API が使われているとしましょう。 GraphQL 内で認証や認可のロジックを実装した場合、その他の API レイヤーでは同じロジックを共有することができません。 認証や認可に限らず同様の理由からビジネスロジックは GraphQL レイヤに実装すべきではありません。

プロダクションに採用する際の Tips

  • 一度に全てのスキーマを書かないこと:小さくスキーマを書いてきちんと動作することを確認しましょう
  • 既存の REST API をリプレイスするのではなく共存するところから始めましょう。REST API を削除するのはいつでもできます
  • ビジネスロジックを GraphQL API レイヤーに書かないこと:同じロジックを API の外側で共有できないからです
  • チームの合意を取りましょう:勝手に GraphQL を追加しようとしてはいけません(笑)
  • 盲目的に GraphQL を採用しないこと: プロジェクトに合ったものをチョイスしましょう
  • "Hello world"などのリードオンリーでシンプルなクエリを書くところから始めましょう: 認証、認可、キャッシングなどクエリ自体はシンプルでも気をつけるべきことが山ほどあります。そういったクエリ内容以外のところを先に対応してから本格的なクエリを書き始めましょう

エコシステム

Resources

References