Help us understand the problem. What is going on with this article?

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

お題

前回、GraphQLを使ったフロントエンド(ライブラリとしてApolloを採用)実装したのだけど、せっかくバックエンド(Goで実装)側がgqlgenというライブラリでスキーマファーストにしているのに、フロントエンド側ではそのスキーマで使う要素に対して自前で型を付けるやり方だった。
せっかくならフロントエンドもスキーマファーストでやりたいので、graphql-codegenを使って、GraphQLスキーマに定義した要素の型を自動生成するようにした。

関連記事索引

開発環境

# 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というファイルを設定ファイルとして使用するようなので用意。

$ 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の差分は下記。

$ 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

自動生成コードの中身

[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スキーマ自体は下記。

[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スキーマに適合した型を自動生成したものに変更する。

前回までの分

[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>

今回の修正後

[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

実際、バックエンド側のGraphQLサーバを直接叩いた結果とも一致してるので、きちんとフロントエンドからバックエンドにGraphQLでアクセスできている。
Screenshot from 2019-12-29 00-25-15.png

まとめ

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

sky0621
Go使い。最近はRustラブ。Webアプリケーション作ることが多い。フロントエンドもクラウド(GCP好き)もそれなりに触る。2019/10からGraphQLも嗜む。
https://github.com/sky0621/Curriculum-Vitae/blob/master/README.md
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away