Node.jsとApollo ServerによるGraphQL APIの構築:SchemaからMutationまで

Development tutorial - IT technology blog
Development tutorial - IT technology blog

なぜREST APIはもはや「唯一の選択肢」ではないのか?

Web開発を始めたばかりの頃を思い出すと、当時はREST APIこそが「黄金のスタンダード」でした。新しいデータが必要になるたびに、せっせとエンドポイントを作成していました:/users/posts/comments。プロジェクトが大きくなるまでは、すべてが順調でした。しかし、フロントエンドのエンジニアからこんな不満が出始めました。「おい、ユーザーの名前だけが必要なのに、なぜAPIはメールアドレスや住所、生年月日までの重いデータを全部返してくるんだ?」あるいは「1つの記事を表示するのに、データを集めるだけで3〜4つの異なるAPIを叩かなければならない」といった具合です。

その時初めて、オーバーフェッチ(データの取りすぎ)とアンダーフェッチ(データの不足)の問題を痛感しました。GraphQLは、これらのトラブルを解消するために誕生しました. バックエンドがデータ構造を強制する代わりに、GraphQLはクライアントに権限を与えます。必要なものをリクエストすれば、サーバーはまさにそれだけを返します。

実用的な違い:定食 vs ビュッフェ

REST API:固定メニューを食べる

RESTは、大衆食堂に入るようなものです。「チキンライスセット」を注文すると、店主はご飯、鶏肉、スープ、漬物のフルセットを持ってきます。たとえ漬物が嫌いでも、それは皿の上に鎮座しています(オーバーフェッチ)。さらにお肉をもう一枚食べたい? 追加料金を払って、別の皿を注文して待つ必要があります(アンダーフェッチ)。

GraphQL:データのビュッフェ形式

GraphQLは全く異なります。それはビュッフェのようなものです。お皿(Query)を持って、自分が食べたいものだけを取ります。nameavatarが欲しい? すぐに手に入ります。そのユーザーが投稿した記事のtitleも一度に取得したい? それもお皿に乗せるだけで完了です。

プロジェクトに導入するメリットとデメリット

私が以前参加したダッシュボードのプロジェクトでは、GraphQLに移行したことで、ページ読み込みごとのリクエスト数を8回からわずか1回に削減できました。しかし、過度に神格化してはいけません。

なぜ使うべきなのか?

  • 帯域幅の節約: ペイロードが大幅に削減されるため、通信速度が不安定な4G/5Gユーザーにとって非常に有益です。
  • 厳格なコントラクト: Schemaはフロントエンドとバックエンドの間の合意事項として機能します。データ型が異なれば、システムはすぐにエラーを吐きます。
  • 一箇所に集約: 複雑なURLを何十個も覚える必要はありません。すべては/graphqlという単一のエンドポイントを中心に回ります。

注意すべきハードル:

  • キャッシュの悩み: すべてのリクエストにPOSTメソッドを使用するため、ブラウザの標準的なHTTPキャッシュを活用するのがRESTよりもはるかに困難です。
  • 新しいマインドセット: 従来のフラットなテーブル構造ではなく、グラフ(Graph)形式で考えることにチームが慣れるまで時間が必要です。

戦略的ツール:Apollo Server

GraphQLを構築するためのライブラリはいくつかありますが、私の第一の選択肢は常にApollo Serverです。安定しているだけでなく、最高のApollo Sandboxが付属しています。このツールを使えば、無機質なJSONの羅列を眺める代わりに、データ構造を視覚的に確認しながらAPIをテストできます。

さあ、コードを書きましょう!

始める前に、Node.jsがインストールされていることを確認してください。今回は「本と著者」を管理するという実践的な例を作成します。

ステップ1:基盤のセットアップ

mkdir node-graphql-demo
cd node-graphql-demo
npm init -y
npm install apollo-server graphql

ステップ2:データ構造の定義(Schema)

index.jsファイルを作成します。ここでSDL(Schema Definition Language)を使用してデータ構造を定義します:

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type Book {
    id: ID!
    title: String
    author: Author
  }

  type Author {
    id: ID!
    name: String
    books: [Book]
  }

  type Query {
    books: [Book]
    book(id: ID!): Book
  }

  type Mutation {
    addBook(title: String!, authorId: ID!): Book
  }
`;

ステップ3:ロジックの実装(Resolvers)

Schemaが骨組みなら、Resolverは脳です。データがどこから取得されるか(データベース、JSONファイル、またはサードパーティAPI)を決定します。

const authors = [
  { id: '1', name: 'ナム・カオ' },
  { id: '2', name: 'ト・ホアイ' },
];

const books = [
  { id: '1', title: '老ハック', authorId: '1' },
  { id: '2', title: 'コオロギの冒険記', authorId: '2' },
];

const resolvers = {
  Query: {
    books: () => books,
    book: (_, args) => books.find(b => b.id === args.id),
  },
  Book: {
    author: (parent) => authors.find(a => a.id === parent.authorId),
  },
  Mutation: {
    addBook: (_, { title, authorId }) => {
      const newBook = { id: String(books.length + 1), title, authorId };
      books.push(newBook);
      return newBook;
    }
  }
};

ステップ4:サーバーの起動

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀 サーバーの準備が整いました: ${url}`);
});

結果の確認

node index.jsコマンドを実行し、表示されたURLをブラウザで開きます。以下のQueryを試してみてください:

query {
  books {
    title
    author {
      name
    }
  }
}

期待通り、本とその著者の名前が瞬時に返ってくるのがわかります。素晴らしいのは、authorフィールドを除外すれば、サーバーは自動的に著者検索のロジックをスキップし、処理リソースを大幅に節約できる点です。

実践プロジェクトでの教訓

以前の最大の失敗は、すべてのSchemaを単一のschema.graphqlファイルに詰め込んでしまったことです。エンティティが50を超えたとき、そのファイルはカオス状態になりました。最初からモジュールごと(User, Product, Order…)に分割するようにしましょう。

もう一つの注意点として、GraphQLは常にHTTPステータスコード200を返します。RESTのようにHTTPコードだけで判断せず、返されたデータ内のerrors配列をチェックして、より正確にエラーを捕捉する習慣をつけましょう。

この記事が、皆さんのGraphQLに対する「心理的ハードル」を下げる助けになれば幸いです。Schemaの考え方さえマスターすれば、API構築が驚くほど楽しく、柔軟になることに気づくはずです。

Share: