MCP HubMCP Hub
SKILL·972A16

go-graphql

eduardo-sl
更新日 11 days ago
4 閲覧
69
9
69
GitHubで表示
メタpowerpointtestingapidesigndata

について

このスキルは、開発者がgqlgenを使用したスキーマファースト生成でGoのGraphQLサーバーを構築するのを支援します。リゾルバーの実装、データローダーによるN+1問題の解決、複雑性制限と認証の追加について解説しています。GoでのGraphQL API実装やクエリパフォーマンスの最適化を行う際にご利用ください。

クイックインストール

Claude Code

推奨
メイン
npx skills add eduardo-sl/go-agent-skills -a claude-code
プラグインコマンド代替
/plugin add https://github.com/eduardo-sl/go-agent-skills
Git クローン代替
git clone https://github.com/eduardo-sl/go-agent-skills.git ~/.claude/skills/go-graphql

このコマンドをClaude Codeにコピー&ペーストしてスキルをインストールします

ドキュメント

Go GraphQL

GraphQL moves query planning to the client. That is the feature and the danger: one innocuous query can become ten thousand database round trips, and one over-permissive field can leak another tenant's data. Both are solved at the server, not in the schema review.

1. Schema-First with gqlgen

The .graphql schema is the source of truth. gqlgen generates models, resolver stubs, and the execution layer from it.

go get -tool github.com/99designs/gqlgen
go tool gqlgen init      # once
go tool gqlgen generate  # after every schema change
# gqlgen.yml — bind generated types to your own models
models:
  User:
    model: github.com/myorg/app/internal/domain.User
  ID:
    model:
      - github.com/99designs/gqlgen/graphql.ID
      - github.com/99designs/gqlgen/graphql.Int64

Bind domain types explicitly. Left to itself gqlgen generates a parallel set of anaemic structs, and every resolver becomes a mapping function.

Commit generated code, and fail CI when it is stale:

go tool gqlgen generate && git diff --exit-code

Never edit generated.go or models_gen.go. resolver.go and the *.resolvers.go files are yours.

2. Resolvers Stay Thin

A resolver translates a GraphQL request into a service call. It contains no business logic and no SQL.

func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
    u, err := r.users.Find(ctx, id)
    if errors.Is(err, domain.ErrNotFound) {
        return nil, nil // nullable field: absent, not an error
    }
    if err != nil {
        return nil, fmt.Errorf("find user %s: %w", id, err)
    }
    return u, nil
}

Inject dependencies through the Resolver struct, never through package globals:

type Resolver struct {
    users  UserService
    orders OrderService
    loader *Loaders
}

Always propagate ctx. It carries the request deadline, the authenticated principal, and the per-request dataloaders.

3. The N+1 Problem — the one that matters

A field resolver on a list type runs once per element.

// ❌ 1 query for the orders, then N queries for the users
func (r *orderResolver) Customer(ctx context.Context, obj *domain.Order) (*domain.User, error) {
    return r.users.Find(ctx, obj.CustomerID)
}

Batch with a dataloader. It collects the keys requested within a short window and issues one query.

import "github.com/vikstrous/dataloadgen"

type Loaders struct {
    UserByID *dataloadgen.Loader[string, *domain.User]
}

func NewLoaders(s UserService) *Loaders {
    return &Loaders{
        UserByID: dataloadgen.NewLoader(func(ctx context.Context, ids []string) ([]*domain.User, []error) {
            return s.FindMany(ctx, ids) // ONE query for all ids
        }, dataloadgen.WithWait(time.Millisecond)),
    }
}

// ✅ 1 query for the orders, 1 for all customers
func (r *orderResolver) Customer(ctx context.Context, obj *domain.Order) (*domain.User, error) {
    return loadersFrom(ctx).UserByID.Load(ctx, obj.CustomerID)
}

Loaders are per request, installed by middleware. A process-wide loader caches across users and leaks data between them.

func withLoaders(svc UserService, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), loadersKey{}, NewLoaders(svc))
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

The batch function must return results in the order of the keys it was given, with a nil entry and an error per missing key. Returning a shorter slice silently misaligns every result.

4. Bound Every Query

A public GraphQL endpoint without limits is a denial-of-service endpoint.

srv := handler.New(generated.NewExecutableSchema(cfg))
srv.AddTransport(transport.POST{})
srv.SetQueryCache(lru.New[*ast.QueryDocument](1000))
srv.Use(extension.FixedComplexityLimit(300))
srv.Use(extension.AutomaticPersistedQuery{Cache: lru.New[string](100)})
  • Complexity limit — assign a cost per field, higher for list fields with a large first. Start at a number your slowest legitimate query fits under, then measure.
  • Depth — recursive types (user { orders { customer { orders ... } } }) must be bounded. gqlgen has no built-in depth limit; enforce it in an operation middleware.
  • Pagination is mandatory on every list field. A field returning an unbounded list is a schema bug.
  • Introspection is only enabled if you install extension.Introspection. Do not install it in production, or gate it behind an authenticated role.
  • Persisted queries let a public client send a hash instead of a document, so the server executes only queries you shipped.

Set srv.AroundOperations to enforce a per-operation timeout, and always run behind an http.Server with ReadTimeout and WriteTimeout set.

5. Errors

GraphQL returns 200 with an errors array. Never leak internals into it.

srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error {
    err := graphql.DefaultErrorPresenter(ctx, e)

    var domainErr *domain.ValidationError
    if errors.As(e, &domainErr) {
        err.Message = domainErr.Message
        err.Extensions = map[string]any{"code": "VALIDATION_FAILED"}
        return err
    }

    slog.ErrorContext(ctx, "graphql resolver failed", "error", e)
    err.Message = "internal server error" // stable, safe
    err.Extensions = map[string]any{"code": "INTERNAL"}
    return err
})

Use srv.SetRecoverFunc to convert a resolver panic into an error instead of killing the connection, and log it with the stack.

Remember the nullability rule: an error on a non-null field nulls out its nearest nullable ancestor. Make a field non-null only when it can never legitimately be absent.

6. Authorization Belongs on the Field

Object-level checks are not enough — a client can reach an object through several paths.

directive @hasRole(role: Role!) on FIELD_DEFINITION

type User {
  id: ID!
  email: String! @hasRole(role: ADMIN)
}
cfg.Directives.HasRole = func(ctx context.Context, obj any, next graphql.Resolver, role model.Role) (any, error) {
    if !auth.FromContext(ctx).HasRole(role) {
        return nil, gqlerror.Errorf("access denied")
    }
    return next(ctx)
}

Authenticate in HTTP middleware, before the GraphQL handler. Authorize in the directive or the resolver, using the principal from the context — never from a query argument.

7. Testing

func TestUserQuery(t *testing.T) {
    c := client.New(handler.NewDefaultServer(generated.NewExecutableSchema(cfg)))

    var resp struct {
        User struct{ ID, Email string }
    }
    c.MustPost(`{ user(id: "u-1") { id email } }`, &resp)

    require.Equal(t, "u-1", resp.User.ID)
}

Assert the query count for any resolver with a dataloader — that is the only way an N+1 regression fails a build rather than a dashboard:

require.Equal(t, 2, db.QueryCount(), "expected batched loads, got N+1")

Verification Checklist

  1. Schema is the source of truth; generated files are committed and CI-checked
  2. Generated types bind to domain models via gqlgen.yml
  3. Resolvers contain no business logic and always propagate ctx
  4. Every list-field resolver that fetches by ID goes through a dataloader
  5. Dataloaders are constructed per request, never shared across requests
  6. Batch functions return one result per key, in key order
  7. A complexity limit and a depth bound are configured and tested
  8. Every list field is paginated
  9. Introspection is disabled or role-gated in production
  10. An error presenter strips internal errors; a recover func is installed
  11. Authorization is enforced per field, from the context principal
  12. A test asserts the query count for at least one batched field

GitHub リポジトリ

eduardo-sl/go-agent-skills
パス: skills/(architecture)/go-graphql
0
FAQ

よくある質問

go-graphql Skillとは何ですか?

go-graphql はeduardo-sl が作成した Claude Skillです。Skillは、Claudeが必要に応じて読み込む指示とリソースをまとめ、追加の指示なしで go-graphql に関連するタスクを実行できるようにします。

go-graphql をインストールするには?

このページのインストールコマンドを使用してください。go-graphql をプラグインとして Claude Code に追加するか、リポジトリを skills ディレクトリにクローンし、Claudeを再起動してSkillを読み込みます。

go-graphql はどのカテゴリに属しますか?

go-graphql は メタ カテゴリに属します。

go-graphql は無料で利用できますか?

はい。go-graphql は AIMCP に掲載されており、無料でインストールできます。

関連スキル

content-collections
メタ

このスキルは、Content Collections(Markdown/MDXファイルを型安全なデータコレクションに変換するTypeScriptファーストのツール)の本番環境でテストされた設定を提供します。Zodバリデーションによる型安全性を実現し、ブログ、ドキュメントサイト、コンテンツ重視のVite + Reactアプリケーション構築時にご利用ください。Viteプラグインの設定、MDXコンパイルから、デプロイ最適化、スキーマバリデーションまで、すべてを網羅しています。

スキルを見る
polymarket
メタ

このスキルは、開発者がPolymarket予測市場プラットフォームを活用したアプリケーション構築を可能にします。API統合による取引や市場データの取得に加え、WebSocketを介したリアルタイムデータストリーミングにより、ライブ取引や市場活動を監視できます。取引戦略の実装や、ライブ市場更新を処理するツールの作成にご利用ください。

スキルを見る
creating-opencode-plugins
メタ

このスキルは、開発者がコマンド、ファイル、LSP操作など25種類以上のイベントタイプにフックするOpenCodeプラグインを作成することを支援します。JavaScript/TypeScriptモジュール向けに、プラグイン構造、イベントAPI仕様、および実装パターンを提供します。カスタムイベント駆動ロジックでOpenCode AIアシスタントのライフサイクルをインターセプト、監視、または拡張する必要がある場合にご利用ください。

スキルを見る
sglang
メタ

SGLangは、高性能なLLMサービングフレームワークであり、RadixAttentionプレフィックスキャッシュを活用したJSON、正規表現、エージェントワークフロー向けの高速で構造化された生成を特長とします。特にプレフィックスが繰り返されるタスクにおいて、大幅に高速な推論を実現し、複雑な構造化出力やマルチターン対話に最適です。制約付きデコードが必要な場合や、広範なプレフィックス共有を伴うアプリケーションを構築する場合は、vLLMなどの代替案ではなくSGLangを選択してください。

スキルを見る