GraphQLとVue3を使ってポケモンのデータを取得してみる

# 概要
現在弊社で開発中のサービス要件を満たす技術選定の際に、データ取得の方法としてRESTFul APIの他に選択肢としてGraphQLが挙げられることがありました。

その時点でのGraphQLの理解が**JSON（のようなもの？）を返すAPIみたいなもの**という
なんともふわっとしたものだったので、これはいけないと思い年末年始に少し触ってみました。

# 開発環境
- yarn 1.22.19
- Typescript 4.9.3
- Vue.js 3.2.45
- vite 4.0.0
- graphql 16.6.0
etc...

# GraphQLとは

[GraphQL公式](https://graphql.org/)

GraphQLとは**API向けのクエリ言語**です。

CircleCIブログ[GraphQLとは？メリットや概要を入門ガイドで学ぶ](https://circleci.com/ja/blog/introduction-to-graphql/)では以下のようにまとめられています。

> - GraphQLは、クライアントアプリケーションが必要なデータをAPIからフェッチするために設計された言語です。
> - GraphQLを使用すると、クライアントアプリケーションはバックエンドAPIから必要なデータの型とシェイプを取得できます。
> - GraphQLでは、どんなタイプのリクエストでも、クライアント アプリケーションが呼び出すエンドポイントは1つだけです。
> - GraphQLはSQLとよく似ていますが、フロントエンドで機能します。

なんとなく分かってきましたが、まだ使い所がよく分かりませんね。
GraphQLの比較対象としてRESTFul APIが挙げられますが、それぞれどのように違うのか、以下で説明します。


# RESTFul APIとの違い

### RestFul API
詳細は[RESTful API とは](https://aws.amazon.com/jp/what-is/restful-api/)に記載がありますが、特徴の１つとして**取得するリソースを特定するために、URIやGET・POSTなどリクエストメソッド等の情報が必要**です。

従って、複数のリソースを取得するには複数のリクエスト（URIなど）が必要となります。

以下にRESTFul APIのリソース取得フローを示します。
図鑑No.25のポケモンの名前と種族値を取得しています。

```mermaid
sequenceDiagram
  APP->>+API: HTTP Request(GET /api/pokemon?id=25)
  API->>+APP: HTTP Responce①

  APP->>+API: HTTP Request(GET /api/basestats?pokemon_id=25)
  API->>+APP: HTTP Responce②

```

```Responce①.json
{
    "id": 25,
    "name": "Pikachu"
}
```

```Responce②.json
{
    "hitPoints": 35,
    "attack": 55,
    "defense": 40,
    "specialAttack": 50,
    "specialDefense": 50,
    "speed": 90
}
```

### GraphQL
次に、GraphQLで同じデータを取得するフローを示します。
エンドポイントが１つのみであり、取得するリソースはクエリによって決定します。


```mermaid
sequenceDiagram
  APP->>+API: HTTP Request(POST /graphql)
  API->>+APP: HTTP Responce①
```

POSTするクエリは以下の通りです。
```graphql
query{
    pokemon(id: 25) {
        id
        name
        basestats {
            hitPoints
            attack
            defense
            specialAttack
            specialDefense
            speed
        }
    }
}
```

```Responce.json
    "pokemon" {
        "id": 25,
        "name": "Pikachu",
        "basestats": {
            "hitPoints": 35,
            "attack": 55,
            "defense": 40,
            "specialAttack": 50,
            "specialDefense": 50,
            "speed": 90
        }
    }
```


このようにGraphQL APIでは1回のリクエストでアプリが必要とするすべてのデータを取得することができます。

GraphQLを利用する際は、関連する必要なデータを1回で取得するクエリを作り、それを**POST**します。
上記の例では、API呼び出しを2つ使うのではなく、APIへのリクエスト1つでポケモンと種族値をネストされたJSONオブジェクトとして取得しています。


# どのような時に使用するか

上記で示した通り、GraphQLの大きな利点は

- クエリによって、欲しいデータのみを全て取得することができる
- その際のAPIへのリクエストは一度だけでよい
- エンドポイントが単一である

ということです。

例えば、関連するデータが多くRESTFul APIでは何回も呼び出しが必要な場合は、GraphQLの利用を検討するとよいでしょう。
呼び出し回数が削減され、サービスのパフォーマンス向上が期待できます。


# Vue.jsからGraphqlを利用してデータ取得・絞り込み検索を実装

それでは、実際に使用してみます。
このあたりの記事が参考になりました。
[GraphQL APIをVueJSフロントエンドに接続する方法](https://takeshiamano.medium.com/graphql-api%E3%82%92vuejs%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95-545e06421a7e)


今回は練習として、ポケモンの名前、タイプ、画像を取得し、表示します。

GraphQLのエンドポイントとして、[Pokemon API](https://wayfair.github.io/dociql/)のものを使用します。
```
https://graphql-pokemon2.vercel.app
```
ちなみに、練習用に使用できるGraphQL APIはこちらにあります。
[8 Free to Use GraphQL APIs for Your Projects and Demos](https://www.apollographql.com/blog/community/backend/8-free-to-use-graphql-apis-for-your-projects-and-demos/)


[GraphQL Playground](https://www.graphqlbin.com/v2/new)に上記のエンドポイントを入力してみましょう。
すると、以下のようにSchema定義などが確認できます。
<img width=600 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/4bbd5916-ac68-efe9-229b-4ec3ee3d08e6.png">

```graphql
type Attack {
  name: String
  type: String
  damage: Int
}

type Pokemon {
  id: ID!
  number: String
  name: String
  weight: PokemonDimension
  height: PokemonDimension
  classification: String
  types: [String]
  resistant: [String]
  attacks: PokemonAttack
  weaknesses: [String]
  fleeRate: Float
  maxCP: Int
  evolutions: [Pokemon]
  evolutionRequirements: PokemonEvolutionRequirement
  maxHP: Int
  image: String
}

type PokemonAttack {
  fast: [Attack]
  special: [Attack]
}

type PokemonDimension {
  minimum: String
  maximum: String
}

type PokemonEvolutionRequirement {
  amount: Int
  name: String
}

type Query {
  query: Query
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}
```
これを参考に実装を進めます。
まずは、以下のようにapollo関連のパッケージをインストールします。

>Apolloは、アプリでGraphQLを使うためのツール群であり、コミュニティの取り組みでもあります。
>[Vue Apollo](https://apollo.vuejs.org/guide/#introduction)

```
yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag @vue/apollo-composable
```
最終的なpackage.jsonは以下の通りです。

```package.json
{
  "name": "vue-graphql",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@types/express": "^4.17.15",
    "@vue/apollo-composable": "^4.0.0-beta.1",
    "apollo-cache-inmemory": "^1.6.6",
    "apollo-client": "^2.6.10",
    "apollo-link": "^1.2.14",
    "apollo-link-http": "^1.5.17",
    "express": "^4.18.2",
    "express-graphql": "^0.12.0",
    "graphql": "^16.6.0",
    "graphql-tag": "^2.12.6",
    "vue": "^3.2.45",
    "vue-apollo": "^3.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "typescript": "^4.9.3",
    "vite": "^4.0.0",
    "vue-tsc": "^1.0.11"
  }
}
```

次に、GraphQLを使用する準備をします。


```main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import VueApollo from 'vue-apollo';

// GraphQL APIへの接続を確立
const httpLink = new HttpLink({
  uri: 'https://graphql-pokemon2.vercel.app'
});

// ApolloClientインスタンス生成
export const apolloClient = new ApolloClient({
  // データが正しいAPIからポーリングされるように設定
  link: httpLink,
  // キャッシュを渡す。InMemoryCacheはApolloClientのデフォルトのキャッシュ実装であるのでこれを使用
  cache: new InMemoryCache(),
  // Apollo Client Devtoolsを、Webブラウザのインスペクタに「Apollo」タブとして表示する
  connectToDevTools: true
});

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});


const app = createApp(App);
// VueApolloをvueで使用する
app.use(apolloProvider);
app.mount('#app');

```

今回取得するポケモンのデータの型定義をしておきます。


```pokemon.ts
export type Pokemon =  {
    name: string;
    types: string[];
    image: string;
}
```

検索フォームのコンポーネントをApp.vueから読み込みます。


```App.vue
<script setup lang="ts">
import PokemonForm from './components/PokemonForm.vue';
</script>

<template>
  <div>ポケモン検索したい！</div>
  <pokemon-form />
</template>
```

検索フォームを、ポケモンの名前（英語表記）での絞り込みができるよう実装します。


コンポーネントのsetup()外でuseQuery()を使用したい場合、
`provideApolloClient(apolloClient);`が必要です。
ドキュメントによるとこれは、Vueのprovide/injectメカニズムでクライアントをインジェクトできないからのようです。
[Usage outside of setup](https://v4.apollo.vuejs.org/guide-composable/setup.html#usage-outside-of-setup)

使用しない場合は以下のエラーが発生します。
Uncaught Error: Apollo client with id default not found. Use provideApolloClient() if you are outside of a component setup.



```PokemonForm.vue
<template>
  <form>
    <input type="text" placeholder="Name" v-model="name">
    <input type="button" value="検索" @click="(e) => {pokemonQuery(name, e)}">
  </form>
  <pokemon-list v-if="fetchdata" :pokemons="fetchdata.pokemons" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useQuery, provideApolloClient } from '@vue/apollo-composable';
import gql from "graphql-tag"
import { apolloClient } from '../main';
import PokemonList from "./PokemonList.vue"
import { Pokemon } from '../model/pokemon';

const name = ref<string>();

type Query = {
  pokemons: Pokemon[]
} | undefined;

const fetchdata = ref<Query>();

// https://v4.apollo.vuejs.org/guide-composable/setup.html#usage-outside-of-setup
// 
provideApolloClient(apolloClient);

const getPokemonByName = (name: string) => {
  console.log("getpPokemonByName");
  const query =  gql`
  query($name: String) {
    pokemon(name: $name) {
      name
      types
      image
    }
  }
  `;

  const {onResult} = useQuery<Query>(query, {
    name
  });

  onResult(result => {
    fetchdata.value = {pokemons: [result.data.pokemon]};
  });
};

const getAllPokemons = () => {
  const query =  gql`
    query {
      pokemons(first: 150){
        name
        types
        image
      }
    }
    `;

  const {onResult} = useQuery<Query>(query);

  onResult(result => {
    fetchdata.value = result.data;
  });
};

const pokemonQuery = (name: string | undefined, event: MouseEvent) => {
  event.preventDefault();
  if (!name) {
    getAllPokemons();
    return;
  };
  getPokemonByName(name);
};

getAllPokemons();

</script>
```
このとき、結果の取得とfetchdataの更新は以下のように行なっています。
```vue
  const {onResult} = useQuery<Query>(query, {
    name
  });

  onResult(result => {
    fetchdata.value = {pokemons: [result.data.pokemon]};
  });
```
注意点として、以下のようにresultプロパティを取得することも可能ですが、fetchdataの更新ができずundefinedとなってしまいます。
これは、useQueryによるデータ取得は非同期処理であるため、データ取得が完了する前に代入処理を実行してしまうからです。

```vue
  hogehoge() => {
    const {result} = useQuery<Query>(query, {
      name
    });
    fetchdata.value = {pokemons: [result.data.pokemon]};
  }
```

次に、ポケモンのリスト表示用コンポーネントを実装します。
初回表示時には、150匹のポケモンを表示するためポケモン１種類ごとにループさせています。

```PokemonList.vue
<script setup lang="ts">
import pokemonItem from './pokemonItem.vue';
import { Pokemon } from '../model/pokemon';

const {pokemons} = defineProps<{pokemons: Pokemon[]}>();
</script>

<template>
  <div v-for="pokemon in pokemons">
    <pokemon-item :pokemon="pokemon" />
  </div>
</template>

```
最後にポケモン１種類を表示するコンポーネントを実装します。
タイプはStringの配列であるため、join()で連結して表示しています。

```PokemonItem.vue
<script setup lang="ts">
import { Pokemon } from '../model/pokemon';

const {pokemon} = defineProps<{pokemon: Pokemon}>();

</script>

<template>
  <div>
    <h3>{{ pokemon.name }}</h3>
    <p>{{ pokemon.types.join("・") }}</p>
    <img :src="pokemon.image" width="100">
  </div>
</template>

```

### 完成！

<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/15537777-2e4f-09b5-7bf6-c1deb3ba266e.png" />

ピカチュウを検索してみます。
<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/1482b588-2ba1-02e5-419c-3ca29ea5e0d3.png" />

# GraphQLを使用する上での注意点
詳細は[GraphQLを導入する時に考えておいたほうが良いこと](https://engineering.mercari.com/blog/entry/20220303-concerns-with-using-graphql/)に、GraphQLの恩恵を受けるために考慮すべき点とその対策が記載されています。

上記の記事で紹介されている課題のうちいくつか例をあげると、
- データを取得する際、N+1問題が起こる場合がある
- クエリが複雑になるとリクエストボディが肥大化する
- POSTを利用するためHTTPキャッシュに乗らない
などがあります。


# まとめ
GraphQLを利用する際は、関連する必要なデータを1回で取得するクエリを作り、それを**POST**します。
RESTFul　APIに比べて、API呼び出しの回数が削減され、サービスのパフォーマンスの向上が期待できます。

一方で、愚直な実装をすると十分にGraphQLの恩恵を受けられないことがあるため、今後も勉強していきたいですね。