LoginSignup
49

More than 3 years have passed since last update.

frontendに「nuxtjs/apollo」、backendに「go+gqlgen」の組み合わせでGraphQLサービスを作る

Last updated at Posted at 2019-11-16

お題

簡易ToDoアプリ(と言っても、この記事上では単に「ToDoの新規登録」と「登録済みの全ToDoの表示」しか出来ない)を題材として、表題の組み合わせでGraphQL通信ロジックを書くとどんな感じになるかを確認してみる。
※今回はRDBを使った永続化などはしないで、バックエンドからは固定値を返す。

前提

Nuxt.jsやGoについては、個人ないし会社での開発経験があり、フロント->サーバの接続は(例えば)Axiosなど使ってRESTでやってたけど、GraphQLに変えてみたいという人が対象。
ただ、「そもそもGraphQLとは?」とか「RESTと比べたメリット・デメリットは?」みたいなことは書かない。(既に記事がいっぱいある。)

また、お題に則り最小限の構造で実装する。
巷で流行りのClean ArchitectureやDDDに即したパッケージ構成とかは考えない。
そのへんは、一度↓みたいな記事を書いた。↓でなく、もっといい記事はググればたくさん出てくる。
Clean Architecture by Golang(with Echo & Gorm & wire)

関連記事索引

開発環境

# 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.10.2"

パッケージマネージャ - Yarn

$ yarn -v
1.19.1

IDE - WebStorm

WebStorm 2019.2.4
Build #WS-192.7142.35, built on October 29, 2019

# バックエンド

言語 - Go

$ go version
go version go1.13.3 linux/amd64

パッケージマネージャ - Go Modules

IDE - Goland

GoLand 2019.2.5
Build #GO-192.7142.48, built on November 8, 2019

参考

GraphQL

フロントエンド

バックエンド

実践

GitHub上に適当なリポジトリを作ってローカルにgit clone
自分の環境だとこんな感じ。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql

フロントエンド

create-nuxt-appコマンドを使ってフロント用のNuxt.jsプロジェクトを作成

ツール類の選択は以下参照。

  • UIはVuetify.js
  • SSRとSPAはどっちでもいいけどデフォのSSR
  • 今回はテストまで考えてないのでテストフレームワークは無し
  • サーバーサイドとの接続はGraphQLなのでAxiosなどは無し
$ yarn create nuxt-app frontend
yarn create v1.19.1
   ・・・
success Installed "create-nuxt-app@2.11.1" with binaries:
      - create-nuxt-app

create-nuxt-app v2.11.1
✨  Generating Nuxt.js project in frontend
? Project name frontend
? Project description My ace Nuxt.js project
? Author name sky0621
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
yarn run v1.19.1
$ eslint --ext .js,.vue --ignore-path .gitignore . --fix
Done in 2.90s.

🎉  Successfully created project frontend

現時点で以下のようなディレクトリ構成となっている。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql
$
$ tree -L 2
.
├── frontend
│   ├── assets
│   ├── components
│   ├── layouts
│   ├── middleware
│   ├── node_modules
│   ├── nuxt.config.js
│   ├── package.json
│   ├── pages
│   ├── plugins
│   ├── README.md
│   ├── static
│   ├── store
│   └── yarn.lock
└── README.md

そして、パッケージのdevDependenciesも念のため記載。

$ cat frontend/package.json 
{
  ・・・
  "dependencies": {
    "nuxt": "^2.0.0"
  },
  "devDependencies": {
    "@nuxtjs/vuetify": "^1.0.0",
    "@nuxtjs/eslint-config": "^1.0.1",
    "@nuxtjs/eslint-module": "^1.0.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-config-prettier": "^4.1.0",
    "eslint-plugin-prettier": "^3.0.1",
    "prettier": "^1.16.4"
  }
}

nuxtjs/apolloの導入

パッケージ追加

$ cd frontend/
$
$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ yarn add @nuxtjs/apollo
yarn add v1.19.1
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
  ・・・
Done in 15.54s.

nuxt-config.jsへ設定追記

$ git diff frontend/nuxt.config.js
diff --git a/frontend/nuxt.config.js b/frontend/nuxt.config.js
index 25a0fe4..afc0db4 100644
--- a/frontend/nuxt.config.js
+++ b/frontend/nuxt.config.js
@@ -42,7 +42,7 @@ export default {
   /*
    ** Nuxt.js modules
    */
-  modules: [],
+  modules: ['@nuxtjs/apollo'],
   /*
    ** vuetify module configuration
    ** https://github.com/nuxt-community/vuetify-module
@@ -64,6 +64,18 @@ export default {
       }
     }
   },
+
+  apollo: {
+    clientConfigs: {
+      default: {
+        // Goサーバを 8080 ポートで起動する予定のため
+        httpEndpoint: 'http://localhost:8080/'
+      }
+    },
+    // 任意だけど、これがないとGraphQL的なエラー起きた時に原因が掴みづらいため
+    errorHandler: '~/plugins/apollo-error-handler.js'
+  },
+
   /*
    ** Build configuration
    */

GraphQLエラーハンドリングプラグイン

$ cat frontend/plugins/apollo-error-handler.js 
export default (error, context) => {
  console.log(error)
  context.error({ statusCode: 304, message: 'Server error' })
}

参考:https://github.com/nuxt-community/apollo-module

フロントエンド起動

ここまででいったん起動してみる。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ yarn run dev
yarn run v1.19.1
$ nuxt

 WARN  Address localhost:8080 is already in use.                                                                                                                                                                                                           11:40:12

ℹ Trying a random port...                                                                                                                                                                                                                                  11:40:12

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

ℹ Preparing project for development                                                                                                                                                                                                                        11:40:13
ℹ Initial build may take a while                                                                                                                                                                                                                           11:40:13
✔ Builder initialized                                                                                                                                                                                                                                      11:40:13
✔ Nuxt files generated                                                                                                                                                                                                                                     11:40:13

● Client █████████████████████████ building (35%) 215/225 modules 10 active
 node_modules/vuetify/dist/vuetify.js

● Server █████████████████████████ building (19%) 81/81 modules 0 active



 ERROR  Failed to compile with 1 errors                                                                                                                                                                                                    friendly-errors 11:40:31


 ERROR  in ./plugins/apollo-error-handler.js                                                                                                                                                                                               friendly-errors 11:40:31

Module Error (from ./node_modules/eslint-loader/dist/cjs.js):                                                                                                                                                                              friendly-errors 11:40:31

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

✖ 2 problems (1 error, 1 warning)
  1 error and 0 warnings potentially fixable with the `--fix` option.

                                                                                                                                                                                                                                           friendly-errors 11:40:31
 @ ./.nuxt/apollo-module.js 93:13-57
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi eventsource-polyfill webpack-hot-middleware/client?reload=true&timeout=30000&ansiColors=&overlayStyles=&name=client&path=/__webpack_hmr/client ./.nuxt/client.js
                                                                                                                                                                                                                                           friendly-errors 11:40:31
ℹ Waiting for file changes                                                                                                                                                                                                                                 11:40:31
ℹ Memory usage: 433 MB (RSS: 559 MB)                                                                                                                                                                                                                       11:40:31

 ERROR  ENOSPC: System limit for number of file watchers reached, watch '/home/sky0621/src/github.com/sky0621/study-graphql/frontend/node_modules/@nuxt/vue-app/template/views/loading'                                                                    11:40:31

  at FSWatcher.start (internal/fs/watchers.js:165:26)
  at Object.watch (fs.js:1329:11)
  at createFsWatchInstance (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:118:15)
  at setFsWatchListener (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:165:15)
  at NodeFsHandler._watchWithNodeFs (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:330:14)
  at NodeFsHandler._handleDir (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:551:19)
  at runMicrotasks (<anonymous>)
  at processTicksAndRejections (internal/process/task_queues.js:93:5)
  at async NodeFsHandler._addToNodeFs (node_modules/@nuxt/builder/node_modules/chokidar/lib/nodefs-handler.js:600:16)

怒られた。Prettierさんチェックが入った。
言われた通り、lint fix する。

$ yarn run lint --fix
yarn run v1.19.1
$ eslint --ext .js,.vue --ignore-path .gitignore . --fix

/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)

Done in 2.27s.

すると、起動できた。
なんか1件 WARN は出てるけど・・・これはエラーハンドラー内でデバッグ用にconsoleログ吐いてるからか。
本番リリース時は消すという前提でいったん無視(まあ、本番リリースしないし)。

  〜〜〜

↻ Updated plugins/apollo-error-handler.js                                                                                                                                                                                                                  11:45:05

✔ Client
  Compiled successfully in 694.86ms

✔ Server
  Compiled successfully in 823.17ms


 WARN  Compiled with 1 warnings                                                                                                                                                                                                            friendly-errors 11:45:07

Module Warning (from ./node_modules/eslint-loader/dist/cjs.js):                                                                                                                                                                            friendly-errors 11:45:07

/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 11:45:07
You may use special comments to disable some warnings.                                                                                                                                                                                     friendly-errors 11:45:07
Use // eslint-disable-next-line to ignore the next line.                                                                                                                                                                                   friendly-errors 11:45:07
Use /* eslint-disable */ to ignore all warnings in a file.                                                                                                                                                                                 friendly-errors 11:45:07

起動ログの冒頭で「 http://localhost:44171/ 」と記載があったので、そのポートで起動チェック。
screenshot-localhost-44171-2019.11.16-11-56-29.png
うん、OK。GraphQL的なあれこれはバックエンド作ってから確認するので、フロントエンドは、ひとまずここまで。

バックエンド

バックエンド用ソース格納ディレクトリとGraphQLスキーマ格納ディレクトリを作成

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql
$ tree -L 1
.
├── frontend
└── README.md

1 directory, 1 file
$ mkdir backend
$ tree -L 1
.
├── backend
├── frontend
└── README.md

Goプロジェクト初期化

$ cd backend/
$ go mod init github.com/sky0621/study-graphql/backend
go: creating new go.mod: module github.com/sky0621/study-graphql/backend$ tree -L 1
.
└── go.mod

0 directories, 1 file
$ cat go.mod 
module github.com/sky0621/study-graphql/backend

go 1.13

gqlgenコマンドによるスケルトン生成

一気にいろいろなファイルが自動生成される。(GraphQLスキーマすら書いてないのにサンプルを用意してくれる)

$ go run github.com/99designs/gqlgen init
Exec "go run ./server/server.go" to start GraphQL server
$
$ tree -L 2
.
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
├── models_gen.go
├── resolver.go
├── schema.graphql
└── server
    └── server.go

自動生成物のチェック

GraphQLスキーマ

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

type Query {
  todos: [Todo!]!
}

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

type Mutation {
  createTodo(input: NewTodo!): Todo!
}

実は、お題を『簡易ToDoアプリ』としたのは、gqlgen がデフォルトで自動生成するスキーマが↑だからでした。
gqlgenはスキーマファーストを前提とするライブラリなので、このスキーマを元にして、初回だけ自動生成されるソースとgqlgenコマンド実行のたびに自動生成されるソースとを組み合わせて機能を実現していく。

リゾルバー

GraphQLスキーマで定義されたQuery(この事例だと「todos: [Todo!]!」(要するにToDo一覧取得))とMutation(この事例だと「createTodo(input: NewTodo!): Todo!」(要するにToDo新規登録))のロジックを実装するソース。
※このソースは、gqlgenコマンドによって初回だけ自動生成されるソース。(以降は、このファイルを消すか、gqlgen.yml の設定をいじらない限り再生成はされない。)

[resolver.go]
package backend

import (
    "context"
) // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.

type Resolver struct{}

func (r *Resolver) Mutation() MutationResolver {
    return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
    return &queryResolver{r}
}

type mutationResolver struct{ *Resolver }

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
    panic("not implemented")
}

type queryResolver struct{ *Resolver }

func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
    panic("not implemented")
}

上記の panic("not implemented") のところを、フロントエンドから呼ばれた時に実行したロジックに変える。
というわけで、↓のような感じで修正した。

$ git diff
diff --git a/backend/resolver.go b/backend/resolver.go
index d015cbe..9bbe326 100644
--- a/backend/resolver.go
+++ b/backend/resolver.go
@@ -16,11 +16,38 @@ func (r *Resolver) Query() QueryResolver {
 type mutationResolver struct{ *Resolver }

 func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
-       panic("not implemented")
+       return &Todo{
+               ID:   "todo001",
+               Text: "部屋の掃除",
+               Done: false,
+               User: &User{
+                       ID:   "user001",
+                       Name: "たろー",
+               },
+       },nil
 }

 type queryResolver struct{ *Resolver }

 func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
-       panic("not implemented")
+       return []*Todo{
+               &Todo{
+                       ID:   "todo001",
+                       Text: "部屋の掃除",
+                       Done: false,
+                       User: &User{
+                               ID:   "user001",
+                               Name: "たろー",
+                       },
+               },
+               &Todo{
+                       ID:   "todo002",
+                       Text: "買い物",
+                       Done: true,
+                       User: &User{
+                               ID:   "user001",
+                               Name: "たろー",
+                       },
+               },
+       },nil
 }

Goサーバー起動ロジック

gqlgenコマンドによる自動生成対象にmain関数及びWebサーバー起動ロジックも含まれている。
8080ポートで起動し、ルートパスでGraphQLのプレイグラウンドが表示されるようになっている。(もう、いたれりつくせり)

[server/server.go]
package main

import (
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/handler"
    "github.com/sky0621/study-graphql/backend"
)

const defaultPort = "8080"

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    http.Handle("/", handler.Playground("GraphQL playground", "/query"))
    http.Handle("/query", handler.GraphQL(backend.NewExecutableSchema(backend.Config{Resolvers: &backend.Resolver{}})))

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

GraphQLサーバー起動

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/backend
$
$ go run server/server.go 
2019/11/16 12:33:49 connect to http://localhost:8080/ for GraphQL playground

で、ブラウザで「http://localhost:8080/」を開くと、こんな感じ。
左側にGraphQLサーバー宛にクエリを書く欄があるので、赤枠のように記載し、
screenshot-localhost-8080-2019.11.16-12-37-10.png
真ん中の実行ボタンを押すと、
screenshot-localhost-8080-2019.11.16-12-38-57.png
resolver.goTodos(ctx context.Context) ([]*Todo, error)に実装したレスポンスが返ってくる。

はい。こんなふうに赤枠内のようなクエリを書くプラットフォームを用意すれば、クエリの内容に応じた結果を返してくれる。
ちなみに、GraphQLの凄いところは、以下のように、欲しい情報だけに絞ってクエリ書けば、それだけが結果として返ってくるというところ。
screenshot-localhost-8080-2019.11.16-12-43-04.png
これが今までのRESTに対するGraphQLの大きなメリットのよう。

ただ、今回はresolver.goの中で固定値を返しているので気にならないけど、RDBへ永続化することを考えると、わりとバックエンドの実装がつらくなりそうな感は出てくると思う。
実際、業務でGraphQLのフロントエンドとバックエンド両方書いているけど、情報のやり取りという点に限って言えば、バックエンドの方が圧倒的につらい。
(まあ、そのへんは、次の記事あたりでRDB接続するコードを書く予定なので、その時に)

さて、Todo新規登録の方の確認は(クエリと一緒なので)端折りつつ、バックエンドはここまで。
最後は、フロントエンドをいじって、バックエンドのGraphQLサーバーと接続させる。

再びフロントエンド

Apolloを使ってGraphQLサーバ接続

Queryファイルの用意

バックエンドでプレイグラウンドで試したように、GraphQLサーバにリクエストするにはQueryを書く必要があるので用意。
apollo というディレクトリを追加し、配下に構造を作って「Todo一覧取得」用のQueryファイルを格納。

$ pwd
/home/sky0621/src/github.com/sky0621/study-graphql/frontend
$
$ tree -L 1
.
├── apollo
├── assets
├── components
   ・・・
$
$ tree -L 2 apollo/
apollo/
└── queries
    └── todos.gql
$
$ cat apollo/queries/todos.gql 
query todos {
  todos {
    id
    text
    done
    user {
      id
      name
    }
  }
}

フロントエンドトップページ

開くとTodo一覧を表示するよう修正。

フロントエンドは以下のディレクトリ構成になっている。

$ tree -L 1
.
├── apollo
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package.json
├── pages
├── plugins
├── README.md
├── static
├── store
└── yarn.lock

このうち、pages/index.vue を修正する。
また、 pages から呼び出すコンポーネントとして components/TodoCard.vue を作成する。

[pages/index.vue]
<template>
  <div>
    <TodoCard />
  </div>
</template>

<script>
import TodoCard from '~/components/TodoCard.vue'

export default {
  components: { TodoCard }
}
</script>

ページの方はコンポーネントを呼び出すだけだから良いでしょう。
以下のコンポーネントで、Todo一覧をApollo経由で取得している。

[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>
import todos from '~/apollo/queries/todos.gql'
export default {
  data() {
    return {
      todos: []
    }
  },

  apollo: {
    todos: {
      prefetch: true,
      query: todos
    }
  }
}
</script>

GraphQLサーバに問い合わせるQueryは、あらかじめ用意していた拡張子 gql のファイルからimportすることが出来る。

import todos from '~/apollo/queries/todos.gql'

nuxt-config.js@nuxtjs/apollo をモジュールとして読み込む設定にしており、Vueファイル上で以下のように定義することでGraphQlサーバに問い合わせた結果を data に定義した todos に格納してくれる。

  apollo: {
    todos: {
      prefetch: true,
      query: todos
    }
  }

一覧表示に使う Vuetify のコンポーネントは v-card を使うことにした。( v-data-table でもよかったけど)
表示部分は上記で todos に取得したTODO一覧を v-for でぐるぐる回しつつ各項目を表示しているだけなので説明は無しで。

あぁ、そうだ。忘れてた。GraphQLサーバのパスは「/query」なのでnuxt-config.jsを以下のように修正。

diff --git a/frontend/nuxt.config.js b/frontend/nuxt.config.js
index afc0db4..8454218 100644
--- a/frontend/nuxt.config.js
+++ b/frontend/nuxt.config.js
@@ -69,7 +69,7 @@ export default {
     clientConfigs: {
       default: {
         // Goサーバを 8080 ポートで起動する予定のため
-        httpEndpoint: 'http://localhost:8080/'
+        httpEndpoint: 'http://localhost:8080/query'
       }
     },

で、このように実装したものをブラウザで表示してみると、↓のようになる。
screenshot-localhost-35567-2019.11.16-15-04-51.png
ちゃんとGoのGraphQLサーバーで resolver.go から返すように実装した内容が表示されている。

まとめ

時間の都合でTodo新規登録は端折ったけど、GraphQL使ってフロントエンドとバックエンドつなぐ実装事例は出せたかな。
ただ、今回のお題と作り込みレベルだとGraphQLを使うべきモチベーションは、まだ湧かないかも・・・。
フロントエンド目線からすると、1ページに一覧として複数階層構造の情報を表示する時に、これまでは階層ごとにAPIを叩いてたみたいなのがなくなって、1ページに必要な情報が一気に取ってこれて(GraphQLライブラリの恩恵で)フロントエンドで使いやすいよう構造化までしてくれるというのが嬉しいかな。
が、逆に、バックエンドは考慮しないといけないことが目白押し、かつ、GraphQL自体は仕様としては存在するものの、例えば「認証」、「ページング」など個別の具体的な実装までは規定していないので、使うライブラリ次第では自前でゴリゴリ車輪の再発明をする必要性に迫られる。
Prismaのようなフレームワーク的(?)なものもあるようだけど、もうちょっとほうぼうでの実績、ナレッジが溜まってこないとプロダクションレベルで使うのは怖い。。。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49