【GraphQL】GraphQLをざっくり理解した


### 本記事の内容
[GraphQL](https://graphql.org/)についてのざっくり理解したので、その内容をまとめます。

### きっかけ
[GitHub REST API](https://docs.github.com/ja/rest?apiVersion=2022-11-28)を使って、タスク管理をトライしようと色々調べていたところ、project(v2)に関するAPIがGraphQLに移行しましたという情報に触れてGraphQLとは何ぞや？と

::: note info
2025/04/01にGitHub Project(classic)のサポートが終了しています
[Sunset Notice - Projects (classic) - GitHub Changelog](https://github.blog/changelog/2024-05-23-sunset-notice-projects-classic/)
:::

### GraphQLとは

 - Facebookが2012年に開発、2015年にオープンソース化されたAPIクエリ言語

 - REST APIの代替として使われることが多い

 - クライアントが必要なデータだけを一つのエンドポイントで取得できる

### REST API　と　GraphQL API　の比較
|比較項目|REST API|GraphQL API|
|:-----------|:------------|:------------|
|エンドポイント|リソースごとに複数必要|通常1つ|
|データ取得量|多すぎる or 足りない可能性|必要なデータだけ取得可能|
|バージョニング|必要になりがち|スキーマで柔軟に対応可能|

各項目の比較について見ていきましょう。

#### エンドポイントに関する比較　REST　vs　GraphQL
|比較項目|REST API|GraphQL API|
|:-----------|:------------|:------------|
|エンドポイント|リソースごとに複数必要|通常1つ|

##### - RESTの場合のエンドポイント
RESTでは、リソース（例：ユーザー、投稿、コメント）ごとに異なるエンドポイントが必要です。
 - 例:

   - /users　　　　　　(ユーザー一覧取得)
   - /users/1　　　　　(ユーザー詳細取得)
   - /users/1/posts　　(あるユーザーの投稿取得)
   - /users/1/posts/created-at　　(あるユーザーの投稿の投稿時間を取得)


クライアントが複数の種類のデータを取得したい場合、複数のリクエストが必要になることがあります。

##### - GraphQLの場合のエンドポイント
GraphQLでは通常、1つのエンドポイント（例：/graphql）に対して、**クエリ（query）**　を使って必要な情報を指定して取得します。

クライアントは必要なリソースやフィールドを一度に指定できるため、1回のリクエストで複数のリソースを取得することができます。

柔軟で効率的なデータ取得が可能になります。

#### データ取得量に関する比較　REST　vs　GraphQL
|比較項目|REST API|GraphQL API|
|:-----------|:------------|:------------|
|データ取得量|多すぎる or 足りない可能性|必要なデータだけ取得可能|

##### - RESTの場合のデータ取得量
RESTのレスポンスは通常、固定された形式で返されるため、必要以上のデータが含まれることがよくあります。

逆に、必要な情報が別のリソースに分かれていて、追加のリクエストが必要な場合もあります。

クライアント側でデータをフィルター・整形する必要があることが多いです。

##### - GraphQLの場合のデータ取得量
クライアントはクエリで取得したいフィールドを明示的に指定するため、必要なデータだけ取得できます。

データ量を最小限に抑えられるため、通信量の削減にもつながります。

フロントエンドの開発者がバックエンドに依存せず柔軟にデータを扱えます。


#### バージョニングに関する比較　REST　vs　GraphQL 
|比較項目|REST API|GraphQL API|
|:-----------|:------------|:------------|
|バージョニング|必要になりがち|スキーマで柔軟に対応可能|

##### - RESTの場合のバージョニング
APIの仕様変更があると、新しいバージョン（v1, v2など）を作成する必要があります。

例：/api/v1/users → /api/v2/users

古いクライアントとの互換性を維持するため、複数のバージョンのAPIを同時に保守する必要が出てきます。

##### - GraphQLの場合のバージョニング
スキーマに変更があっても、**フィールド単位で非推奨（deprecated）** にすることが可能。

古いフィールドを残しつつ、新しいフィールドを追加できるため、バージョン分けの必要がほとんどありません。

### 例で比較してみる
想定シナリオとして、
`ユーザーの名前と、そのユーザーが書いた投稿の投稿日（createdAt）を取得したい` とします

![Qiita-examples-REST-and-GraphQL.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606327/14571e7a-7b44-4d26-b6d8-074676089005.jpeg)


 - RESTの場合
   - 例１：　バックエンドのレスポンスから必要な情報をフィルタリングして抽出する
   - 例２：　バックエンドのレスポンス自体を必要なもののみのエンドポイントを新しく定義する
   - 例３：　バックエンド側にBFFとして必要情報を抽出するロジックを実装する
 - GraphQLの場合
   - 例４：　GraphQLのスキーマ設定により必要情報のみのレスポンスを取得するようにする場合

#### - RESTの場合

##### 例１：バックエンドのレスポンスから必要な情報をフィルタリングして抽出する

特定のユーザーの投稿内容を取得するエンドポイントを実装したとします。

```
GET /users/1/posts
```

下記のようなレスポンスが返ってくるようにバックエンドで定義したとします。
```
[
  {
    "id": 101,
    "title": "はじめての投稿",
    "content": "これはテスト投稿です",
    "createdAt": "2024-05-01T10:00:00Z"
  },
  {
    "id": 102,
    "title": "GraphQLについて",
    "content": "GraphQLは便利です",
    "createdAt": "2024-05-02T15:30:00Z"
  }
]
```

レスポンスの中には投稿日以外の内容も含まれているため、フロントエンド側でフィルタリングして投稿日を表示する必要があります。

``` js:投稿日の取得、表示の例①（React＋Axios）
// CreatedAtListFromFullApi.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';

const CreatedAtListFromFullApi = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await axios.get('/users/1/posts');
        const data = response.data;

        // id と createdAt だけ抽出
        const filtered = data.map(post => ({
          id: post.id,
          createdAt: post.createdAt,
        }));

        setPosts(filtered);
      } catch (error) {
        console.error('データ取得エラー:', error);
      }
    };

    fetchPosts();
  }, []);

  return (
    <div>
      <h2>作成日時一覧（REST API）</h2>
      <ul>
        {posts.map(({ id, createdAt }) => (
          <li key={id}>
            ID: {id}, 作成日時: {createdAt}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CreatedAtListFromFullApi;

```

##### 例２：バックエンドのレスポンス自体を必要なもののみのエンドポイントを新しく定義する
特定のユーザーの投稿の投稿日のみを取得するエンドポイントを実装したとします。

```
GET /users/1/posts/created-at
```

```
[
  {
    "id": 101,
    "createdAt": "2024-05-01T10:00:00Z"
  },
  {
    "id": 102,
    "createdAt": "2024-05-02T15:30:00Z"
  }
]
```

この場合は、フロントエンド側でバックエンドのレスポンスをそのまま使用するように設定します。
``` js:投稿日の取得、表示の例②（React＋Axios）
// CreatedAtListFromCreatedAtApi.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';

const CreatedAtListFromCreatedAtApi = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchCreatedAt = async () => {
      try {
        const response = await axios.get('/users/1/posts/created-at');
        setPosts(response.data);
      } catch (error) {
        console.error('作成日時取得エラー:', error);
      }
    };

    fetchCreatedAt();
  }, []);

  return (
    <div>
      <h2>作成日時一覧（created-at API）</h2>
      <ul>
        {posts.map(({ id, createdAt }) => (
          <li key={id}>
            ID: {id}, 作成日時: {createdAt}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CreatedAtListFromCreatedAtApi;

```

##### 例3：バックエンドのレスポンスから必要な情報をフィルタリングして抽出するためのBFF（Backend For Frontend）を実装する
特定のユーザーの投稿内容を取得するエンドポイントを実装したとします。

```
GET /users/1/posts
```

下記のようなレスポンスが返ってくるようにバックエンドで定義したとします。
```
[
  {
    "id": 101,
    "title": "はじめての投稿",
    "content": "これはテスト投稿です",
    "createdAt": "2024-05-01T10:00:00Z"
  },
  {
    "id": 102,
    "title": "GraphQLについて",
    "content": "GraphQLは便利です",
    "createdAt": "2024-05-02T15:30:00Z"
  }
]
```
ここまでは例1と同じですが、バックエンド側でフロントエンドに渡すべき情報をフィルタリングするロジック（BFF）を実装します

バックエンドをNode.js + Expressで実装した場合のBFFの実装例を以下に示します。
``` js:BFF（Backend For Frontend）の実装例(Node.js + Express)
// bffServer.js
const express = require('express');
const axios = require('axios');
const app = express();
const PORT = 3001;

app.get('/users/:userId/posts/created-at', async (req, res) => {
  const { userId } = req.params;

  try {
    const response = await axios.get(`http://your-backend-api.local/users/${userId}/posts`);
    const posts = response.data;

    // id と createdAt だけを抽出
    const filtered = posts.map(post => ({
      id: post.id,
      createdAt: post.createdAt
    }));

    res.json(filtered);
  } catch (error) {
    console.error('BFF エラー:', error.message);
    res.status(500).json({ error: '取得に失敗しました' });
  }
});

app.listen(PORT, () => {
  console.log(`BFF running at http://localhost:${PORT}`);
});
```

BFFでフィルタリングしたレスポンスをもとに、フロントエンドで必要な情報を表示します。
``` js:投稿日の取得、表示の例③（React＋Axios）
// CreatedAtListFromBff.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';

const CreatedAtListFromBff = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchFromBff = async () => {
      try {
        const response = await axios.get('http://localhost:3001/users/1/posts/created-at');
        setPosts(response.data);
      } catch (error) {
        console.error('BFFからの取得エラー:', error);
      }
    };

    fetchFromBff();
  }, []);

  return (
    <div>
      <h2>作成日時一覧（BFF）</h2>
      <ul>
        {posts.map(({ id, createdAt }) => (
          <li key={id}>
            ID: {id}, 作成日時: {createdAt}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CreatedAtListFromBff;

```

### GraphQLを使用する場合
#### 例４：GraphQLを使用して必要情報を抽出したレスポンスを取得するようにする場合

GraphQLを使用する場合、エンドポイントは下記のように一元化されます。
```
POST /graphql
```

GraphQLサーバーのスキーマ定義で取り出したい情報のクエリを設定します。
```
query GetPosts {
  posts {
    id
    createdAt
  }
}
```

GraphQLのスキーマ設定に対応して得られるレスポンスは下記のようになります。
```
{
  "data": {
    "posts": [
      {
        "id": 101,
        "title": "はじめての投稿",
        "createdAt": "2024-05-01T10:00:00Z"
      },
      {
        "id": 102,
        "title": "GraphQLについて",
        "createdAt": "2024-05-02T15:30:00Z"
      }
    ]
  }
}

```


GraphQLで必要情報を抽出したレスポンスを、フロントエンドで表示します。
``` js:投稿日の取得、表示の例④（React＋Axios）
// CreatedAtListFromGraphql.jsx
import React, { useEffect, useState } from 'react';
import { request, gql } from 'graphql-request';

const GRAPHQL_ENDPOINT = 'http://localhost:4000/graphql'; // ← GraphQLサーバーのエンドポイントに応じて調整

const CreatedAtListFromGraphql = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchFromGraphql = async () => {
      const query = gql`
        query {
          posts {
            id
            createdAt
          }
        }
      `;

      try {
        const data = await request(GRAPHQL_ENDPOINT, query);
        setPosts(data.posts);
      } catch (error) {
        console.error('GraphQLからの取得エラー:', error);
      }
    };

    fetchFromGraphql();
  }, []);

  return (
    <div>
      <h2>作成日時一覧（GraphQL）</h2>
      <ul>
        {posts.map(({ id, createdAt }) => (
          <li key={id}>
            ID: {id}, 作成日時: {createdAt}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CreatedAtListFromGraphql;

```

### 終わりに
以上、GraphQLのざっくり理解でした。
実際に実装に反映できたらアウトプットしたいと思います！