Goにおけるgqlgenを使った型安全なスキーマファーストGraphQLサーバ構築
Emily Parker
Product Engineer · Leapcell

堅牢で保守性の高い API の構築は、現代のソフトウェア開発の基盤です。 アプリケーションが複雑化するにつれて、効率的なデータ取得、クライアントとサーバー間の明確な契約、および開発ワークフローの簡素化の必要性が最重要視されます。 従来、REST API は一般的な選択肢でしたが、過剰取得、過少取得、および複数のエンドポイントを管理する複雑さといった課題をしばしば提示します。 ここで GraphQL が輝き、柔軟で強力な代替手段を提供します。 しかし、単に GraphQL を使用するだけでは十分ではありません。 その利点を真に活用するため、特に Go のような強く型付けされた言語では、型安全でスキーマファーストのアプローチを採用することが不可欠です。 この記事では、GraphQL の利点を Go エコシステムにもたらす強力なツールである gqlgen
を使用して、そのようなサーバーを構築するプロセスを説明します。
コアコンセプトの理解
実装に飛び込む前に、サーバー開発を支える基本的な概念を明確に理解しましょう。
GraphQL: GraphQL は、API のためのクエリ言語であり、既存のデータでこれらのクエリを処理するためのランタイムです。 REST とは異なり、通常は複数のエンドポイントにアクセスしてデータを収集しますが、GraphQL ではクライアントは単一のクエリで、階層的に構造化された、必要なデータを正確に要求できます。 これにより、ネットワークリクエストと過剰取得が最小限に抑えられます。
スキーマファースト開発: このパラダイムは、GraphQL スキーマを API の唯一の真実の情報源として定義することに重点を置いています。 まず GraphQL のスキーマ定義言語 (SDL) でスキーマを記述し、API がサポートするすべての型、フィールド、および操作 (クエリ、ミューテーション、サブスクリプション) を指定します。 gqlgen
のようなツールは、このスキーマを使用してサーバーサイドコードの大部分を生成し、実装が定義された契約に厳密に準拠していることを保証します。 このアプローチは、フロントエンドとバックエンド チーム間の明確なコミュニケーションを促進し、API の進化を簡素化します。
型安全性: Go のような強く型付けされた言語では、型安全性とは、コンパイル時に変数と式が明確に定義された型を持つことを保証し、型関連のエラーを防ぎ、コードをより予測可能で保守しやすくすることです。 スキーマファースト開発と組み合わせることで、gqlgen
は GraphQL スキーマの型定義を利用して Go の構造体とインターフェースを生成し、GraphQL の型を Go の型に効果的にマッピングします。 これにより、クライアントのクエリから Go のリゾルバー関数まで、エンドツーエンドの型安全性が提供され、開発サイクルの早い段階で潜在的な問題が捕捉されます。
gqlgen
: これは、GraphQL スキーマから GraphQL サーバーを生成する Go ライブラリです。 スキーマファースト開発に非常に重点を置いており、強力で柔軟なコード生成エンジンを提供することに焦点を当てており、開発者は定型的なコードではなくビジネスロジックの実装に集中できます。
gqlgen を使った GraphQL サーバーの構築
Todo
アイテムのリストを管理するための簡単な GraphQL API を構築してみましょう。
プロジェクトのセットアップ
まず、Go がインストールされていることを確認してください。 次に、新しい Go モジュールを作成します。
mkdir todo-graphql-server cd todo-graphql-server go mod init todo-graphql-server
次に、gqlgen
とその依存関係をインストールします。
go get github.com/99designs/gqlgen go get github.com/99designs/gqlgen/cmd@latest
これで、プロジェクト内で gqlgen
を初期化します。これにより、gqlgen.yml
、graph/schema.resolvers.go
、graph/schema.graphqls
、および graph/generated.go
という必須ファイルが作成されます。
go run github.com/99designs/gqlgen init
GraphQL スキーマの定義
graph/schema.graphqls
を開きます。 ここには GraphQL スキーマ定義が含まれます。 Todo
型と基本的なクエリおよびミューテーションを定義しましょう。
# graph/schema.graphqls type Todo { id: ID! text: String! done: Boolean! user: User! } type User { id: ID! name: String! } type Query { todos: [Todo!]! } type Mutation { createTodo(text: String!, userId: ID!): Todo! markTodoDone(id: ID!): Todo }
スキーマを更新した後、gqlgen generate
を実行して生成された Go コードを更新します。
go run github.com/99designs/gqlgen generate
このコマンドは graph/generated.go
と graph/model/models_gen.go
を更新します。 models_gen.go
には、Todo
と User
型、および定義されていれば入力型を表す Go の構造体が含まれるようになります。 たとえば、次のようになります。
// graph/model/models_gen.go package model type NewTodo struct { Text string `json:"text"` UserID string `json:"userId"` } type Todo struct { ID string `json:"id"` Text string `json:"text"` Done bool `json:"done"` User *User `json:"user"` } type User struct { ID string `json:"id"` Name string `json:"name"` }
gqlgen
が createTodo
ミューテーションの引数に基づいて NewTodo
入力型を自動的に推測していることに注意してください。
リゾルバーの実装
生成された graph/schema.resolvers.go
ファイルには、リゾルバーの基本的なスケルトンが含まれています。 リゾルバーは、スキーマの特定のフィールドのデータを取得する責任がある関数です。
graph/schema.resolvers.go
を変更して、createTodo
、todos
、および markTodoDone
のロジックを実装しましょう。 簡単にするために、インメモリ ストアをデータに使用します。
まず、データストアと、通常は graph/resolver.go
で一意の ID を生成する方法を定義します。
// graph/resolver.go package graph import ( "context" "fmt" "math/rand" "sync" "time" "todo-graphql-server/graph/model" ) // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { mu sync.Mutex todos []*model.Todo users []*model.User } func init() { rand.Seed(time.Now().UnixNano()) } var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randString(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) } func (r *Resolver) GetUserByID(id string) *model.User { for _, user := range r.users { if user.ID == id { return user } } return nil }
次に、graph/schema.resolvers.go
でリゾルバーロジックを記入しましょう。
// graph/schema.resolvers.go package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. // Code generated by github.com/99designs/gqlgen version v0.17.45 import ( "context" "fmt" "todo-graphql-server/graph/model" ) // CreateTodo is the resolver for the createTodo field. func (r *mutationResolver) CreateTodo(ctx context.Context, text string, userID string) (*model.Todo, error) { r.mu.Lock() defer r.mu.Unlock() user := r.GetUserByID(userID) if user == nil { return nil, fmt.Errorf("user with ID %s not found", userID) } newTodo := &model.Todo{ ID: randString(8), // Generate a unique ID Text: text, Done: false, User: user, } r.todos = append(r.todos, newTodo) return newTodo, nil } // MarkTodoDone is the resolver for the markTodoDone field. func (r *mutationResolver) MarkTodoDone(ctx context.Context, id string) (*model.Todo, error) { r.mu.Lock() defer r.mu.Unlock() for _, todo := range r.todos { if todo.ID == id { todo.Done = true return todo, nil } } return nil, fmt.Errorf("todo with ID %s not found", id) } // Todos is the resolver for the todos field. func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { r.mu.Lock() defer r.mu.Unlock() return r.todos, nil } // User is the resolver for the user field. func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) { // The user is already embedded in the Todo model, so we just return it. // In a real application, you might fetch the user from a database here if it's not eager-loaded. return obj.User, nil } // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // Todo returns TodoResolver implementation. func (r *Resolver) Todo() TodoResolver { return &todoResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type todoResolver struct{ *Resolver }
生成された Todo
型の User
フィールドのリゾルバー Todo()
に注目してください。これは、フィールドリゾルバーであり、型の特定フィールドの解決方法をカスタマイズできます。 Todo
には既に User
オブジェクトが含まれているため、それを返すだけです。 Todo
構造体に ID
としてのみユーザーが格納されていた場合、ここでその ID に基づいてデータストアから User
オブジェクトを取得します。 この柔軟性は GraphQL の重要な強みです。
サーバーのセットアップ
最後に、GraphQL API を公開する HTTP サーバーをセットアップする必要があります。 プロジェクト のルートに server.go
ファイルを作成します。
// server.go package main import ( "log" "net/http" "os" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "todo-graphql-server/graph" "todo-graphql-server/graph/model" ) const defaultPort = "8080" func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } // ダミーデータでリゾルバーを初期化します resolver := &graph.Resolver{ odos: []*model.Todo{}, users: []*model.User{ {ID: "U1", Name: "Alice"}, {ID: "U2", Name: "Bob"}, }, } srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: 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)) }
server.go
では、graph.Resolver
を初期化し、gqlgen
の実行可能スキーマに注入します。 次に、GraphQL Playground (API テスト用の便利な GUI) と実際の GraphQL エンドポイントの 2 つの HTTP ハンドラーを設定します。
サーバーの実行とテスト
サーバーを実行します。
go run server.go
ブラウザで http://localhost:8080/
を開きます。GraphQL Playground が表示されます。
いくつかの操作を実行してみましょう。
Todo の作成:
mutation CreateTodo { createTodo(text: "Learn gqlgen", userId: "U1") { id text done user { name } } }
すべての Todo の取得:
query GetTodos { todos { id text done user { id name } } }
Todo を完了としてマーク ( createTodo
ミューテーションからの ID を TODO_ID
に置き換えてください):
mutation MarkTodoDone { markTodoDone(id: "TODO_ID") { id text done user { name } } }
応答が強く型付けされており、クエリで要求された構造と一致していることがわかります。 gqlgen
は、Go のリゾルバー関数が GraphQL スキーマに正確に準拠した引数を受け取り、値を返すように保証し、開発プロセス全体で優れた型安全性を提供します。
アプリケーションシナリオ
この型安全でスキーマファーストな gqlgen
アプローチは、以下に最適です。
- 大規模で共同作業を行うチーム: スキーマは明確な契約として機能し、フロントエンドとバックエンドのチームが並行して作業し、誤解を減らすのに役立ちます。
- 複雑な API: API サーフェスが拡大するにつれて、生成されたコードと型安全性は、複雑さを管理し、微妙なエラーを防ぐのに役立ちます。
- マイクロサービスアーキテクチャ: GraphQL は API ゲートウェイとして機能し、さまざまなマイクロサービスからデータを集約できます。
gqlgen
は、このゲートウェイの統一スキーマを定義することを容易にします。 - 公開 API: よく定義された型安全なスキーマは、クライアント ライブラリの生成を簡素化し、API コンシューマーの開発者エクスペリエンスを向上させます。
結論
Go で gqlgen
を使用して型安全なスキーマファースト GraphQL サーバーを構築することは、開発効率、堅牢な型チェック、および保守可能なコードの強力な組み合わせを提供します。 definitive な契約として GraphQL スキーマから始めることで、gqlgen
は定型的なコードを排除し、Go リゾルバーが API の仕様と正確に一致することを保証し、バグを減らし、開発エクスペリエンスを向上させます。 このアプローチは、進化する API のための強固な基盤を提供し、柔軟なデータ取得と組み込みの Go 型安全性とのギャップを埋めます。