はじめに
GraphQLをハンズオンで学ぶためにTodoアプリを作ってみました。
本記事ではpart1として、GraphQLクエリを使ったTodoアプリのバックエンド開発手順をまとめました。
part2のフロントエンド編はこちらをご覧ください。
使用した技術スタック
言語:Go、TypeScript
ライブラリ:React、gqlgen、codegen、xorm
DB:Postgres
gqlgenとcodegenはGraphQLのスキーマ定義から、それぞれGoとTypeScriptのコードを自動生成してくれるライブラリです。
xormはgoのORMライブラリで、DBにはPostgresを使用しました。
Todoアプリの概要
Todoアプリの完成イメージはこちらです。本記事ではこのバックエンド部分についてハンズオン形式でまとめます。
※ChromeのプラグインでGraphQL Network Inspectorを使っています。このプラグインでは、GraphQLのリクエスト・レスポンスのみを見ることができます。
本記事では、下記のようにGraphQL Playgroundでクエリを実行できるところまでを解説します。
また主にGraphQLに関連する部分を解説するため、適宜ソースコードを参照してください〜!🙏
ディレクトリ構造
ディレクトリ構造は下記のようになっており、本記事ではapp(バックエンドのアプリケーションコード)を作成していきます。
.
├── app - バックエンドのアプリケーションコード
│ ├── Dockerfile
│ ├── go.mod
│ ├── go.sum
│ ├── gqlgen.yml - gqlgenの設定ファイル
│ ├── graph - GraphQL関連のコード
│ │ ├── generated.go - gqlgenによって自動生成されるGoのコード
│ │ ├── model - GraphQLモデルを定義するGoのコード
│ │ │ ├── models_gen.go - gqlgenによって自動生成されるモデルのコード
│ │ │ └── todo.go
│ │ ├── resolver.go - GraphQLリゾルバのインターフェースを定義する
│ │ └── schema.resolvers.go - リゾルバインターフェースを具体的に実装するコード
│ ├── infrastructure
│ │ └── todo.go - インフラのTodoモデル
│ ├── server.go - サーバーコードのエントリーポイント
│ ├── sql
│ │ └── init.sql - 初期化SQLスクリプト
│ └── tools.go - gqlgenをインストール用のファイル
│
├── front - フロントエンドのアプリケーションコード
│ ├── Dockerfile
│ ├── codegen.yml - codegen(フロントのGraphQLコード生成)の設定ファイル
│ ├── graphql - GraphQLのクエリやスキーマを定義
│ │ ├── mutation - データを変更するGraphQLのmutation定義
│ │ │ ├── createTodo.graphql
│ │ │ ├── deleteTodo.graphql
│ │ │ └── updateTodoStatus.graphql
│ │ └── query - データを取得するGraphQLのquery定義
│ │ └── getAllTodos.graphql
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── src - アプリケーションのソースコード
│ │ ├── App.tsx - メインのアプリケーションコンポーネント
│ │ ├── components - 再利用可能なUIコンポーネント
│ │ │ ├── CreateTodoForm.tsx
│ │ │ └── TodoList.tsx
│ │ ├── index.tsx - アプリケーションのエントリーポイント
│ │ └── types - TypeScriptの型定義
│ │ └── gen - codegenによって自動生成される型定義
│ │ ├── api.ts - APIの型定義
│ │ └── possibleTypes-result.ts
│ └── tsconfig.json
│
├── docker-compose.yml
└── schema.graphqls - 共通のGraphQLスキーマを定義
Todoアプリのハンズオン
では、実際にGraphQLを使ったTodoアプリのバックエンド開発手順を解説します。
バックエンド開発手順の概要
- プロジェクトのセットアップ
- gqlgenで雛形を作成
- DBとの接続設定
- GraphQLのスキーマ定義
- gqlgenでGoのコード自動生成
- リゾルバ実装
※リゾルバとはGraphQLのQueryやMutationが実行されたときに呼び出される関数で、具体的なデータ取得やデータ操作のロジックを実装します。つまり、クライアントからのクエリに対して実際のデータをどのように返すか、またはどのように変更するかを決定する役割を担っています。
プロジェクトのセットアップ
まず、新しいディレクトリを作成し、そこでGoモジュールを初期化します。
mkdir ~/go/src/github.com/[username]/todoapp-graphql-go-react
cd ~/go/src/github.com/[username]/todoapp-graphql-go-react
mkdir app
cd app
go mod init github.com/[username]/todoapp-graphql-go-react
gqlgenで雛形を作成
以下のgqlgenコマンドを使用して新しいプロジェクトの雛形を作成します。
cd app
go run github.com/99designs/gqlgen init
gqlgenのインストール方法や、雛形を作成するための詳細はgqlgenの公式ドキュメントを参照してください🙏
DBとの接続設定
アプリケーションとデータベースとの接続を確立する必要があります。xormライブラリを使用してPostgresデータベースと接続します。ここでは、server.goファイルにデータベース接続のコードを追加します。
package main
import (
"log"
"net/http"
"os"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
_ "github.com/lib/pq"
"github.com/rs/cors"
"github.com/[username]/todoapp-graphql-go-react/app/graph"
"xorm.io/xorm"
)
const defaultPort = "8080"
func main() {
// DBへの接続情報を設定
connectionString := "user=postgres password=postgres dbname=testdb host=db port=5432 sslmode=disable"
engine, err := xorm.NewEngine("postgres", connectionString)
if err != nil {
log.Fatalln("error - create engine: ", err)
}
// DBへ接続
err = engine.Ping()
if err != nil {
log.Fatalln("error - connect DB: ", err)
}
log.Println("success - connect DB")
// 環境変数からポート番号を取得、設定されていない場合はデフォルト値を使用
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
// gqlgenのサーバを新規作成し、リゾルバとしてDB接続を渡す
srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{DB: engine}}))
// フロントエンドから接続可能にするためCORSを設定
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
})
handler := c.Handler(http.DefaultServeMux)
// ルートURLでGraphQLのPlaygroundを起動
http.Handle("/", playground.Handler("GraphQL playground", "/query"))
// /queryのパスでGraphQLのエンドポイントを設定
http.Handle("/query", srv)
// 注意: Docker環境のportとローカル環境のportが違うため、実際にローカル環境から接続するportは異なり8081である
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
// サーバを起動、エラーが発生した場合はログに出力して終了
log.Fatal(http.ListenAndServe(":"+port, handler))
}
ここで、後に実装するリゾルバでxormエンジンを使用するために、resolver.goのResolverにDBフィールドを追加します。
package graph
import "xorm.io/xorm"
type Resolver struct {
// xormエンジンのインスタンスをフィールドとして保持
DB *xorm.Engine
}
GraphQLのスキーマ定義
このステップでは、Todoアプリで呼ばれるクエリと型のスキーマ定義を行います。
このスキーマ定義により、QueryとMutationのエンドポイントが定義されます。Queryはデータの取得を、Mutationはデータの変更を行うための定義になります。
# データ取得のためのスキーマ
type Query {
todos: [Todo!]!
}
# データ変更のためのスキーマ
type Mutation {
createTodo(todoInput: CreateTodoInput!): Todo!
updateTodoStatus(todoId: ID!, done: Boolean!): Boolean!
deleteTodo(todoId: ID!): Boolean!
}
# Todoの型定義
type Todo {
id: ID!
text: String!
done: Boolean!
}
# Todo作成の際に必要な引数を定義
input CreateTodoInput {
text: String!
}
gqlgenでGoのコード自動生成
gqlgenを使って、GraphQLのスキーマからGoのコードを自動生成します。
cd app
go run github.com/99designs/gqlgen generate
自動生成する際は、app/gqlgen.yml(スキーマからGoのコードを自動生成するための設定ファイル)をもとに生成されます。
# スキーマ定義ファイルのパスを指定
schema:
- ../*.graphqls
# gqlgenによって自動生成される実行可能なGraphQLコードの設定
exec:
filename: graph/generated.go # 生成するファイル名
package: graph # 生成するパッケージ名
# gqlgenによって自動生成されるモデルコードの設定
model:
filename: graph/model/models_gen.go # 生成するファイル名
package: model # 生成するパッケージ名
# リゾルバの設定
resolver:
layout: follow-schema # スキーマと同じ構造でリゾルバを生成する
dir: graph # リゾルバを生成するディレクトリ
package: graph # リゾルバを生成するパッケージ名
# スキーマの型とGoの型のマッピング設定
models:
ID: # GraphQLのID型に対応するGoの型を指定
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int: # GraphQLのInt型に対応するGoの型を指定
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
リゾルバ実装
リゾルバの実装では、上記で定義した各エンドポイントの実際の動作を定義します。例えば、createTodoエンドポイントは、新しいTodo項目を作成し、その新しいTodo項目を返します。
具体的には、createTodoは以下のような動作を行います。
- UUIDを生成して新しいTodo項目を作成
- 生成したTodo項目をデータベースに保存
- エラーが発生しなければ、新しいTodo項目を返す
他のエンドポイントも同様に、具体的なデータ操作やデータ取得のロジックを実装します。
package graph
import (
"context"
"log"
"github.com/google/uuid"
"github.com/[username]/todoapp-graphql-go-react/app/graph/model"
"github.com/[username]/todoapp-graphql-go-react/app/infrastructure"
)
// 新しいTodoを作成するためのリゾルバ
func (r *mutationResolver) CreateTodo(ctx context.Context, todoInput model.CreateTodoInput) (*model.Todo, error) {
// 新しいUUIDの生成
newUUID, err := uuid.NewRandom()
if err != nil {
log.Printf("Error generating UUID: %v\n", err)
return nil, err
}
// 新しいTodoの作成
todo := &infrastructure.Todo{
ID: newUUID.String(),
Text: todoInput.Text,
}
// DBに新しいTodoを挿入
_, err = r.DB.Insert(todo)
if err != nil {
log.Printf("Error insert todo: %v\n", err)
return nil, err
}
// 新しいTodoを返す
return model.NewTodo(todo), nil
}
// Todoのステータスを更新するためのリゾルバ
func (r *mutationResolver) UpdateTodoStatus(ctx context.Context, todoID string, done bool) (bool, error) {
todo := &infrastructure.Todo{
Done: done,
}
// 指定されたIDのTodoのステータスを更新
_, err := r.DB.ID(todoID).Cols("done").Update(todo)
if err != nil {
log.Printf("Error update todo status: %v\n", err)
return false, err
}
// 更新成功を返す
return true, nil
}
// Todoを削除するためのリゾルバ
func (r *mutationResolver) DeleteTodo(ctx context.Context, todoID string) (bool, error) {
// 指定されたIDのTodoを削除
_, err := r.DB.ID(todoID).Delete(&infrastructure.Todo{})
if err != nil {
log.Printf("Error delete todo: %v\n", err)
return false, err
}
// 削除成功を返す
return true, nil
}
// 全てのTodoを取得するためのリゾルバ
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
var todos []*infrastructure.Todo
// DBから全てのTodoを取得
r.DB.AllCols().Find(&todos)
// 取得したTodoを返す
return model.NewTodos(todos), nil
}
ここまでが、GraphQL、Goを使ってTodoアプリのバックエンドを開発する基本的なステップです。必要に応じて各ステップの詳細な説明や、それぞれの技術の基本的な知識などは、各技術の公式ドキュメントを参照してください!👍
おわりに
簡単なTodoアプリ開発ですが、GraphQLの基本的な部分をハンズオンで学べてよかったです!
まだ学べていない範囲もあるので、これからも勉強していきます。
ここまで読んでいただきありがとうございました!
参考リンク