今回はgqlgen
を使ってGraphQLのサーバーを立ち上げるまでのチュートリアルを作成していきます
GraphQLってどういうもの?
RESTとの違いを示します
REST
- 複数のエンドポイント
- エンドポイントごとに固定のデータを取得
- HTTPメソッドによって実行する処理を決定
- HTTPステータスコードでエラーハンドリング
- 型定義があいまい
GraphQL
- 単一のエンドポイント
- エンドポイントから必要なデータを指定して取得
- query, mutationによって実行する処理を決定
- メッセージでエラーハンドリング
- 型定義が厳密
RESTでは,クライアントが必要なデータを取得するために複数のエンドポイントから過剰なデータを取得していました
GraphQLでは,クライアントは単一のエンドポイントから必要な分だけデータを取得することが可能で,無駄なデータ通信やリクエストを削減できます
GoでGraphQLを使ってみる
GoのGraphQLライブラリとしてgqlgen
があります
インストール方法
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/
は後で作り直すので,一旦削除しましょう
.
├── 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
を用意します
-
GraphQLのデータ操作は,
Query
とMutation
に分けられます -
Query
はデータ取得に相当します -
Mutation
はそれ以外の操作に相当します
# 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
で指定できます
今回は次の構成にしてみます
# スキーマファイルへのパス
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
がエラーを吐いてしまうので中身をすべてコメントアウトしておいたほうが良いかもしれません
gqlgen generate
プロジェクトの構成を変えたので,合うように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))
}
次の構成になっているはずです
.
├── api
│ └── graph
│ ├── generated.go # 触らない
│ ├── todo.graphqls # スキーマファイル
│ └── types.gen.go # 触らない
├── gqlgen.yml
├── internal
│ └── resolver
│ ├── resolver.go # 触る
│ └── todo.resolvers.go # 触る
└── server.go # 触る
Resolverの実装
いよいよResolverを実装していきましょう.internal/resolver/
のファイルをいじっていきます
デフォルトでは次のようになっています
package resolver
type Resolver struct{}
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上では実装しているので興味があれば)
// 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
を用意してあります
go run server.go
http://localhost:8080
にアクセスするとGraphQL PlayGroundが表示されます
先程作成したcreateTodo
を実行してみます
実行結果としてid, textを取得してみます
意味はないですが,引数にはtext: "todo 1"
を指定します
mutation {
createTodo (input: {text: "todo 1"}) {
id # idを返す
text # textを返す
}
}
id, textを含んだレスポンスが返ってきます
{
"data": {
"createTodo": {
"id": "1",
"text": "todo 1"
}
}
}
query
も実行してみましょう
todoのid, text, done属性を選んで取得してみます
query {
todos {
id,
text,
done
}
}
先程登録したテストデータが返ってきます
{
"data": {
"todos": [
{
"id": "1",
"text": "todo 1",
"done": false
},
{
"id": "2",
"text": "todo 2",
"done": false
},
{
"id": "3",
"text": "todo 3",
"done": false
}
]
}
}
以上です.余裕があればロジック部分も記事にします
参考
gqlgen公式チュートリアル
GraphQL スキーマ
GraphQLベストプラクティス
API設計の参考