graphql-codegenでフロントエンドをGraphQLスキーマファースト

# お題
[前回](https://qiita.com/sky0621/items/0297f5a026be380a5929)、GraphQLを使ったフロントエンド（ライブラリとして[Apollo](https://www.apollographql.com/)を採用）実装したのだけど、せっかくバックエンド（Goで実装）側が[gqlgen](https://gqlgen.com/)というライブラリでスキーマファーストにしているのに、フロントエンド側ではそのスキーマで使う要素に対して自前で型を付けるやり方だった。
せっかくならフロントエンドもスキーマファーストでやりたいので、[graphql-codegen](https://graphql-code-generator.com/)を使って、GraphQLスキーマに定義した要素の型を自動生成するようにした。

# 関連記事索引
- 第12回「[GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)](https://qiita.com/sky0621/items/957c010ba01c7a756ef7)」
- 第11回「[Dataloadersを使ったN+1問題への対応](https://qiita.com/sky0621/items/621f075e4257270a9e02)」
- 第10回「[GraphQL(gqlgen)エラーハンドリング](https://qiita.com/sky0621/items/affc833e770fee8b576e)」
- 第９回「[GraphQLにおける認証認可事例（Auth0 RBAC仕立て）](https://qiita.com/sky0621/items/efbb8fa085094230d0fd)」
- 第８回「[GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例](https://qiita.com/sky0621/items/5c2c565678c102d9d4ed)」
- 第７回「[GraphQLにおけるRelayスタイルによるページング実装（後編：フロントエンド）](https://qiita.com/sky0621/items/fdcdaefeef7cf4b750ac)」
- 第６回「[GraphQLにおけるRelayスタイルによるページング実装（前編：バックエンド）](https://qiita.com/sky0621/items/1e8823200633f2c46013)」
- 第５回「[DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動](https://qiita.com/sky0621/items/58fa2bcfc0935f6186cb)」
- 第４回「[graphql-codegenでフロントエンドをGraphQLスキーマファースト](https://qiita.com/sky0621/items/f03631f0309f5c375b1d)」
- 第３回「[go+gqlgenでGraphQLサーバを作る（GORM使ってDB接続）](https://qiita.com/sky0621/items/32e3e26189589080e492)」
- 第２回「[NuxtJS(with Apollo)のTypeScript対応](https://qiita.com/sky0621/items/0297f5a026be380a5929)」
- 第１回「[frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る](https://qiita.com/sky0621/items/8abd445edba347e8f6f1)」

# 開発環境
## # OS - Linux(Ubuntu)
```
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
```

## # フロントエンド
### Nuxt.js
```
$ cat yarn.lock | grep "@nuxt/vue-app"
    "@nuxt/vue-app" "2.11.0"
"@nuxt/vue-app@2.11.0":
  resolved "https://registry.yarnpkg.com/@nuxt/vue-app/-/vue-app-2.11.0.tgz#05aa5fd7cc69bcf6a763b89c51df3bd27b58869e"
```

### パッケージマネージャ - Yarn
```
$ yarn -v
1.19.2
```

### IDE - WebStorm
```
WebStorm 2019.3
Build #WS-193.5233.80, built on November 25, 2019
```

# 実践

今回のソース一式は下記。
https://github.com/sky0621/study-graphql/tree/v0.4.0

## 導入

以下を参考に。
https://graphql-code-generator.com/docs/getting-started/

```
$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ ll package.json 
-rw-rw-r-- 1 sky0621 sky0621 906 Dec 22 00:49 package.json
$
$ yarn add graphql
yarn add v1.19.2
　　・
　　・
　　・
Done in 7.70s.
$
$ yarn add -D @graphql-codegen/cli
yarn add v1.19.2
　　・
　　・
　　・
Done in 16.59s.
$
$ yarn add -D @graphql-codegen/typescript
yarn add v1.19.2
　　・
　　・
　　・
Done in 12.19s.
$
$ yarn add -D @graphql-codegen/typescript-operations
yarn add v1.19.2
　　・
　　・
　　・
Done in 8.53s.
```

コード自動生成は`graphql-codegen`というコマンドで行う。
上記コマンドは`codegen.yml`というファイルを設定ファイルとして使用するようなので用意。

```yaml
$ cat codegen.yml 
overwrite: true
schema: http://localhost:5050/query
generates:
  ./gql-types.d.ts:
    plugins:
      - typescript
      - typescript-operations
```

`schema`という項目にはGraphQLスキーマを参照できるサーバを指定。要するにGraphQLサーバ。
今回はローカルでGoで実装したGraphQLサーバを立てていて、そのポートが`5050`なのでそこを指定。

あとは、`package.json`にコード自動生成コマンドを登録
`yarn add`も含めた最終的な`package.json`の差分は下記。

```json
$ git diff package.json
diff --git a/frontend/package.json b/frontend/package.json
index b0b174f..6afbdea 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,16 +9,21 @@
     "build": "nuxt build",
     "start": "nuxt start",
     "generate": "nuxt generate",
-    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
+    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
+    "codegen": "graphql-codegen"
   },
   "dependencies": {
     "@nuxtjs/apollo": "^4.0.0-rc17",
     "@typescript-eslint/parser": "^2.12.0",
+    "graphql": "^14.5.8",
     "nuxt": "^2.0.0",
     "nuxt-property-decorator": "^2.5.0",
     "vue-apollo": "^3.0.2"
   },
   "devDependencies": {
+    "@graphql-codegen/cli": "^1.9.1",
+    "@graphql-codegen/typescript": "^1.9.1",
+    "@graphql-codegen/typescript-operations": "^1.9.1",
     "@nuxt/typescript-build": "^0.5.2",
```

## コード自動生成

あ、`npm`使ってしまった。

```
$ npm run codegen

> frontend@1.0.0 codegen /home/sky0621/src/github.com/sky0621/study-graphql/frontend
> graphql-codegen

  ✔ Parse configuration
  ✔ Generate outputs
$
$ ll gql-types.d.ts 
-rw-r--r-- 1 sky0621 sky0621 1.1K Dec 28 23:51 gql-types.d.ts
```

## 自動生成コードの中身

```ts:[frontend/gql-types.d.ts]
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string,
  String: string,
  Boolean: boolean,
  Int: number,
  Float: number,
};

export type Mutation = {
   __typename?: 'Mutation',
  createTodo: Scalars['ID'],
  createUser: Scalars['ID'],
};


export type MutationCreateTodoArgs = {
  input: NewTodo
};


export type MutationCreateUserArgs = {
  input: NewUser
};

export type NewTodo = {
  text: Scalars['String'],
  userId: Scalars['String'],
};

export type NewUser = {
  name: Scalars['String'],
};

export type Query = {
   __typename?: 'Query',
  todos: Array<Todo>,
  todo: Todo,
  users: Array<User>,
  user: User,
};


export type QueryTodoArgs = {
  id: Scalars['ID']
};


export type QueryUserArgs = {
  id: Scalars['ID']
};

export type Todo = {
   __typename?: 'Todo',
  id: Scalars['ID'],
  text: Scalars['String'],
  done: Scalars['Boolean'],
  user: User,
};

export type User = {
   __typename?: 'User',
  id: Scalars['ID'],
  name: Scalars['String'],
  todos: Array<Todo>,
};
```

生成元であるGraphQLスキーマ自体は下記。

```graphql:[schema/schema.graphql]
# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
  todos: [Todo!]!
}

type Query {
  todos: [Todo!]!
  todo(id: ID!): Todo!

  users: [User!]!
  user(id: ID!): User!
}

input NewTodo {
  text: String!
  userId: String!
}

input NewUser {
  name: String!
}

type Mutation {
  createTodo(input: NewTodo!): ID!
  createUser(input: NewUser!): ID!
}
```

これで、機能追加、変更を行う時には、まず、GraphQLスキーマを修正する。
で、バックエンド側もフロントエンド側も各自動生成コマンドによってスキーマ定義に依存する部分は手動メンテが不要になった。

## 自動生成コードの適用

前回は同じコンポーネント内に自前で定義していたGraphQLスキーマに適合した型を自動生成したものに変更する。

### 前回までの分

```typescript:[frontend/components/TodoCard.vue]
<template>
  <div>
    <v-row>
      <v-col cols="12" sm="6" offset-sm="3">
        <v-card>
          <v-list two-line subheader>
            <v-list-item v-for="todo in todos" :key="todo.id" link>
              <v-list-item-avatar>
                <v-icon>mdi-gift-outline</v-icon>
              </v-list-item-avatar>
              <v-list-item-content>
                <v-list-item-title>{{ todo.text }}</v-list-item-title>
                <v-list-item-subtitle>{{ todo.done }}</v-list-item-subtitle>
              </v-list-item-content>
              <v-list-item-content>
                <v-list-item-title>
                  {{ todo.user.name }}
                </v-list-item-title>
              </v-list-item-content>
            </v-list-item>
          </v-list>
        </v-card>
      </v-col>
    </v-row>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import 'vue-apollo'
import todos from '~/apollo/queries/todos.gql'

// 自前で定義
interface User {
  id: String
  name: String
}

// 自前で定義
interface Todo {
  id: String
  text: String
  done: Boolean
  user: User
}

@Component({
  apollo: {
    todos: {
      prefetch: true,
      query: todos
    }
  }
})
export default class TodoCard extends Vue {
  todos: Todo[] = []
}
</script>
```

### 今回の修正後

```typescript:[frontend/components/TodoCard.vue]
<template>
　　〜〜　差分無しなので省略　〜〜
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import 'vue-apollo'
import todos from '~/apollo/queries/todos.gql'
// eslint-disable-next-line no-unused-vars
import { Todo } from '~/gql-types'
// ↑自前定義していた interface の代わりに自動生成された型を使う。

@Component({
  apollo: {
    todos: {
      prefetch: true,
      query: todos
    }
  }
})
export default class TodoCard extends Vue {
  todos: Todo[] = []
}
</script>
```

## 適用後の動作確認

```
$ yarn dev
yarn run v1.19.2
$ nuxt --port 3000

   ╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.11.0                           │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://localhost:3000/      │
   │                                             │
   ╰─────────────────────────────────────────────╯

ℹ Preparing project for development                                                                                                                                 00:17:06
ℹ Initial build may take a while                                                                                                                                    00:17:06
✔ Builder initialized                                                                                                                                               00:17:06
✔ Nuxt files generated                                                                                                                                              00:17:06
ℹ Starting type checking service...                                                                                                                 nuxt:typescript 00:17:10
ℹ Using 1 worker with 2048MB memory limit                                                                                                           nuxt:typescript 00:17:10

✔ Client
  Compiled successfully in 17.19s

✔ Server
  Compiled successfully in 14.76s

ℹ No type errors found                                                                                                                              nuxt:typescript 00:17:27
ℹ Version: typescript 3.7.4                                                                                                                         nuxt:typescript 00:17:27
ℹ Time: 13927ms                                                                                                                                     nuxt:typescript 00:17:27

 WARN  Compiled with 1 warnings                                                                                                                     friendly-errors 00:17:27

Module Warning (from ./node_modules/eslint-loader/dist/cjs.js):                                                                                     friendly-errors 00:17:27

/home/sky0621/src/github.com/sky0621/study-graphql/frontend/plugins/apollo-error-handler.js
  2:3  warning  Unexpected console statement  no-console

✖ 1 problem (0 errors, 1 warning)

                                                                                                                                                    friendly-errors 00:17:27
You may use special comments to disable some warnings.                                                                                              friendly-errors 00:17:27
Use // eslint-disable-next-line to ignore the next line.                                                                                            friendly-errors 00:17:27
Use /* eslint-disable */ to ignore all warnings in a file.                                                                                          friendly-errors 00:17:27
ℹ Waiting for file changes                                                                                                                                          00:17:27
ℹ Memory usage: 649 MB (RSS: 768 MB)                                                       
```

さて、ブラウザで確認。（この画面では登録済みのユーザ別のTODOを全て表示する）
![Screenshot from 2019-12-29 00-24-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/bbd95be9-e84b-928c-5e2b-6ab910833c94.png)

実際、バックエンド側のGraphQLサーバを直接叩いた結果とも一致してるので、きちんとフロントエンドからバックエンドにGraphQLでアクセスできている。
![Screenshot from 2019-12-29 00-25-15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180599/251205d9-176c-f7b7-c696-fd1680a92e72.png)

# まとめ
せっかくスキーマ定義できる技術をベースに開発するのであれば、やっぱり極力ボイラープレートは避けたい。
これでフロントエンドもバックエンドも”型”で縛る開発スタイルになった。
次回は、[前回](https://qiita.com/sky0621/items/32e3e26189589080e492)言及したN+1問題の解消か、ページング（GraphQLでは[Relay](https://facebook.github.io/relay/graphql/connections.htm#)と呼ぶカーソルベース接続方式に準拠するのが筋が良いらしい）の対応のどちらかにトライしよう。