iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

GraphQLでバックエンドを開発する

■ はじめに

こんにちは。

株式会社iimonでエンジニアをしている遠藤です。

「GraphQL」という用語をよく耳にするのですが、実際に触ったことがなく、どのようなものなのかの理解が曖昧でした。

そのためこの機会に、そもそもGraphQLとはどういうものなのかをRESTと比較しながら、どういった場面で使うのが良いのかや基本的な仕様について、実際にアプリケーションを構築しながら整理していきたいと思います。

■ GraphQLとは

GraphQLとは、API向けのクエリ言語であり、既存のデータを使ってクエリを実行するためのランタイムです。

GraphQLは、必要な特定のデータをサーバーにリクエストすることができ、データの過剰フェッチや過小フェッチをなくすことができます。クライアントが必要なデータの構造を指定すると、サーバーはそのデータで正確に応答するため、ネットワークトラフィックが削減され、パフォーマンスが向上します。

また、1 回のリクエストで多数のソースにアクセスできるため、ネットワークの呼び出し回数や帯域幅の要件が緩和され、アプリケーションによって消費されるバッテリー寿命や CPU サイクルを節約できます。

GraphQLはまた、クライアントとサーバー間で機能する、強く型付けされたスキーマを提供しています。スキーマには型とフィールドを定義し、クライアントがAPIの機能を発見し理解することを可能にします。

■ RESTとの比較

具体的にRESTと比較しながら、GraphQLについて整理します。

ここでは、RESTについての詳しい説明は省くので、以下参考になりそうなリンクを貼らせていただきます。

https://aws.amazon.com/jp/what-is/restful-api/

類似点

  • アーキテクチャ
    • REST と GraphQL はどちらも、以下の API アーキテクチャ原則に沿っています。
      • ステートレスであり、サーバーはリクエスト間のレスポンス履歴を保存しない
      • 1 つのクライアントからのリクエストに対して、 1 つのサーバーからレスポンスを返す
      • HTTP が基盤となる通信プロトコルである
  • リソースベースのデザイン
    • リソース(クライアントが API を介してアクセスして操作できるあらゆるデータまたはオブジェクト)を中心にデータ交換を設計している。各リソースには、一意のIDと、クライアントが実行できる一連の操作 (HTTP メソッド) がある。
  • データ交換
    • REST と GraphQL はどちらも同様のデータ形式(一般的に使われるのはJSON)をサポートしている。
    • キャッシュをサポートしている。そのため、クライアントとサーバーは頻繁にアクセスされるデータをキャッシュして通信速度を上げることができる。
  • 言語とデータベースの中立性
    • クライアント側とサーバー側の両方で、あらゆるデータベース構造とプログラミング言語で動作するため、どのアプリケーションとも高度に相互運用できる。

相違点

REST API はアプリケーション通信のアーキテクチャコンセプトであるのに対し、GraphQL は仕様であり、API クエリ言語であり、ツールのセットでもあります。

また、リクエストやレスポンスの形式を比較すると以下の図のようになります。図はECサイトでユーザー名と購入履歴を取得するような例です。

REST APIは、アクションを決定するHTTP動詞と処理対象となるリソースを識別するエンドポイントを指定してリクエストを送るため、複数のリソースにアクセスするには複数回のREST APIリクエストが必要となります。また、REST API は常にデータセット全体を返すため、ユーザー名のみ取得したい場合にも、すべてのデータを返却します。

GraphQLの場合、クエリは 1 回の API リクエストとレスポンス交換で、必要なデータを取得することができます。また、GraphQL は内部的にはすべてのクライアントリクエストを POST HTTP リクエストとして送信します。

その他、RESTとGraphQLには以下のような違いがあります。

  • サーバー側スキーマ
    • REST APIではサーバー側スキーマは不要です(オプションで定義することはできる)。
    • GraphQL スキーマ定義言語で書かれたスキーマには、次のような詳細が含まれています。
      • 各オブジェクトに属するオブジェクトタイプとフィールド
      • 各フィールドの操作を定義するサーバー側リゾルバー関数
  • バージョニング
    • REST API では、データ構造や操作の変更をクライアントが把握するために、/api/v1/hogeのように、URL にバージョニングが含まれていることがよくあります。
    • GraphQLはクライアントが欲しい情報をリクエストして、そのままレスポンスを返すため、基本的にバージョンレスです。
  • エラー処理
    • REST API は型指定が弱いため、エラー処理を周囲のコードに組み込む必要があります。
    • GraphQLスキーマの詳細レベルにより、システムは自動的にリクエストエラーを識別し、有用なエラーメッセージを表示できます。

■ ハンズオン

フロントはReact バックエンドはNodejsを用いて実際にプロジェクトを作成してみます。

  1. プロジェクト用ディレクトリを作成し移動する

     $ mkdir sample-graphql-api && cd $_
    
  2. バックエンド用のフォルダを作成し、backendフォルダ配下にpackage.jsonとindex.jsを作成

     $ mkdir backend && cd $_
     $ npm init -y
     $ touch index.js
    
  3. バックエンドで使用するライブラリ(graphql とapollo-server)を開発環境にインストール

     $ npm i graphql apollo-server -D
    
  4. 開発サーバーを開くために、package.jsonの”scripts”の中身にdevコマンド追加

     "scripts": {
         "test": "echo \"Error: no test specified\" && exit 1",
             "dev": "node index.js"
       },
    
  5. index.jsの中身を以下のように修正

     const {ApolloServer, gql} = require("apollo-server");
    
     // 擬似的なデータを定義する
     const drinks = [
             {name: "コーラ", price: 200},
         {name: "ラムネ", price: 210},
         {name: "コーヒー", price: 100},
         {name: "紅茶", price: 110},
     ]
    
     // GraphQLのスキーマを定義する(どのようにAPIを呼び出すか)
     const typeDefs = gql`
             # ここで定義した型は、Queryの中で使用することができる
         type Drink { 
             name: String,
             price: Int,
         }
    
             # Queryは、データを取得するためのもの
         # ここで定義した型は、クライアント側で呼び出すことができる
         type Query {
             drinks: [Drink]
         }
     `
    
     // 呼び出されたkeyに対して、どのようにデータを操作するかを定義する
     const resolvers = {
         Query: {
             drinks: () => drinks,
         }
     }
    
     const server = new ApolloServer({typeDefs, resolvers})
    
     server.listen().then(({url})=>{
         console.log(`Server ready at ${url}`)
      })
    
  6. サーバーを立ち上げる

     $ npm run dev
    

    以下のような画面が開けばOKです

  7. 今回はReact + viteを使用するため、sample-graphql-apiディレクトリで以下を実行

     $ npm create vite@latest
    

    色々と聞かれるので、青字のように入力してください

     ? Project name: frontend
     ? Select a framework: React
     ? Select a variant: TypeScript
    
  8. frontendディレクトリに移動し、開発サーバーを立ち上げる

     $ cd frontend
     $ npm install
     $ npm run dev
    

    上手くいかない場合はnodeのバージョンを切り替えてみてください

     $ nodenv local 19.7.0
    

    https://zenn.dev/donchan922/articles/b08a66cf3cbbc5

  9. フロントで使用するライブラリをインストール

     $ cd frontend
     $ npm i @apollo/client @apollo/react-hooks graphql apollo-boost
    
  10. クライアント側とAPIを紐づけるため、main.tsxを以下のように修正する

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App.tsx';
    import './index.css';
    import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
    
    // どこにアクセスしに行くのかを指定して通信できるようにする
    const client = new ApolloClient({
      uri: "http://localhost:4000/",
      cache: new InMemoryCache()
    });
    
    ReactDOM.createRoot(document.getElementById('root')!).render(
      <React.StrictMode>
            {/* <App /> 配下すべてのファイルでGraphQLを使えるようになる */}
        <ApolloProvider client={client}>
        <App/>
        </ApolloProvider>
      </React.StrictMode>,
    );
    
  11. クエリを呼び出すクライアント側のコードを記述

    $ touch src/Drink.tsx
    

    Drink.tsxの中身

    import { useQuery } from "@apollo/client";
    import { gql } from '@apollo/client'
    
    type DrinkType = {
        name: string;
        price: number;
    };
    
    const DRINKS = gql`
    query {
      drinks {
        name
        price
      }
    }
    `;
    
    const Drink = () => {
      const {loading, error, data} = useQuery(DRINKS);
      return (
        <div>
            {loading && <p>取得中...</p>}
            {error && <p>{error.message}</p>}
            {data &&  data.drinks.map((drink: DrinkType, i: number) => (
                <div key={i} style={{display:"flex", gap: "6px"}}>
                <p>商品名:{drink.name}</p>  
                <p>価格:{drink.price}円</p>
                </div>
            ))}
        </div>
    )
    };
    
    export default Drink;
    
  12. 作成したコンポーネントをApp.tsxで読み込む

    App.tsxを以下のように書き換える

    import './App.css'
    import Drink from './Drink'
    
    function App() {
    
      return (
          <div>
            <h2>ドリンク一覧</h2>
            <Drink />
          </div>
      )
    }
    
    export default App
    
  13. 動作確認

    以下のように取得されたデータが表示されていればOKです

■ まとめ

実際に軽くGraphQLを触ってみて、簡単にまとめると以下のようなメリットがあることが理解できました。

  1. 1つのエンドポイントで良いため、API リクエストを処理するために大量のコードを記述しなくて良い
  2. 複数のデータソースを一回のリクエストで取得できるため、リクエスト数を最小限にできる
  3. 余計なデータを取得せずに済む
  4. 型指定でデータが明確になる

そのため、パフォーマンスの向上をより期待できる規模の大きなサービスや、クライアント毎に必要な情報のみを扱えるためマイクロサービスなどを構築する際に使用するとメリットをより感じられそうだと個人的には思いました。

次回は、認証認可周りや、実際のDBとのデータのやり取り、データを更新するミューテーションの実装など、もう少し詳しい部分についても調べられたらなと思います。

参考:

https://youtu.be/u8vD2NESjC0?si=Xfu6ffghQY_2laCZ

https://aws.amazon.com/jp/graphql/guide/

https://aws.amazon.com/jp/graphql/

https://aws.amazon.com/jp/compare/the-difference-between-graphql-and-rest/

https://hasura.io/learn/ja/graphql/intro-graphql/graphql-vs-rest/

https://qiita.com/jabba/items/8d77ab86641937847673

https://zenn.dev/waddy/books/graphql-nestjs-nextjs-bootcamp/viewer/overview_from_backend

https://zenn.dev/saboyutaka/articles/e5515872871534

https://www.apollographql.com/docs/apollo-server/getting-started/