GoでGraphQLを導入する

今回は`gqlgen`を使ってGraphQLのサーバーを立ち上げるまでのチュートリアルを作成していきます

- Github: https://github.com/tf63/go-graph-exp

- 実行環境: https://github.com/tf63/go-graph-exp/blob/main/docker/backend/Dockerfile

### GraphQLってどういうもの?
RESTとの違いを示します

REST
- 複数のエンドポイント
- エンドポイントごとに固定のデータを取得
- HTTPメソッドによって実行する処理を決定
- HTTPステータスコードでエラーハンドリング
- 型定義があいまい

GraphQL
- 単一のエンドポイント
- エンドポイントから必要なデータを指定して取得
- query, mutationによって実行する処理を決定
- メッセージでエラーハンドリング
- 型定義が厳密

RESTでは，クライアントが必要なデータを取得するために複数のエンドポイントから過剰なデータを取得していました

GraphQLでは，クライアントは単一のエンドポイントから必要な分だけデータを取得することが可能で，無駄なデータ通信やリクエストを削減できます

### GoでGraphQLを使ってみる

GoのGraphQLライブラリとして`gqlgen`があります

インストール方法

```bash
    go get github.com/99designs/gqlgen@latest
    go install github.com/99designs/gqlgen@latest

    go get github.com/99designs/gqlgen/graphql/handler
```

### GraphQLプロジェクトの作成

プロジェクトの初期化
```
    gqlgen init
```

デフォルトではこのような構成でファイルが生成されます

`graph/`は後で作り直すので，一旦削除しましょう

```bash
    .
    ├── gqlgen.yml
    ├── graph
    │   ├── generated.go # GraphQLサーバー (触らない)
    │   ├── model
    │   │   └── models_gen.go # スキーマで定義したmodel (触らない)
    │   ├── resolver.go # 雛形 (触る)
    │   ├── schema.graphqls # スキーマ　(触る)
    │   └── schema.resolvers.go # 雛形 (触る)
    └── server.go
```

### GraphQL スキーマの定義

`gqlgen`は`*.graphqls`に記述したGraphQLのスキーマから，雛形となるコードを自動生成してくれます

`api/graph`ディレクトリを作成し，今回作成するGraphQLのスキーマとして`api/graph/todo.graphqls`を用意します

- (参考) https://graphql.org/learn/schema

- GraphQLのデータ操作は，`Query`と`Mutation`に分けられます
- `Query`はデータ取得に相当します
- `Mutation`はそれ以外の操作に相当します

```todo.graphqls
    # Todoモデル
    type Todo {
        id: ID!
        text: String!
        done: Boolean!
    }

    # データ取得
    type Query {
        todos: [Todo!]!
    }

    # mutationのinput
    input NewTodo {
        text: String!
    }

    # データ作成
    # ここにupdateTodoとかdeleteTodoとかも書いたりする
    type Mutation {
        createTodo(input: NewTodo!): Todo!
    }
```

このスキーマから雛形となるコードを自動生成してみましょう

自動生成されるコードの構成は`gqlgen.yml`で指定できます

今回は次の構成にしてみます

```gqlgen.yml
    # スキーマファイルへのパス
    schema:
        - api/graph/*.graphqls

    # 自動生成されるコード (サーバー内部の動作など)
    exec:
        filename: api/graph/generated.go
        package: graph

    # 自動生成されるコード (モデル)
    model:
        filename: api/graph/types.gen.go
        package: graph

    # 自動生成される雛形 (これを実装する)
    resolver:
        layout: follow-schema
        dir: internal/resolver
        package: resolver
        filename_template: "{name}.resolvers.go"

    (略)
```

変更した`gqlgen.yml`をもとにコードを再生成します

- `server.go`がエラーを吐いてしまうので中身をすべてコメントアウトしておいたほうが良いかもしれません

```bash
    gqlgen generate
```

プロジェクトの構成を変えたので，合うように`server.go`を修正します
```server.go
    package main

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

        "github.com/99designs/gqlgen/graphql/handler"
        "github.com/99designs/gqlgen/graphql/playground"
        "github.com/tf63/go-graph-exp/api/graph"         // 修正
        "github.com/tf63/go-graph-exp/internal/resolver" // 修正
    )

    const defaultPort = "8080"

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

        srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &resolver.Resolver{}})) // 修正

        http.Handle("/", playground.Handler("GraphQL playground", "/query"))
        http.Handle("/query", srv)

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

次の構成になっているはずです
```bash
.
├── api
│   └── graph
│       ├── generated.go # 触らない
│       ├── todo.graphqls # スキーマファイル
│       └── types.gen.go # 触らない
├── gqlgen.yml
├── internal
│   └── resolver
│       ├── resolver.go # 触る
│       └── todo.resolvers.go # 触る
└── server.go # 触る
```

### Resolverの実装

いよいよResolverを実装していきましょう．`internal/resolver/`のファイルをいじっていきます

デフォルトでは次のようになっています

```resolver.go
    package resolver

    type Resolver struct{}
```

```todo.resolvers.go
    package resolver

    import (
        "context"
        "fmt"

        "github.com/tf63/go-graph-exp/api/graph"
    )

    // CreateTodo is the resolver for the createTodo field.
    func (r *mutationResolver) CreateTodo(ctx context.Context, input graph.NewTodo) (*graph.Todo, error) {
        panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
    }

    // Todos is the resolver for the todos field.
    func (r *queryResolver) Todos(ctx context.Context) ([]*graph.Todo, error) {
        panic(fmt.Errorf("not implemented: Todos - todos"))
    }

    // Mutation returns graph.MutationResolver implementation.
    func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} }

    // Query returns graph.QueryResolver implementation.
    func (r *Resolver) Query() graph.QueryResolver { return &queryResolver{r} }

    type mutationResolver struct{ *Resolver }
    type queryResolver struct{ *Resolver }
```

`todo.resolvers.go`の`CreateTodo`, `Todos`を実装していきます

本記事では，実際にDBに接続して動作するところまでは紹介しません

([Github](https://github.com/tf63/go-graph-exp)上では実装しているので興味があれば)

```todo.resolvers.go
    // CreateTodo is the resolver for the createTodo field.
    func (r *mutationResolver) CreateTodo(ctx context.Context, input graph.NewTodo) (*graph.Todo, error) {
        // inputを元にTodoを生成
        // -----------------------------------
        // (今回は実装しません)
        // -----------------------------------

        // 作成したTodoを返す
        todo1 := graph.Todo{
            ID:   "1",
            Text: "todo 1",
            Done: false,
        }
        return &todo1, nil
    }

    // Todos is the resolver for the todos field.
    func (r *queryResolver) Todos(ctx context.Context) ([]*graph.Todo, error) {
        // テストデータを返す
        todos := []*graph.Todo{
            {ID: "1", Text: "todo 1", Done: false},
            {ID: "2", Text: "todo 2", Done: false},
            {ID: "3", Text: "todo 3", Done: false},
        }

        return todos, nil
    }
```

### GraphQLサーバーの起動

GraphQLのサーバーを起動してみます

一応`Dockerfile`を用意してあります
- https://github.com/tf63/go-graph-exp/blob/main/docker/backend/Dockerfile

```bash
    go run server.go
```

`http://localhost:8080`にアクセスするとGraphQL PlayGroundが表示されます
![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3291419/7e74d563-86c3-53db-53b3-c4a2be1097ca.png)


先程作成した`createTodo`を実行してみます

実行結果としてid, textを取得してみます

意味はないですが，引数には`text: "todo 1"`を指定します

```graphql
    mutation {
        createTodo (input: {text: "todo 1"}) {
            id # idを返す
            text # textを返す
        }
    }
```

id, textを含んだレスポンスが返ってきます
```json
    {
        "data": {
            "createTodo": {
                "id": "1",
                "text": "todo 1"
            }
        }
    }
```

`query`も実行してみましょう

todoのid, text, done属性を選んで取得してみます
```graphql
    query {
        todos {
            id,
            text,
            done
        }
    }
```

先程登録したテストデータが返ってきます
```json
    {
        "data": {
            "todos": [
                {
                    "id": "1",
                    "text": "todo 1",
                    "done": false
                },
                {
                    "id": "2",
                    "text": "todo 2",
                    "done": false
                },
                {
                    "id": "3",
                    "text": "todo 3",
                    "done": false
                }
            ]
        }
    }
```
![graph-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3291419/e31ef532-4975-ac5f-a14b-3b5e36bbabcf.png)

以上です．余裕があればロジック部分も記事にします

### 参考

gqlgen公式チュートリアル

https://gqlgen.com/getting-started

GraphQL スキーマ

https://graphql.org/learn/schema

GraphQLベストプラクティス

https://maku.blog/p/4reqy9i

API設計の参考

https://github.com/koga456/sample-api/tree/master