お題
前回、GraphQLを使ったフロントエンド(ライブラリとしてApolloを採用)実装したのだけど、せっかくバックエンド(Goで実装)側がgqlgenというライブラリでスキーマファーストにしているのに、フロントエンド側ではそのスキーマで使う要素に対して自前で型を付けるやり方だった。
せっかくならフロントエンドもスキーマファーストでやりたいので、graphql-codegenを使って、GraphQLスキーマに定義した要素の型を自動生成するようにした。
関連記事索引
- 第12回「GraphQLにおけるRelayスタイルによるページング実装再考(Window関数使用版)」
- 第11回「Dataloadersを使ったN+1問題への対応」
- 第10回「GraphQL(gqlgen)エラーハンドリング」
- 第9回「GraphQLにおける認証認可事例(Auth0 RBAC仕立て)」
- 第8回「GraphQL/Nuxt.js(TypeScript/Vuetify/Apollo)/Golang(gqlgen)/Google Cloud Storageの組み合わせで動画ファイルアップロード実装例」
- 第7回「GraphQLにおけるRelayスタイルによるページング実装(後編:フロントエンド)」
- 第6回「GraphQLにおけるRelayスタイルによるページング実装(前編:バックエンド)」
- 第5回「DB接続付きGraphQLサーバ(by Golang)をローカルマシン上でDockerコンテナ起動」
- 第4回「graphql-codegenでフロントエンドをGraphQLスキーマファースト」
- 第3回「go+gqlgenでGraphQLサーバを作る(GORM使ってDB接続)」
- 第2回「NuxtJS(with Apollo)のTypeScript対応」
- 第1回「frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせで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
自動生成コードの中身
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 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スキーマに適合した型を自動生成したものに変更する。
前回までの分
<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>
今回の修正後
<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を全て表示する)
実際、バックエンド側のGraphQLサーバを直接叩いた結果とも一致してるので、きちんとフロントエンドからバックエンドにGraphQLでアクセスできている。
まとめ
せっかくスキーマ定義できる技術をベースに開発するのであれば、やっぱり極力ボイラープレートは避けたい。
これでフロントエンドもバックエンドも”型”で縛る開発スタイルになった。
次回は、前回言及したN+1問題の解消か、ページング(GraphQLではRelayと呼ぶカーソルベース接続方式に準拠するのが筋が良いらしい)の対応のどちらかにトライしよう。