{"id":"609767d3-07f3-4dff-890c-dec50d3941ff","entityType":"agent","slug":"clawhub-samber-golang-graphql","name":"golang-graphql","canonicalUrl":"https://www.xpersona.co/agent/clawhub-samber-golang-graphql","canonicalPath":"/agent/clawhub-samber-golang-graphql","generatedAt":"2026-10-11T17:44:02.514Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T15:03:48.149Z","emptyReason":null},"description":"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`. Skill: golang-graphql Owner: samber Summary: Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports github.com/99designs/gqlgen or github.com/graph-gophers/graphql-go. Tags: latest:0.2.0 Version history: v0.2.0 | 2026-08-2","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s173arkhs3131fq5jf769qq75583hdgt:golang-graphql","sourceUrl":"https://clawhub.ai/samber/golang-graphql","homepage":"https://clawhub.ai/samber/skills/golang-graphql","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/samber/golang-graphql","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/samber/skills/golang-graphql","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions,"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T15:03:48.149Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T15:03:48.149Z","emptyReason":null},"stars":null,"forks":null,"downloads":1044,"packageName":null,"latestVersion":"0.2.0","tractionLabel":"1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T15:03:48.075Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T15:03:48.149Z","lastCrawledAt":"2026-10-11T15:03:48.075Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T15:03:48.075Z","lastVerifiedAt":null,"highlights":[{"version":"0.2.0","createdAt":"2026-08-21T13:21:01.360Z","changelog":"golang-graphql v0.2.0 - Added support for additional agent tools: Bash(godig:*), Bash(gopls:*), LSP, mcp__gopls__*. - Declared path restrictions: skill now applies only to Go source files (`**/*.go`). - Updated compatibility statement to mention Codex and harnesses, not just Claude Code. - Integrated references to `godig` and `gopls` skills for better Go package discovery and navigation. - Removed the obsolete `skill-card.md` file.","fileCount":7,"zipByteSize":19272},{"version":"0.0.3","createdAt":"2026-05-22T16:58:08.887Z","changelog":"golang-graphql v0.0.3 - Bump version to 0.0.3 in SKILL.md. - Update references for gqlgen and graphql-go documentation. - Refresh evaluation data in evals/evals.json. - No major changes to best practices or skill guidance.","fileCount":7,"zipByteSize":19224},{"version":"0.0.2","createdAt":"2026-05-02T16:58:45.549Z","changelog":"- Expanded documentation on best practices for schema design, resolver patterns, N+1 prevention, authentication, error handling, and subscriptions for GraphQL in Golang. - Added comparison table of major Go GraphQL libraries (`gqlgen`, `graphql-go`, and `graphql-go/graphql`). - Provided guidance on DataLoader usage, including safe per-request instantiation. - Clarified nullability rules and mutation patterns in schema definitions. - Outlined recommended project/library choices and integration strategies with examples. - Updated compatibility, persona, and usage instructions for coding agent environments.","fileCount":6,"zipByteSize":17785}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s173arkhs3131fq5jf769qq75583hdgt:golang-graphql","setupComplexity":"low","setupSteps":["Setup complexity is LOW. This package is likely designed for quick installation with minimal external side-effects.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T17:44:02.508Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-samber-golang-graphql/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T15:03:48.149Z","emptyReason":null},"readme":"Skill: golang-graphql\n\nOwner: samber\n\nSummary: Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`.\n\nTags: latest:0.2.0\n\nVersion history:\n\nv0.2.0 | 2026-08-21T13:21:01.360Z | auto\n\ngolang-graphql v0.2.0\n\n- Added support for additional agent tools: Bash(godig:*), Bash(gopls:*), LSP, mcp__gopls__*.\n- Declared path restrictions: skill now applies only to Go source files (`**/*.go`).\n- Updated compatibility statement to mention Codex and harnesses, not just Claude Code.\n- Integrated references to `godig` and `gopls` skills for better Go package discovery and navigation.\n- Removed the obsolete `skill-card.md` file.\n\nv0.0.3 | 2026-05-22T16:58:08.887Z | auto\n\ngolang-graphql v0.0.3\n\n- Bump version to 0.0.3 in SKILL.md.\n- Update references for gqlgen and graphql-go documentation.\n- Refresh evaluation data in evals/evals.json.\n- No major changes to best practices or skill guidance.\n\nv0.0.2 | 2026-05-02T16:58:45.549Z | auto\n\n- Expanded documentation on best practices for schema design, resolver patterns, N+1 prevention, authentication, error handling, and subscriptions for GraphQL in Golang.\n- Added comparison table of major Go GraphQL libraries (`gqlgen`, `graphql-go`, and `graphql-go/graphql`).\n- Provided guidance on DataLoader usage, including safe per-request instantiation.\n- Clarified nullability rules and mutation patterns in schema definitions.\n- Outlined recommended project/library choices and integration strategies with examples.\n- Updated compatibility, persona, and usage instructions for coding agent environments.\n\nArchive index:\n\nArchive v0.2.0: 7 files, 19272 bytes\n\nFiles: evals/evals.json (11727b), references/gqlgen.md (7477b), references/graphql-go.md (7218b), references/testing.md (5023b), skill-card.md (2318b), SKILL.md (13086b), _meta.json (133b)\n\nFile v0.2.0:SKILL.md\n\n---\nname: golang-graphql\ndescription: \"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`.\"\nuser-invocable: false\nlicense: MIT\ncompatibility: Designed for Claude Code, Codex or similar harness, and for projects using Golang.\nmetadata:\n  author: samber\n  version: \"0.2.0\"\n  openclaw:\n    emoji: \"🔮\"\n    homepage: https://github.com/samber/cc-skills-golang\n    requires:\n      bins:\n        - go\n    install: []\n    skill-library-version: \"0.17.89\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs Bash(curl:*) Bash(godig:*) Bash(gopls:*) LSP mcp__gopls__*\npaths:\n  - \"**/*.go\"\n---\n\n**Persona:** You are a Go GraphQL engineer. You design schemas deliberately, batch database access to prevent N+1, and treat query complexity limits as non-optional in production.\n\n**Modes:**\n\n- **Build mode** — generating new schemas, resolvers, or server setup: follow the skill's sequential instructions; launch a background agent to grep for existing resolver patterns and naming conventions before generating new code.\n- **Review mode** — auditing a GraphQL codebase or PR: use a sub-agent to scan for N+1 resolver patterns, missing complexity caps, global DataLoaders, and introspection enabled in production, in parallel with reading the business logic.\n\n> **Community default.** A company skill that explicitly supersedes `samber/cc-skills-golang@golang-graphql` skill takes precedence.\n\n# Go GraphQL Best Practices\n\nBoth major libraries are schema-first: write SDL (`.graphql` files), bind Go resolvers. Choose based on project size and team preferences.\n\nThis skill is not exhaustive. Refer to each library's official documentation and code examples for current API signatures. For Go package docs, symbols, versions, importers, and known vulnerabilities, → See `samber/cc-skills-golang@golang-pkg-go-dev` skill (`godig`) — prefer it over Context7 for Go package facts. To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See `samber/cc-skills-golang@golang-gopls` skill (`gopls`). Context7 remains a fallback for docs not indexed on pkg.go.dev.\n\n## Library Choice\n\n| Library | Approach | Type safety | Build step | Best for |\n| --- | --- | --- | --- | --- |\n| `github.com/99designs/gqlgen` | Codegen | Compile-time | `go generate` | Large schemas, federation, strict types |\n| `github.com/graph-gophers/graphql-go` | Reflection | Parse-time | None | Simple schemas, fast iteration |\n| `github.com/graphql-go/graphql` | Code-first | Runtime | None | **Avoid** — verbose, no SDL |\n\nPick **gqlgen** when: Apollo Federation is required, schema is large (100+ types), or the team wants generated stubs and zero reflection overhead.\n\nPick **graph-gophers** when: schema is small/medium, the build pipeline should stay simple, or a dynamic schema is needed.\n\nFor deep-dive on each library, see [gqlgen reference](./references/gqlgen.md) and [graphql-go reference](./references/graphql-go.md).\n\n## Schema Design\n\n```graphql\n# ✓ Good — explicit nullability; ID scalar for opaque identifiers\ntype User {\n  id: ID!\n  email: String! # non-null: the server can always return this\n  bio: String # nullable: may be unset\n  posts(first: Int = 10, after: String): PostConnection!\n}\n\n# ✗ Bad — Int ID leaks implementation details, breaks client caching\ntype Post {\n  id: Int!\n}\n```\n\n**Nullability rule:** mark a field `!` only when the server can _always_ return a value. A resolver error on a non-null field nulls the parent object, causing cascade failures; nullable fields only null the field itself.\n\n**Pagination:** use Relay cursor connections (`Connection`/`Edge`/`PageInfo`) for list fields. Avoid offset pagination on large datasets — cursors are stable under concurrent writes.\n\n**Mutations:** wrap results in an envelope type so clients receive business errors alongside partial results without polluting the GraphQL `errors` array:\n\n```graphql\ntype CreateUserPayload {\n  user: User\n  errors: [UserError!]!\n}\n```\n\n## Resolver Patterns\n\nKeep resolvers thin — they translate GraphQL inputs to domain calls and domain responses to GraphQL outputs.\n\n```go\n// ✓ Good — resolver delegates to service layer\nfunc (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error) {\n    user, err := r.userService.Create(ctx, input.Email, input.Name)\n    if err != nil {\n        return nil, formatError(err)\n    }\n    return &model.CreateUserPayload{User: toGQLUser(user)}, nil\n}\n\n// ✗ Bad — SQL in resolver, no separation of concerns\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {\n    row := r.db.QueryRowContext(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    // ...\n}\n```\n\nUse per-type resolver structs (`userResolver`, `postResolver`) rather than one monolithic resolver for all fields.\n\n## N+1 Prevention (DataLoaders)\n\nEach `User.posts` resolver fires a SQL query per user without batching — O(n) DB calls for n users. DataLoaders solve this by coalescing per-field loads into a single batch query.\n\n**Critical rule: DataLoaders MUST be created per-request in HTTP middleware, never globally.** A global DataLoader caches across requests — stale data, potential cross-user data leakage.\n\n```go\n// ✓ Good — per-request DataLoader in middleware\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: newPostsByUserIDLoader(r.Context(), db),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// ✗ Bad — global DataLoader shared across all requests\nvar globalLoader = newPostsByUserIDLoader(context.Background(), db)\n```\n\nIn gqlgen, mark batched fields with `resolver: true` in `gqlgen.yml` to force a dedicated resolver method. See [gqlgen reference](./references/gqlgen.md) for full DataLoader wiring.\n\n## Authentication and Authorization\n\nTwo-layer model:\n\n1. **HTTP middleware** — extract and validate tokens, stash identity in `context.Context`.\n2. **Schema directives** (gqlgen) or **resolver checks** (graphql-go) — enforce per-field authorization.\n\n```go\n// HTTP middleware layer (both libraries)\nfunc AuthMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        token := r.Header.Get(\"Authorization\")\n        user, err := validateToken(token)\n        if err != nil {\n            http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n            return\n        }\n        ctx := context.WithValue(r.Context(), userKey, user)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n```\n\nIn gqlgen, use `@hasRole` schema directives for field-level authorization — authorization policy lives in the schema, not scattered across resolvers. See [gqlgen reference](./references/gqlgen.md).\n\n## Error Handling\n\nNever return raw internal errors — they leak SQL messages, stack traces, or service internals to clients.\n\n```go\n// gqlgen — custom ErrorPresenter strips internal details\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr // already formatted\n    }\n    // log internal err here\n    return gqlerror.Errorf(\"internal error\") // safe client message\n})\n\n// Add extension codes for client-side error handling\nreturn nil, &gqlerror.Error{\n    Message: \"user not found\",\n    Extensions: map[string]any{\"code\": \"NOT_FOUND\"},\n}\n```\n\nFor graph-gophers, implement the `ResolverError` interface to attach `Extensions()`. See [graphql-go reference](./references/graphql-go.md).\n\nUse `graphql.AddError(ctx, err)` in gqlgen for non-fatal field errors where the resolver can still return partial data.\n\nFor error wrapping patterns, see the `samber/cc-skills-golang@golang-error-handling` skill.\n\n## Subscriptions\n\nSubscriptions use long-lived WebSocket connections. The critical discipline: **always respect context cancellation** — a leaked goroutine per disconnected client exhausts resources silently.\n\n```go\n// ✓ Good — closes channel when client disconnects\nfunc (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {\n    ch := make(chan *model.Message, 1)\n    sub := r.pubsub.Subscribe(room) // subscribe once before the goroutine\n    go func() {\n        defer close(ch) // always close; signals iteration to stop\n        for {\n            select {\n            case <-ctx.Done():\n                return // client disconnected\n            case msg := <-sub:\n                select {\n                case ch <- msg:\n                case <-ctx.Done():\n                    return\n                }\n            }\n        }\n    }()\n    return ch, nil\n}\n\n// ✗ Bad — goroutine leaks forever when client disconnects\nfunc (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {\n    ch := make(chan *model.Message, 1)\n    go func() {\n        for msg := range r.pubsub.Subscribe(room) {\n            ch <- msg // blocks forever after client gone\n        }\n    }()\n    return ch, nil\n}\n```\n\n## Performance and Safety\n\nProduction GraphQL servers require explicit limits. Without them, a single deeply nested query exhausts CPU and memory.\n\n```go\n// gqlgen — wire these into every production handler\nsrv := handler.NewDefaultServer(es)\nsrv.Use(extension.FixedComplexityLimit(200)) // max cost per query\n\n// Gate introspection — only in non-production environments\nif os.Getenv(\"ENV\") != \"production\" {\n    srv.Use(extension.Introspection{})\n}\n```\n\nFor graph-gophers: `graphql.MaxDepth(10)` and `graphql.MaxParallelism(10)` options at `ParseSchema` time.\n\n**Query allow-listing:** in production, consider persisted queries (gqlgen APQ extension) to reject arbitrary query strings.\n\n## Common Mistakes\n\n| Mistake | Why it matters | Fix |\n| --- | --- | --- |\n| N+1 queries in child resolvers | One SQL per parent row → O(n) DB calls | Use per-request DataLoader |\n| Global DataLoader | Cross-request cache — stale data, data leaks | Create DataLoader in request middleware |\n| Editing `models_gen.go` directly | Next `go generate` wipes hand edits | Use `autobind` or `models.<T>.model` in `gqlgen.yml` |\n| Forgetting `go generate` after schema change | Resolver interface mismatch at compile time | Re-run `go tool gqlgen generate` |\n| `int` field in graph-gophers resolver | Library requires `int32` for `Int` scalar | Use `int32` (or `float64` for `Float`) |\n| Introspection enabled in production | Exposes full schema to attackers | Gate with `ENV` check |\n| No complexity cap | Deeply nested query → CPU/memory DoS | `extension.FixedComplexityLimit(N)` |\n| Leaking DB errors from resolvers | Exposes SQL internals to clients | Wrap in `ErrorPresenter` / `ResolverError` |\n| Subscription goroutine leak | Client disconnect → goroutine runs forever | `defer close(ch)` + `select ctx.Done()` |\n| Nullable field for always-required data | Clients must null-check everywhere | Mark `!` in schema; return error from resolver |\n\n## Deep Dives\n\n- **[gqlgen reference](./references/gqlgen.md)** — codegen workflow, `gqlgen.yml`, DataLoaders, Federation v2, directives\n- **[graphql-go reference](./references/graphql-go.md)** — reflection resolver model, type mapping, tracing\n- **[Testing](./references/testing.md)** — gqlgen client harness, gqltesting, httptest patterns\n\n## Cross-References\n\n- → See `samber/cc-skills-golang@golang-context` skill for context propagation in resolvers and subscriptions\n- → See `samber/cc-skills-golang@golang-error-handling` skill for error wrapping and sentinel patterns\n- → See `samber/cc-skills-golang@golang-testing` skill for table-driven and integration test patterns\n- → See `samber/cc-skills-golang@golang-observability` skill for tracing and metrics in resolvers\n- → See `samber/cc-skills-golang@golang-security` skill for input validation and injection prevention\n- → See `samber/cc-skills-golang@golang-database` skill for N+1 query patterns and DataLoader database batching\n\n## References\n\n- [gqlgen](https://github.com/99designs/gqlgen)\n- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)\n- [Relay cursor connections spec](https://relay.dev/graphql/connections.htm)\n\nIf you encounter a bug or unexpected behavior in gqlgen, open an issue at <https://github.com/99designs/gqlgen/issues>.\n\nIf you encounter a bug or unexpected behavior in graph-gophers/graphql-go, open an issue at <https://github.com/graph-gophers/graphql-go/issues>.\n\nFile v0.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn72rhnkwjfeex9wr1n7y24qa983cjn3\",\n  \"slug\": \"golang-graphql\",\n  \"version\": \"0.2.0\",\n  \"publishedAt\": 1787318461360\n}\n\nFile v0.2.0:references/gqlgen.md\n\n# gqlgen Reference\n\ngqlgen is a schema-first, code-generation library. Write SDL, run `go generate`, fill in resolver bodies.\n\n## Project Setup\n\n```bash\n# Bootstrap a new project\ngo run github.com/99designs/gqlgen init\n\n# Pin the tool in go.mod for reproducible generation (Go 1.24+)\ngo get -tool github.com/99designs/gqlgen@latest\n```\n\nFor Go <1.24 modules, use the legacy `tools.go` blank-import workaround instead.\n\n```bash\n# Regenerate after every schema change\ngo tool gqlgen generate\n```\n\nNever hand-edit generated files (`generated.go`, `models_gen.go`) — `generate` overwrites them.\n\n## gqlgen.yml\n\n```yaml\nschema:\n  - graph/schema/*.graphql\n\nexec:\n  filename: graph/generated.go\n  package: graph\n\nmodel:\n  filename: graph/model/models_gen.go\n  package: model\n\nresolver:\n  layout: follow-schema # one resolvers file per schema file\n  dir: graph\n  package: graph\n  filename_template: \"{name}.resolvers.go\"\n\nautobind:\n  - github.com/me/app/internal/domain # reuse existing structs\n\nmodels:\n  # ID: graphql.IntID  # legacy only — use opaque string IDs for new schemas\n  User:\n    model: github.com/me/app/internal/domain.User\n    fields:\n      posts:\n        resolver: true # force a custom resolver (required for DataLoader fields)\n\nomit_slice_element_pointers: true\nstruct_fields_always_pointers: false\nresolvers_always_return_pointers: true\n```\n\nKey knobs:\n\n- `autobind` — maps Go structs to GraphQL types; fields must match by name (case-insensitive)\n- `models.<T>.model` — override which Go type backs a GraphQL type\n- `fields.<f>.resolver: true` — force a custom resolver instead of struct field access; required for any field that should batch via DataLoader\n- `struct_fields_always_pointers` / `resolvers_always_return_pointers` — controls `*T` vs `T` in generated signatures; match your domain model conventions\n\n## Resolver Structure\n\nThe generated `Config` holds a `Resolvers` field of the generated interface. You implement it:\n\n```go\n// graph/resolver.go — you own this file, not generated\ntype Resolver struct {\n    db          *sql.DB\n    userService *service.UserService\n    loaders     *dataloaders.Loaders // injected per-request\n}\n```\n\nPer-type resolvers implement the generated interface split by GraphQL type:\n\n```go\ntype queryResolver struct{ *Resolver }\ntype mutationResolver struct{ *Resolver }\ntype userResolver struct{ *Resolver }\n\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { ... }\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) { ... }\n```\n\n`obj` is the parent object — the entry point for walking the graph.\n\n## DataLoaders (gqlgen)\n\nUse `github.com/vikstrous/dataloadgen` (generics, fast) or `github.com/graph-gophers/dataloader`:\n\n```go\n// Inject per-request via middleware\nfunc Middleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: dataloadgen.NewLoader(func(ctx context.Context, ids []string) ([][]*domain.Post, []error) {\n                return batchPostsByUserID(ctx, db, ids) // returns one []Post per user ID\n            }, dataloadgen.WithWait(1*time.Millisecond)),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// Resolver uses the loader — never the DB directly\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) {\n    return loaders.For(ctx).PostsByUserID.Load(ctx, obj.ID)\n}\n```\n\nSet `wait` to 1–2ms — allows multiple concurrent resolvers to register keys before the batch fires.\n\n## Authentication Directives\n\n```graphql\ndirective @hasRole(role: Role!) on FIELD_DEFINITION\n\ntype Query {\n  adminStats: Stats! @hasRole(role: ADMIN)\n}\n```\n\n```go\n// Implement the directive function\nfunc HasRole(ctx context.Context, obj any, next graphql.Resolver, role model.Role) (any, error) {\n    user := auth.UserFromContext(ctx)\n    if user == nil || user.Role != role {\n        return nil, &gqlerror.Error{\n            Message:    \"access denied\",\n            Extensions: map[string]any{\"code\": \"FORBIDDEN\"},\n        }\n    }\n    return next(ctx)\n}\n\n// Register at server bootstrap\nc := generated.Config{\n    Resolvers: &graph.Resolver{...},\n    Directives: generated.DirectiveRoot{\n        HasRole: HasRole,\n    },\n}\n```\n\n## Middleware Hooks\n\n```go\nsrv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {\n    // log operation name, add trace span\n    return next(ctx)\n})\nsrv.AroundFields(func(ctx context.Context, next graphql.Resolver) (any, error) {\n    // per-field tracing, timing\n    return next(ctx)\n})\n```\n\n## Error Presenter\n\n```go\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr\n    }\n    log.Ctx(ctx).Error(\"resolver error\", \"err\", err)\n    return gqlerror.Errorf(\"internal server error\")\n})\n\nsrv.SetRecoverFunc(func(ctx context.Context, err any) error {\n    log.Ctx(ctx).Error(\"panic in resolver\", \"err\", err)\n    return fmt.Errorf(\"internal server error\")\n})\n```\n\n## Subscriptions\n\n```go\nsrv.AddTransport(transport.Websocket{\n    KeepAlivePingInterval: 10 * time.Second,\n    Upgrader: websocket.Upgrader{\n        // Restrict to your own origin in production; true here is dev-only.\n        CheckOrigin: func(r *http.Request) bool {\n            return r.Header.Get(\"Origin\") == \"https://app.example.com\"\n        },\n    },\n    InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {\n        // auth at connection time\n        token := initPayload.Authorization()\n        user, err := validateToken(token)\n        if err != nil {\n            return ctx, nil, err\n        }\n        return context.WithValue(ctx, userKey, user), &initPayload, nil\n    },\n})\n```\n\ngqlgen supports both `graphql-ws` (legacy) and `graphql-transport-ws` (current) subprotocols.\n\n## File Uploads\n\n```go\nsrv.AddTransport(transport.MultipartForm{\n    MaxUploadSize: 10 << 20, // 10 MB total\n    MaxMemory:     5 << 20,  // 5 MB in memory; rest spills to disk\n})\n```\n\nSchema:\n\n```graphql\nscalar Upload\n\ntype Mutation {\n  uploadAvatar(file: Upload!): User!\n}\n```\n\nResolver receives `graphql.Upload{File io.Reader, Filename string, Size int64, ContentType string}`.\n\n## Apollo Federation v2\n\n`gqlgen.yml`:\n\n```yaml\nfederation:\n  filename: graph/federation.go\n  version: 2\n```\n\nSchema:\n\n```graphql\nextend schema\n  @link(\n    url: \"https://specs.apollo.dev/federation/v2.3\"\n    import: [\"@key\", \"@shareable\", \"@external\"]\n  )\n\ntype User @key(fields: \"id\") {\n  id: ID!\n  name: String!\n}\n```\n\nImplement `FindUserByID` in the generated entity resolver. Works with Apollo Router and Cosmo.\n\n## Production Handler Setup\n\n```go\nsrv := handler.New(es)\nsrv.AddTransport(transport.Options{})\nsrv.AddTransport(transport.GET{})\nsrv.AddTransport(transport.POST{})\nsrv.AddTransport(transport.MultipartForm{MaxUploadSize: 10 << 20, MaxMemory: 5 << 20})\nsrv.AddTransport(transport.Websocket{KeepAlivePingInterval: 10 * time.Second})\n\nsrv.SetQueryCache(lru.New[*ast.QueryDocument](1000))\nif os.Getenv(\"ENV\") != \"production\" {\n    srv.Use(extension.Introspection{})\n}\nsrv.Use(extension.AutomaticPersistedQuery{Cache: lru.New[string](100)})\nsrv.Use(extension.FixedComplexityLimit(200))\n```\n\nFile v0.2.0:references/graphql-go.md\n\n# graph-gophers/graphql-go Reference\n\nSchema-first, reflection-based — no codegen. Write SDL, bind Go resolver structs. Parse-time validation gives a fail-fast contract.\n\n## Setup\n\n```go\nimport (\n    \"github.com/graph-gophers/graphql-go\"\n    \"github.com/graph-gophers/graphql-go/relay\"\n    \"github.com/graph-gophers/graphql-go/trace/otel\"\n)\n\nschema := graphql.MustParseSchema(sdlString, &RootResolver{},\n    graphql.MaxDepth(10),\n    graphql.MaxParallelism(10),\n    graphql.UseFieldResolvers(), // expose exported struct fields without explicit methods\n    graphql.Tracer(otel.DefaultTracer()),\n)\n\nhttp.Handle(\"/graphql\", &relay.Handler{Schema: schema})\n```\n\n`MustParseSchema` panics on invalid SDL or resolver mismatch — catch it at startup, not at request time.\n\n## Resolver Structure\n\nOne exported method per schema field; name match is case-insensitive:\n\n```go\ntype RootResolver struct {\n    db *sql.DB\n}\n\ntype QueryResolver struct {\n    db *sql.DB\n}\n\nfunc (r *RootResolver) Query() *QueryResolver { return &QueryResolver{db: r.db} }\n\n// Args struct for field arguments\nfunc (r *QueryResolver) User(ctx context.Context, args struct{ ID graphql.ID }) (*UserResolver, error) {\n    user, err := r.db.GetUser(ctx, string(args.ID))\n    if err != nil {\n        return nil, err\n    }\n    return &UserResolver{user: user}, nil\n}\n```\n\nReturn resolver wrapper structs, not domain models directly — keeps GraphQL projection separate from persistence.\n\n## Type Mapping\n\n<!-- prettier-ignore -->\n|GraphQL type|Go type|Notes|\n|---|---|---|\n|`ID`|`graphql.ID`|string alias|\n|`Int`|`int32`|**NOT `int`** — mismatch is a parse-time error|\n|`Float`|`float64`||\n|`String`|`string`||\n|`Boolean`|`bool`||\n|`[T]`|`[]*T` or `[]T`||\n|Nullable `T`|`*T`|pointer = nullable|\n|Non-null `T!`|`T`|non-pointer|\n|Custom scalar|implement `UnmarshalGraphQL(input any) error` + `MarshalJSON() ([]byte, error)`||\n|Enum|typed string alias||\n|Input|exported struct with field tags optional||\n|Interface/Union|Go interface returned; `ToConcreteType() (*T, bool)` discriminators||\n\nCommon mistake: using `int` for an `Int!` field — the parser rejects it with a type mismatch error.\n\n## Nullable vs Non-null Arguments\n\n```go\n// ✓ Good — pointer arg = nullable in schema\nfunc (r *QueryResolver) Users(ctx context.Context, args struct {\n    Role *string // nullable: Role in SDL\n    Limit int32  // non-null: Limit! in SDL\n}) ([]*UserResolver, error) { ... }\n```\n\nForgetting `*` on a nullable argument causes unmarshal failure when clients send `null`.\n\n## Custom Scalar\n\n```go\ntype DateTime struct{ time.Time }\n\nfunc (d *DateTime) UnmarshalGraphQL(input any) error {\n    s, ok := input.(string)\n    if !ok {\n        return fmt.Errorf(\"DateTime must be a string\")\n    }\n    t, err := time.Parse(time.RFC3339, s)\n    if err != nil {\n        return err\n    }\n    d.Time = t\n    return nil\n}\n\nfunc (d DateTime) MarshalJSON() ([]byte, error) {\n    return json.Marshal(d.Time.Format(time.RFC3339))\n}\n```\n\n## Interfaces and Unions\n\n```graphql\ninterface Node {\n  id: ID!\n}\nunion SearchResult = User | Post\n```\n\n```go\n// Interface — implement ToUser, ToPost discriminators\ntype SearchResultResolver struct{ result any }\n\nfunc (r *SearchResultResolver) ToUser() (*UserResolver, bool) {\n    u, ok := r.result.(*domain.User)\n    return &UserResolver{u}, ok\n}\n\nfunc (r *SearchResultResolver) ToPost() (*PostResolver, bool) {\n    p, ok := r.result.(*domain.Post)\n    return &PostResolver{p}, ok\n}\n```\n\n## DataLoaders\n\nUse `github.com/graph-gophers/dataloader` per-request:\n\n```go\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loader := dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {\n            ids := make([]string, len(keys))\n            for i, k := range keys {\n                ids[i] = k.String()\n            }\n            posts, err := batchPostsByUserID(ctx, db, ids)\n            // map results back to keys order ...\n            return results\n        })\n        ctx := context.WithValue(r.Context(), postsLoaderKey, loader)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// In resolver\nfunc (r *UserResolver) Posts(ctx context.Context) ([]*PostResolver, error) {\n    thunk := ctx.Value(postsLoaderKey).(*dataloader.Loader).Load(ctx, dataloader.StringKey(r.user.ID))\n    result, err := thunk()\n    // ...\n}\n```\n\n## Error Handling\n\nImplement `ResolverError` to attach structured extensions:\n\n```go\ntype ResolverError interface {\n    error\n    Extensions() map[string]any\n}\n\ntype AppError struct {\n    msg  string\n    code string\n}\n\nfunc (e *AppError) Error() string { return e.msg }\nfunc (e *AppError) Extensions() map[string]any {\n    return map[string]any{\"code\": e.code}\n}\n\n// Usage in resolver\nreturn nil, &AppError{msg: \"user not found\", code: \"NOT_FOUND\"}\n```\n\nPanics in resolvers are caught automatically and converted to GraphQL errors.\n\n## OpenTelemetry Tracing\n\n```go\nimport \"github.com/graph-gophers/graphql-go/trace/otel\"\n\nschema := graphql.MustParseSchema(sdl, &RootResolver{},\n    graphql.Tracer(otel.DefaultTracer()),\n)\n```\n\nEmits spans per request, validation, and field resolution with operation name and field path.\n\n## Subscriptions\n\n```go\nfunc (r *SubscriptionResolver) MessageAdded(ctx context.Context, args struct{ Room string }) <-chan *MessageResolver {\n    ch := make(chan *MessageResolver, 1)\n    go func() {\n        defer close(ch)\n        sub := r.pubsub.Subscribe(args.Room)\n        defer sub.Unsubscribe()\n        for {\n            select {\n            case <-ctx.Done():\n                return\n            case msg := <-sub.Chan():\n                select {\n                case ch <- &MessageResolver{msg: msg}:\n                case <-ctx.Done():\n                    return\n                }\n            }\n        }\n    }()\n    return ch\n}\n```\n\nWebSocket transport is not bundled — pair with `gorilla/websocket` or use the relay handler with a WebSocket-aware mux.\n\n## Disabling Introspection\n\n```go\nschema := graphql.MustParseSchema(sdl, &RootResolver{},\n    graphql.DisableIntrospection(),\n)\n```\n\n## Testing\n\nUse `gqltesting.RunTests`:\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{ \"user\": { \"name\": \"Alice\", \"email\": \"alice@example.com\" } }`,\n        },\n    })\n}\n```\n\nFor HTTP-level tests, drive `relay.Handler` with `httptest.NewRecorder()`.\n\n## graph-gophers vs gqlgen Summary\n\n| Concern          | graph-gophers         | gqlgen                      |\n| ---------------- | --------------------- | --------------------------- |\n| Type safety      | Parse-time reflection | Compile-time codegen        |\n| Build complexity | None                  | `go generate` step          |\n| Performance      | Slower (reflection)   | Faster (static dispatch)    |\n| Federation       | Manual                | First-class (v2)            |\n| File uploads     | Manual                | Built-in MultipartForm      |\n| Best for         | Small/medium schemas  | Large schemas, strict teams |\n\nFile v0.2.0:references/testing.md\n\n# Testing GraphQL in Go\n\n## gqlgen — Client Harness\n\nThe `github.com/99designs/gqlgen/client` package drives the full stack (directives, middleware, resolvers) via an `http.Handler`:\n\n```go\nfunc TestCreateUser(t *testing.T) {\n    // Build the full handler with real dependencies (use a test DB)\n    srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{\n        Resolvers: &graph.Resolver{\n            DB: testDB,\n        },\n    }))\n\n    c := client.New(srv)\n\n    var resp struct {\n        CreateUser struct {\n            User struct {\n                ID    string\n                Email string\n            }\n            Errors []struct{ Message string }\n        }\n    }\n\n    c.MustPost(`\n        mutation CreateUser($email: String!, $name: String!) {\n            createUser(input: {email: $email, name: $name}) {\n                user { id email }\n                errors { message }\n            }\n        }\n    `, &resp,\n        client.Var(\"email\", \"alice@example.com\"),\n        client.Var(\"name\", \"Alice\"),\n        client.AddHeader(\"Authorization\", \"Bearer test-token\"),\n    )\n\n    require.Empty(t, resp.CreateUser.Errors)\n    require.Equal(t, \"alice@example.com\", resp.CreateUser.User.Email)\n}\n```\n\nFor unit testing individual resolvers, call resolver methods directly with a constructed `Resolver` and a real `context.Context` — no HTTP overhead.\n\n## gqlgen — Testing with DataLoaders\n\nWrap the test server with the DataLoader middleware so resolver tests exercise the full batching path:\n\n```go\nsrv := handler.NewDefaultServer(es)\nh := dataloaders.Middleware(testDB, srv)\n\nc := client.New(h)\n```\n\n## gqlgen — Testing Subscriptions\n\nUse `client.Subscription` to test subscription resolvers:\n\n```go\nsub := c.Subscription(`subscription { messageAdded(room: \"general\") { content } }`)\ndefer sub.Close()\n\n// Trigger an event\npublishMessage(\"general\", \"hello\")\n\nvar event struct{ MessageAdded struct{ Content string } }\nerr := sub.Next(&event)\nrequire.NoError(t, err)\nrequire.Equal(t, \"hello\", event.MessageAdded.Content)\n```\n\n## graph-gophers — gqltesting\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{\"user\":{\"name\":\"Alice\",\"email\":\"alice@example.com\"}}`,\n        },\n        {\n            Schema:        schema,\n            Query:         `{ user(id: \"999\") { name } }`,\n            ExpectedErrors: []*gqlerrors.QueryError{\n                {Message: \"user not found\", Extensions: map[string]any{\"code\": \"NOT_FOUND\"}},\n            },\n        },\n    })\n}\n```\n\nFor HTTP-level tests:\n\n```go\nfunc TestRelayHandler(t *testing.T) {\n    body := `{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}`\n    req := httptest.NewRequest(http.MethodPost, \"/graphql\", strings.NewReader(body))\n    req.Header.Set(\"Content-Type\", \"application/json\")\n    w := httptest.NewRecorder()\n\n    relay.Handler{Schema: schema}.ServeHTTP(w, req)\n\n    require.Equal(t, http.StatusOK, w.Code)\n    require.Contains(t, w.Body.String(), `\"Alice\"`)\n}\n```\n\n## Testing Error Handling\n\nVerify error extensions reach the client:\n\n```go\nvar resp struct {\n    Errors []struct {\n        Message    string\n        Extensions struct{ Code string }\n    }\n}\nc.Post(`{ user(id: \"999\") { name } }`, &resp)\nrequire.Equal(t, \"NOT_FOUND\", resp.Errors[0].Extensions.Code)\n```\n\n## Testing Auth Directives (gqlgen)\n\nTest the directive function directly:\n\n```go\nfunc TestHasRoleDirective(t *testing.T) {\n    ctx := context.WithValue(context.Background(), userKey, &domain.User{Role: \"USER\"})\n    _, err := HasRole(ctx, nil, func(ctx context.Context) (any, error) {\n        return \"ok\", nil\n    }, model.RoleAdmin)\n    require.Error(t, err)\n\n    var gqlErr *gqlerror.Error\n    require.True(t, errors.As(err, &gqlErr))\n    require.Equal(t, \"FORBIDDEN\", gqlErr.Extensions[\"code\"])\n}\n```\n\n## Table-Driven Tests\n\n```go\nfunc TestUserQueries(t *testing.T) {\n    tests := []struct {\n        name     string\n        query    string\n        vars     map[string]any\n        wantCode string\n        wantName string\n    }{\n        {\"existing user\", `query($id:ID!){user(id:$id){name}}`, map[string]any{\"id\": \"1\"}, \"\", \"Alice\"},\n        {\"missing user\", `query($id:ID!){user(id:$id){name}}`, map[string]any{\"id\": \"999\"}, \"NOT_FOUND\", \"\"},\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            var resp struct {\n                User   *struct{ Name string }\n                Errors []struct {\n                    Extensions struct{ Code string }\n                }\n            }\n            c.Post(tt.query, &resp, client.Var(\"id\", tt.vars[\"id\"]))\n            if tt.wantCode != \"\" {\n                require.Equal(t, tt.wantCode, resp.Errors[0].Extensions.Code)\n            } else {\n                require.Equal(t, tt.wantName, resp.User.Name)\n            }\n        })\n    }\n}\n```\n\nFor testing patterns across the codebase, see the `samber/cc-skills-golang@golang-testing` skill.\n\nFile v0.2.0:skill-card.md\n\n## Description:\n\nImplements GraphQL APIs in Go using gqlgen or graph-gophers/graphql-go, covering schema design, resolvers, subscriptions, testing, and production safety.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[samber](https://clawhub.ai/user/samber)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and engineering agents use this skill to build and review Go GraphQL APIs, choose gqlgen or graph-gophers/graphql-go, write schema and resolver code, prevent N+1 query patterns, test handlers and subscriptions, and apply production safety limits.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill requests broad outbound curl access beyond its documented workflows.\n\nMitigation: Review the curl permission before installation, especially in repositories with secrets or untrusted content.\n\nRisk: Mutable latest-based tool commands can make generated GraphQL code or guidance less reproducible.\n\nMitigation: Prefer scoped documentation tools and pin gqlgen to a reviewed version before running generation commands.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/samber/skills/golang-graphql)\n- [ClawHub metadata homepage](https://github.com/samber/cc-skills-golang)\n- [gqlgen reference](references/gqlgen.md)\n- [graph-gophers/graphql-go reference](references/graphql-go.md)\n- [Testing GraphQL in Go](references/testing.md)\n- [gqlgen](https://github.com/99designs/gqlgen)\n- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)\n- [Relay cursor connections spec](https://relay.dev/graphql/connections.htm)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Markdown, Code, Shell commands, Configuration]\n\n**Output Format:** [Markdown guidance with Go, GraphQL SDL, YAML, and shell command snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Applies to Go source files and may recommend Go tooling commands when relevant.]\n\n## Skill Version(s):\n\n0.2.0 (source: server release metadata and skill frontmatter)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v0.2.0:evals/evals.json\n\n{\n  \"skill_name\": \"golang-graphql\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"I have a gqlgen project with a User type and a Post type. Users have many posts. Write the Go resolver for User.posts. We fetch posts from a PostgreSQL database. The project is set up with a standard gqlgen layout.\",\n      \"expected_output\": \"A resolver that uses a per-request DataLoader (not direct DB calls) to batch-fetch posts by user IDs. Must NOT query the database directly inside the resolver. Must NOT use a global DataLoader. Should use context to access the per-request loader.\",\n      \"assertions\": [\n        \"Uses a DataLoader or batch loader to fetch posts, not a direct db.Query/QueryContext call inside the resolver method\",\n        \"Accesses the DataLoader from context (not a package-level or global variable)\",\n        \"The resolver function signature uses obj *model.User as a parameter to access the parent user's ID\",\n        \"Does not query the database directly inside the Posts resolver body\",\n        \"Mentions that the DataLoader must be injected per-request via HTTP middleware\",\n        \"DataLoader middleware creates a new loader instance per request, not a shared global\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"We have a graph-gophers/graphql-go project. I need to add a resolver that returns the total comment count for a post. The field is declared as `commentCount: Int!` in the SDL. Write the Go resolver method.\",\n      \"expected_output\": \"Resolver method using int32 (not int) as the return type for the Int! scalar field. Must use int32, since graph-gophers requires this specific type.\",\n      \"assertions\": [\n        \"Returns int32 (not int, int64, or uint) for the Int! scalar field\",\n        \"Method signature matches the SDL field name (case-insensitive: CommentCount or commentCount)\",\n        \"Does not return plain Go int — which causes a type mismatch at parse time with graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"Set up a production-ready gqlgen HTTP handler. The app will be deployed publicly. We need to make sure it's safe to expose.\",\n      \"expected_output\": \"Handler setup that gates introspection (disabled or ENV-checked in production) and adds a query complexity limit. Must not leave introspection unconditionally enabled.\",\n      \"assertions\": [\n        \"Introspection is gated — either disabled in production or guarded by an environment variable check\",\n        \"A complexity limit is set using extension.FixedComplexityLimit or equivalent\",\n        \"Does NOT call srv.Use(extension.Introspection{}) unconditionally without an env guard\",\n        \"Uses handler.New or handler.NewDefaultServer from github.com/99designs/gqlgen/graphql/handler\",\n        \"Mentions MaxDepth or complexity limiting as a protection against deeply nested queries\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"Implement a messageAdded subscription resolver in gqlgen. Messages are published via an in-memory pub/sub system. The resolver should stream new messages to subscribers in a given room.\",\n      \"expected_output\": \"Subscription resolver that closes the channel on context cancellation (defer close(ch) + ctx.Done() in a select). Must handle client disconnect to avoid goroutine leaks.\",\n      \"assertions\": [\n        \"Uses defer close(ch) to close the output channel when done\",\n        \"Uses a select statement with ctx.Done() to detect client disconnection\",\n        \"Returns a receive-only channel (<-chan *model.Message or similar)\",\n        \"Does not use a plain for-range loop without ctx.Done() check — this would cause a goroutine leak on disconnect\",\n        \"The goroutine terminates when ctx is cancelled\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"I'm using gqlgen and want to customize the User type to reuse my existing domain.User struct instead of having gqlgen generate a new one. The domain struct has an Email field. How do I configure this?\",\n      \"expected_output\": \"Uses autobind or models.<T>.model in gqlgen.yml to map the GraphQL User type to domain.User. Must NOT instruct editing models_gen.go directly.\",\n      \"assertions\": [\n        \"Uses gqlgen.yml configuration (autobind or models.<T>.model) to bind the existing struct\",\n        \"Does NOT suggest editing models_gen.go or generated.go directly\",\n        \"Shows the correct gqlgen.yml syntax for either autobind or models.<T>.model\",\n        \"Mentions that generated files are overwritten on next go generate\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"In a graph-gophers/graphql-go resolver, I need to return a user profile that includes a nullable bio field. The SDL has: bio: String. Write the Go struct field or return type for bio.\",\n      \"expected_output\": \"Uses *string (pointer to string) for the nullable String field. Non-pointer string would imply non-null in graph-gophers.\",\n      \"assertions\": [\n        \"Uses *string (pointer) for the nullable bio field, not plain string\",\n        \"Explains that non-pointer = non-null and pointer = nullable in graph-gophers type mapping\",\n        \"Does not use sql.NullString or other DB-specific nullable types for the GraphQL resolver layer\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"My gqlgen resolver calls a database function that can return sql.ErrNoRows or other database errors. Write the resolver for GetUser(id: ID!): User! that handles these cases correctly for clients.\",\n      \"expected_output\": \"Resolver wraps or transforms errors before returning them. Uses ErrorPresenter or gqlerror.Error with extensions code. Must NOT return raw sql.ErrNoRows to the client.\",\n      \"assertions\": [\n        \"Does NOT return raw sql.ErrNoRows or other internal errors directly to the client\",\n        \"Translates sql.ErrNoRows to a GraphQL error with a meaningful message or extension code (e.g. NOT_FOUND)\",\n        \"Uses gqlerror.Error or gqlerror.Errorf to format the client error\",\n        \"Mentions the ErrorPresenter as the right place to centralize error sanitization\",\n        \"Wraps internal errors so they don't expose SQL messages to API consumers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"Design a GraphQL mutation for creating a user where the email is always required but the bio is optional. The mutation should handle validation errors gracefully without returning HTTP errors.\",\n      \"expected_output\": \"Uses a mutation envelope payload type with a user field and an errors field. Email is String! (non-null) in input; bio is String (nullable). Errors are returned in the payload, not as GraphQL top-level errors.\",\n      \"assertions\": [\n        \"Input type has email: String! (non-null) for required email\",\n        \"Input type has bio: String (nullable, no !) for optional bio\",\n        \"Mutation returns a payload envelope type with both user and errors fields\",\n        \"Validation errors are returned inside the payload errors field, not as GraphQL top-level errors\",\n        \"The payload errors field uses a non-null list type like [UserError!]! or similar\",\n        \"Does NOT use HTTP 400/422 errors for validation — uses the GraphQL payload pattern\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"Write a gqlgen subscription resolver for order status updates. Use an in-memory pub/sub broker. The resolver should subscribe to a topic named after the order ID and stream status events.\",\n      \"expected_output\": \"Resolver subscribes to the pub/sub topic ONCE before launching the goroutine, not inside the goroutine loop. The channel is closed when the context is cancelled.\",\n      \"assertions\": [\n        \"Calls the pub/sub Subscribe (or equivalent) method BEFORE the go func() call, not inside the goroutine\",\n        \"Does NOT call Subscribe() inside the for-select loop body (which would create a new subscription per iteration)\",\n        \"Uses defer close(ch) to signal iteration end\",\n        \"Uses select with ctx.Done() to handle client disconnect\",\n        \"The subscription/topic handle is captured in a variable before the goroutine starts\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"In a graph-gophers/graphql-go project, add a search query: `search(query: String!, first: Int, after: ID): [User!]!`. The first and after arguments are optional pagination parameters. Write the Go args struct for this resolver.\",\n      \"expected_output\": \"Uses *int32 (not *int or int32) for the nullable Int argument 'first', and graphql.ID (or *graphql.ID) for the after argument. Nullable args use pointer types.\",\n      \"assertions\": [\n        \"Uses *int32 (pointer to int32) for the optional 'first: Int' argument — NOT *int or int32\",\n        \"Uses graphql.ID or *graphql.ID for the 'after: ID' argument\",\n        \"Optional arguments are represented as pointers to allow nil (absent) values\",\n        \"Does NOT use plain int or *int for an Int field in graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"Write a dataloadgen batch function for a gqlgen project that fetches all posts for a list of user IDs. Each user can have zero or more posts.\",\n      \"expected_output\": \"Batch function returns [][]*domain.Post (a slice of post slices, one per user ID), not []*domain.Post. The outer slice length must match the input IDs length.\",\n      \"assertions\": [\n        \"Batch function return type is [][]*domain.Post (2D slice) or equivalent — NOT []*domain.Post\",\n        \"The outer slice has exactly one element per input user ID (same length as the ids parameter)\",\n        \"Handles users with zero posts by including an empty slice (not nil or missing entry)\",\n        \"Does NOT return a flat []*domain.Post that collapses all posts into one slice\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"Add OpenTelemetry tracing to a graph-gophers/graphql-go server. Show the import statement and the ParseSchema call with the tracer configured.\",\n      \"expected_output\": \"Uses github.com/graph-gophers/graphql-go/trace/otel import and otel.DefaultTracer() — not an external otelgraphql package. Passed as graphql.Tracer() option.\",\n      \"assertions\": [\n        \"Imports github.com/graph-gophers/graphql-go/trace/otel (the bundled tracer package)\",\n        \"Uses otel.DefaultTracer() to construct the tracer — NOT otelgraphql.DefaultTracer() or similar hallucinated package\",\n        \"Passes the tracer as graphql.Tracer(otel.DefaultTracer()) option to MustParseSchema or ParseSchema\",\n        \"Does NOT import an external third-party otelgraphql package that does not exist in graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"We're building a federated GraphQL system with gqlgen. The User service owns the User type and must be resolvable by its id field from other services. What configuration and code changes are needed?\",\n      \"expected_output\": \"Configures federation.version: 2 in gqlgen.yml, adds @key(fields: 'id') to the User schema type, and implements a FindUserByID entity resolver. Mentions Apollo Router or Cosmo as the gateway.\",\n      \"assertions\": [\n        \"Adds federation block with version: 2 to gqlgen.yml\",\n        \"Adds @key(fields: \\\"id\\\") directive to the User type in the SDL schema\",\n        \"Implements or mentions the FindUserByID entity resolver (generated by gqlgen's federation support)\",\n        \"Imports or links the Apollo Federation v2 spec URL in the schema extend block\",\n        \"Does NOT describe a manual federation approach without gqlgen.yml config\"\n      ],\n      \"files\": []\n    }\n  ]\n}\n\nArchive v0.0.3: 7 files, 19224 bytes\n\nFiles: evals/evals.json (11727b), references/gqlgen.md (7477b), references/graphql-go.md (7269b), references/testing.md (5023b), skill-card.md (2785b), SKILL.md (12656b), _meta.json (133b)\n\nFile v0.0.3:SKILL.md\n\n---\nname: golang-graphql\ndescription: \"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`.\"\nuser-invocable: false\nlicense: MIT\ncompatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.\nmetadata:\n  author: samber\n  version: \"0.0.3\"\n  openclaw:\n    emoji: \"🔮\"\n    homepage: https://github.com/samber/cc-skills-golang\n    requires:\n      bins:\n        - go\n    install: []\n    skill-library-version: \"0.17.89\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs Bash(curl:*)\n---\n\n**Persona:** You are a Go GraphQL engineer. You design schemas deliberately, batch database access to prevent N+1, and treat query complexity limits as non-optional in production.\n\n**Modes:**\n\n- **Build mode** — generating new schemas, resolvers, or server setup: follow the skill's sequential instructions; launch a background agent to grep for existing resolver patterns and naming conventions before generating new code.\n- **Review mode** — auditing a GraphQL codebase or PR: use a sub-agent to scan for N+1 resolver patterns, missing complexity caps, global DataLoaders, and introspection enabled in production, in parallel with reading the business logic.\n\n> **Community default.** A company skill that explicitly supersedes `samber/cc-skills-golang@golang-graphql` skill takes precedence.\n\n# Go GraphQL Best Practices\n\nBoth major libraries are schema-first: write SDL (`.graphql` files), bind Go resolvers. Choose based on project size and team preferences.\n\nThis skill is not exhaustive. Refer to each library's official documentation and code examples for current API signatures. Context7 can help as a discoverability platform.\n\n## Library Choice\n\n| Library | Approach | Type safety | Build step | Best for |\n| --- | --- | --- | --- | --- |\n| `github.com/99designs/gqlgen` | Codegen | Compile-time | `go generate` | Large schemas, federation, strict types |\n| `github.com/graph-gophers/graphql-go` | Reflection | Parse-time | None | Simple schemas, fast iteration |\n| `github.com/graphql-go/graphql` | Code-first | Runtime | None | **Avoid** — verbose, no SDL |\n\nPick **gqlgen** when: Apollo Federation is required, schema is large (100+ types), or the team wants generated stubs and zero reflection overhead.\n\nPick **graph-gophers** when: schema is small/medium, the build pipeline should stay simple, or a dynamic schema is needed.\n\nFor deep-dive on each library, see [gqlgen reference](./references/gqlgen.md) and [graphql-go reference](./references/graphql-go.md).\n\n## Schema Design\n\n```graphql\n# ✓ Good — explicit nullability; ID scalar for opaque identifiers\ntype User {\n  id: ID!\n  email: String! # non-null: the server can always return this\n  bio: String # nullable: may be unset\n  posts(first: Int = 10, after: String): PostConnection!\n}\n\n# ✗ Bad — Int ID leaks implementation details, breaks client caching\ntype Post {\n  id: Int!\n}\n```\n\n**Nullability rule:** mark a field `!` only when the server can _always_ return a value. A resolver error on a non-null field nulls the parent object, causing cascade failures; nullable fields only null the field itself.\n\n**Pagination:** use Relay cursor connections (`Connection`/`Edge`/`PageInfo`) for list fields. Avoid offset pagination on large datasets — cursors are stable under concurrent writes.\n\n**Mutations:** wrap results in an envelope type so clients receive business errors alongside partial results without polluting the GraphQL `errors` array:\n\n```graphql\ntype CreateUserPayload {\n  user: User\n  errors: [UserError!]!\n}\n```\n\n## Resolver Patterns\n\nKeep resolvers thin — they translate GraphQL inputs to domain calls and domain responses to GraphQL outputs.\n\n```go\n// ✓ Good — resolver delegates to service layer\nfunc (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error) {\n    user, err := r.userService.Create(ctx, input.Email, input.Name)\n    if err != nil {\n        return nil, formatError(err)\n    }\n    return &model.CreateUserPayload{User: toGQLUser(user)}, nil\n}\n\n// ✗ Bad — SQL in resolver, no separation of concerns\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {\n    row := r.db.QueryRowContext(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    // ...\n}\n```\n\nUse per-type resolver structs (`userResolver`, `postResolver`) rather than one monolithic resolver for all fields.\n\n## N+1 Prevention (DataLoaders)\n\nEach `User.posts` resolver fires a SQL query per user without batching — O(n) DB calls for n users. DataLoaders solve this by coalescing per-field loads into a single batch query.\n\n**Critical rule: DataLoaders MUST be created per-request in HTTP middleware, never globally.** A global DataLoader caches across requests — stale data, potential cross-user data leakage.\n\n```go\n// ✓ Good — per-request DataLoader in middleware\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: newPostsByUserIDLoader(r.Context(), db),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// ✗ Bad — global DataLoader shared across all requests\nvar globalLoader = newPostsByUserIDLoader(context.Background(), db)\n```\n\nIn gqlgen, mark batched fields with `resolver: true` in `gqlgen.yml` to force a dedicated resolver method. See [gqlgen reference](./references/gqlgen.md) for full DataLoader wiring.\n\n## Authentication and Authorization\n\nTwo-layer model:\n\n1. **HTTP middleware** — extract and validate tokens, stash identity in `context.Context`.\n2. **Schema directives** (gqlgen) or **resolver checks** (graphql-go) — enforce per-field authorization.\n\n```go\n// HTTP middleware layer (both libraries)\nfunc AuthMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        token := r.Header.Get(\"Authorization\")\n        user, err := validateToken(token)\n        if err != nil {\n            http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n            return\n        }\n        ctx := context.WithValue(r.Context(), userKey, user)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n```\n\nIn gqlgen, use `@hasRole` schema directives for field-level authorization — authorization policy lives in the schema, not scattered across resolvers. See [gqlgen reference](./references/gqlgen.md).\n\n## Error Handling\n\nNever return raw internal errors — they leak SQL messages, stack traces, or service internals to clients.\n\n```go\n// gqlgen — custom ErrorPresenter strips internal details\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr // already formatted\n    }\n    // log internal err here\n    return gqlerror.Errorf(\"internal error\") // safe client message\n})\n\n// Add extension codes for client-side error handling\nreturn nil, &gqlerror.Error{\n    Message: \"user not found\",\n    Extensions: map[string]any{\"code\": \"NOT_FOUND\"},\n}\n```\n\nFor graph-gophers, implement the `ResolverError` interface to attach `Extensions()`. See [graphql-go reference](./references/graphql-go.md).\n\nUse `graphql.AddError(ctx, err)` in gqlgen for non-fatal field errors where the resolver can still return partial data.\n\nFor error wrapping patterns, see the `samber/cc-skills-golang@golang-error-handling` skill.\n\n## Subscriptions\n\nSubscriptions use long-lived WebSocket connections. The critical discipline: **always respect context cancellation** — a leaked goroutine per disconnected client exhausts resources silently.\n\n```go\n// ✓ Good — closes channel when client disconnects\nfunc (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {\n    ch := make(chan *model.Message, 1)\n    sub := r.pubsub.Subscribe(room) // subscribe once before the goroutine\n    go func() {\n        defer close(ch) // always close; signals iteration to stop\n        for {\n            select {\n            case <-ctx.Done():\n                return // client disconnected\n            case msg := <-sub:\n                select {\n                case ch <- msg:\n                case <-ctx.Done():\n                    return\n                }\n            }\n        }\n    }()\n    return ch, nil\n}\n\n// ✗ Bad — goroutine leaks forever when client disconnects\nfunc (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {\n    ch := make(chan *model.Message, 1)\n    go func() {\n        for msg := range r.pubsub.Subscribe(room) {\n            ch <- msg // blocks forever after client gone\n        }\n    }()\n    return ch, nil\n}\n```\n\n## Performance and Safety\n\nProduction GraphQL servers require explicit limits. Without them, a single deeply nested query exhausts CPU and memory.\n\n```go\n// gqlgen — wire these into every production handler\nsrv := handler.NewDefaultServer(es)\nsrv.Use(extension.FixedComplexityLimit(200)) // max cost per query\n\n// Gate introspection — only in non-production environments\nif os.Getenv(\"ENV\") != \"production\" {\n    srv.Use(extension.Introspection{})\n}\n```\n\nFor graph-gophers: `graphql.MaxDepth(10)` and `graphql.MaxParallelism(10)` options at `ParseSchema` time.\n\n**Query allow-listing:** in production, consider persisted queries (gqlgen APQ extension) to reject arbitrary query strings.\n\n## Common Mistakes\n\n| Mistake | Why it matters | Fix |\n| --- | --- | --- |\n| N+1 queries in child resolvers | One SQL per parent row → O(n) DB calls | Use per-request DataLoader |\n| Global DataLoader | Cross-request cache — stale data, data leaks | Create DataLoader in request middleware |\n| Editing `models_gen.go` directly | Next `go generate` wipes hand edits | Use `autobind` or `models.<T>.model` in `gqlgen.yml` |\n| Forgetting `go generate` after schema change | Resolver interface mismatch at compile time | Re-run `go tool gqlgen generate` |\n| `int` field in graph-gophers resolver | Library requires `int32` for `Int` scalar | Use `int32` (or `float64` for `Float`) |\n| Introspection enabled in production | Exposes full schema to attackers | Gate with `ENV` check |\n| No complexity cap | Deeply nested query → CPU/memory DoS | `extension.FixedComplexityLimit(N)` |\n| Leaking DB errors from resolvers | Exposes SQL internals to clients | Wrap in `ErrorPresenter` / `ResolverError` |\n| Subscription goroutine leak | Client disconnect → goroutine runs forever | `defer close(ch)` + `select ctx.Done()` |\n| Nullable field for always-required data | Clients must null-check everywhere | Mark `!` in schema; return error from resolver |\n\n## Deep Dives\n\n- **[gqlgen reference](./references/gqlgen.md)** — codegen workflow, `gqlgen.yml`, DataLoaders, Federation v2, directives\n- **[graphql-go reference](./references/graphql-go.md)** — reflection resolver model, type mapping, tracing\n- **[Testing](./references/testing.md)** — gqlgen client harness, gqltesting, httptest patterns\n\n## Cross-References\n\n- → See `samber/cc-skills-golang@golang-context` skill for context propagation in resolvers and subscriptions\n- → See `samber/cc-skills-golang@golang-error-handling` skill for error wrapping and sentinel patterns\n- → See `samber/cc-skills-golang@golang-testing` skill for table-driven and integration test patterns\n- → See `samber/cc-skills-golang@golang-observability` skill for tracing and metrics in resolvers\n- → See `samber/cc-skills-golang@golang-security` skill for input validation and injection prevention\n- → See `samber/cc-skills-golang@golang-database` skill for N+1 query patterns and DataLoader database batching\n\n## References\n\n- [gqlgen](https://github.com/99designs/gqlgen)\n- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)\n- [Relay cursor connections spec](https://relay.dev/graphql/connections.htm)\n\nIf you encounter a bug or unexpected behavior in gqlgen, open an issue at <https://github.com/99designs/gqlgen/issues>.\n\nIf you encounter a bug or unexpected behavior in graph-gophers/graphql-go, open an issue at <https://github.com/graph-gophers/graphql-go/issues>.\n\nFile v0.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn72rhnkwjfeex9wr1n7y24qa983cjn3\",\n  \"slug\": \"golang-graphql\",\n  \"version\": \"0.0.3\",\n  \"publishedAt\": 1779469088887\n}\n\nFile v0.0.3:references/gqlgen.md\n\n# gqlgen Reference\n\ngqlgen is a schema-first, code-generation library. Write SDL, run `go generate`, fill in resolver bodies.\n\n## Project Setup\n\n```bash\n# Bootstrap a new project\ngo run github.com/99designs/gqlgen init\n\n# Pin the tool in go.mod for reproducible generation (Go 1.24+)\ngo get -tool github.com/99designs/gqlgen@latest\n```\n\nFor Go <1.24 modules, use the legacy `tools.go` blank-import workaround instead.\n\n```bash\n# Regenerate after every schema change\ngo tool gqlgen generate\n```\n\nNever hand-edit generated files (`generated.go`, `models_gen.go`) — `generate` overwrites them.\n\n## gqlgen.yml\n\n```yaml\nschema:\n  - graph/schema/*.graphql\n\nexec:\n  filename: graph/generated.go\n  package: graph\n\nmodel:\n  filename: graph/model/models_gen.go\n  package: model\n\nresolver:\n  layout: follow-schema # one resolvers file per schema file\n  dir: graph\n  package: graph\n  filename_template: \"{name}.resolvers.go\"\n\nautobind:\n  - github.com/me/app/internal/domain # reuse existing structs\n\nmodels:\n  # ID: graphql.IntID  # legacy only — use opaque string IDs for new schemas\n  User:\n    model: github.com/me/app/internal/domain.User\n    fields:\n      posts:\n        resolver: true # force a custom resolver (required for DataLoader fields)\n\nomit_slice_element_pointers: true\nstruct_fields_always_pointers: false\nresolvers_always_return_pointers: true\n```\n\nKey knobs:\n\n- `autobind` — maps Go structs to GraphQL types; fields must match by name (case-insensitive)\n- `models.<T>.model` — override which Go type backs a GraphQL type\n- `fields.<f>.resolver: true` — force a custom resolver instead of struct field access; required for any field that should batch via DataLoader\n- `struct_fields_always_pointers` / `resolvers_always_return_pointers` — controls `*T` vs `T` in generated signatures; match your domain model conventions\n\n## Resolver Structure\n\nThe generated `Config` holds a `Resolvers` field of the generated interface. You implement it:\n\n```go\n// graph/resolver.go — you own this file, not generated\ntype Resolver struct {\n    db          *sql.DB\n    userService *service.UserService\n    loaders     *dataloaders.Loaders // injected per-request\n}\n```\n\nPer-type resolvers implement the generated interface split by GraphQL type:\n\n```go\ntype queryResolver struct{ *Resolver }\ntype mutationResolver struct{ *Resolver }\ntype userResolver struct{ *Resolver }\n\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { ... }\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) { ... }\n```\n\n`obj` is the parent object — the entry point for walking the graph.\n\n## DataLoaders (gqlgen)\n\nUse `github.com/vikstrous/dataloadgen` (generics, fast) or `github.com/graph-gophers/dataloader`:\n\n```go\n// Inject per-request via middleware\nfunc Middleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: dataloadgen.NewLoader(func(ctx context.Context, ids []string) ([][]*domain.Post, []error) {\n                return batchPostsByUserID(ctx, db, ids) // returns one []Post per user ID\n            }, dataloadgen.WithWait(1*time.Millisecond)),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// Resolver uses the loader — never the DB directly\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) {\n    return loaders.For(ctx).PostsByUserID.Load(ctx, obj.ID)\n}\n```\n\nSet `wait` to 1–2ms — allows multiple concurrent resolvers to register keys before the batch fires.\n\n## Authentication Directives\n\n```graphql\ndirective @hasRole(role: Role!) on FIELD_DEFINITION\n\ntype Query {\n  adminStats: Stats! @hasRole(role: ADMIN)\n}\n```\n\n```go\n// Implement the directive function\nfunc HasRole(ctx context.Context, obj any, next graphql.Resolver, role model.Role) (any, error) {\n    user := auth.UserFromContext(ctx)\n    if user == nil || user.Role != role {\n        return nil, &gqlerror.Error{\n            Message:    \"access denied\",\n            Extensions: map[string]any{\"code\": \"FORBIDDEN\"},\n        }\n    }\n    return next(ctx)\n}\n\n// Register at server bootstrap\nc := generated.Config{\n    Resolvers: &graph.Resolver{...},\n    Directives: generated.DirectiveRoot{\n        HasRole: HasRole,\n    },\n}\n```\n\n## Middleware Hooks\n\n```go\nsrv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {\n    // log operation name, add trace span\n    return next(ctx)\n})\nsrv.AroundFields(func(ctx context.Context, next graphql.Resolver) (any, error) {\n    // per-field tracing, timing\n    return next(ctx)\n})\n```\n\n## Error Presenter\n\n```go\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr\n    }\n    log.Ctx(ctx).Error(\"resolver error\", \"err\", err)\n    return gqlerror.Errorf(\"internal server error\")\n})\n\nsrv.SetRecoverFunc(func(ctx context.Context, err any) error {\n    log.Ctx(ctx).Error(\"panic in resolver\", \"err\", err)\n    return fmt.Errorf(\"internal server error\")\n})\n```\n\n## Subscriptions\n\n```go\nsrv.AddTransport(transport.Websocket{\n    KeepAlivePingInterval: 10 * time.Second,\n    Upgrader: websocket.Upgrader{\n        // Restrict to your own origin in production; true here is dev-only.\n        CheckOrigin: func(r *http.Request) bool {\n            return r.Header.Get(\"Origin\") == \"https://app.example.com\"\n        },\n    },\n    InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {\n        // auth at connection time\n        token := initPayload.Authorization()\n        user, err := validateToken(token)\n        if err != nil {\n            return ctx, nil, err\n        }\n        return context.WithValue(ctx, userKey, user), &initPayload, nil\n    },\n})\n```\n\ngqlgen supports both `graphql-ws` (legacy) and `graphql-transport-ws` (current) subprotocols.\n\n## File Uploads\n\n```go\nsrv.AddTransport(transport.MultipartForm{\n    MaxUploadSize: 10 << 20, // 10 MB total\n    MaxMemory:     5 << 20,  // 5 MB in memory; rest spills to disk\n})\n```\n\nSchema:\n\n```graphql\nscalar Upload\n\ntype Mutation {\n  uploadAvatar(file: Upload!): User!\n}\n```\n\nResolver receives `graphql.Upload{File io.Reader, Filename string, Size int64, ContentType string}`.\n\n## Apollo Federation v2\n\n`gqlgen.yml`:\n\n```yaml\nfederation:\n  filename: graph/federation.go\n  version: 2\n```\n\nSchema:\n\n```graphql\nextend schema\n  @link(\n    url: \"https://specs.apollo.dev/federation/v2.3\"\n    import: [\"@key\", \"@shareable\", \"@external\"]\n  )\n\ntype User @key(fields: \"id\") {\n  id: ID!\n  name: String!\n}\n```\n\nImplement `FindUserByID` in the generated entity resolver. Works with Apollo Router and Cosmo.\n\n## Production Handler Setup\n\n```go\nsrv := handler.New(es)\nsrv.AddTransport(transport.Options{})\nsrv.AddTransport(transport.GET{})\nsrv.AddTransport(transport.POST{})\nsrv.AddTransport(transport.MultipartForm{MaxUploadSize: 10 << 20, MaxMemory: 5 << 20})\nsrv.AddTransport(transport.Websocket{KeepAlivePingInterval: 10 * time.Second})\n\nsrv.SetQueryCache(lru.New[*ast.QueryDocument](1000))\nif os.Getenv(\"ENV\") != \"production\" {\n    srv.Use(extension.Introspection{})\n}\nsrv.Use(extension.AutomaticPersistedQuery{Cache: lru.New[string](100)})\nsrv.Use(extension.FixedComplexityLimit(200))\n```\n\nFile v0.0.3:references/graphql-go.md\n\n# graph-gophers/graphql-go Reference\n\nSchema-first, reflection-based — no codegen. Write SDL, bind Go resolver structs. Parse-time validation gives a fail-fast contract.\n\n## Setup\n\n```go\nimport (\n    \"github.com/graph-gophers/graphql-go\"\n    \"github.com/graph-gophers/graphql-go/relay\"\n    \"github.com/graph-gophers/graphql-go/trace/otel\"\n)\n\nschema := graphql.MustParseSchema(sdlString, &RootResolver{},\n    graphql.MaxDepth(10),\n    graphql.MaxParallelism(10),\n    graphql.UseFieldResolvers(), // expose exported struct fields without explicit methods\n    graphql.Tracer(otel.DefaultTracer()),\n)\n\nhttp.Handle(\"/graphql\", &relay.Handler{Schema: schema})\n```\n\n`MustParseSchema` panics on invalid SDL or resolver mismatch — catch it at startup, not at request time.\n\n## Resolver Structure\n\nOne exported method per schema field; name match is case-insensitive:\n\n```go\ntype RootResolver struct {\n    db *sql.DB\n}\n\ntype QueryResolver struct {\n    db *sql.DB\n}\n\nfunc (r *RootResolver) Query() *QueryResolver { return &QueryResolver{db: r.db} }\n\n// Args struct for field arguments\nfunc (r *QueryResolver) User(ctx context.Context, args struct{ ID graphql.ID }) (*UserResolver, error) {\n    user, err := r.db.GetUser(ctx, string(args.ID))\n    if err != nil {\n        return nil, err\n    }\n    return &UserResolver{user: user}, nil\n}\n```\n\nReturn resolver wrapper structs, not domain models directly — keeps GraphQL projection separate from persistence.\n\n## Type Mapping\n\n| GraphQL type | Go type | Notes |\n| --- | --- | --- |\n| `ID` | `graphql.ID` | string alias |\n| `Int` | `int32` | **NOT `int`** — mismatch is a parse-time error |\n| `Float` | `float64` | |\n| `String` | `string` | |\n| `Boolean` | `bool` | |\n| `[T]` | `[]*T` or `[]T` | |\n| Nullable `T` | `*T` | pointer = nullable |\n| Non-null `T!` | `T` | non-pointer |\n| Custom scalar | implement `UnmarshalGraphQL(input any) error` + `MarshalJSON() ([]byte, error)` | |\n| Enum | typed string alias | |\n| Input | exported struct with field tags optional | |\n| Interface/Union | Go interface returned; `ToConcreteType() (*T, bool)` discriminators | |\n\nCommon mistake: using `int` for an `Int!` field — the parser rejects it with a type mismatch error.\n\n## Nullable vs Non-null Arguments\n\n```go\n// ✓ Good — pointer arg = nullable in schema\nfunc (r *QueryResolver) Users(ctx context.Context, args struct {\n    Role *string // nullable: Role in SDL\n    Limit int32  // non-null: Limit! in SDL\n}) ([]*UserResolver, error) { ... }\n```\n\nForgetting `*` on a nullable argument causes unmarshal failure when clients send `null`.\n\n## Custom Scalar\n\n```go\ntype DateTime struct{ time.Time }\n\nfunc (d *DateTime) UnmarshalGraphQL(input any) error {\n    s, ok := input.(string)\n    if !ok {\n        return fmt.Errorf(\"DateTime must be a string\")\n    }\n    t, err := time.Parse(time.RFC3339, s)\n    if err != nil {\n        return err\n    }\n    d.Time = t\n    return nil\n}\n\nfunc (d DateTime) MarshalJSON() ([]byte, error) {\n    return json.Marshal(d.Time.Format(time.RFC3339))\n}\n```\n\n## Interfaces and Unions\n\n```graphql\ninterface Node {\n  id: ID!\n}\nunion SearchResult = User | Post\n```\n\n```go\n// Interface — implement ToUser, ToPost discriminators\ntype SearchResultResolver struct{ result any }\n\nfunc (r *SearchResultResolver) ToUser() (*UserResolver, bool) {\n    u, ok := r.result.(*domain.User)\n    return &UserResolver{u}, ok\n}\n\nfunc (r *SearchResultResolver) ToPost() (*PostResolver, bool) {\n    p, ok := r.result.(*domain.Post)\n    return &PostResolver{p}, ok\n}\n```\n\n## DataLoaders\n\nUse `github.com/graph-gophers/dataloader` per-request:\n\n```go\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loader := dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {\n            ids := make([]string, len(keys))\n            for i, k := range keys {\n                ids[i] = k.String()\n            }\n            posts, err := batchPostsByUserID(ctx, db, ids)\n            // map results back to keys order ...\n            return results\n        })\n        ctx := context.WithValue(r.Context(), postsLoaderKey, loader)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// In resolver\nfunc (r *UserResolver) Posts(ctx context.Context) ([]*PostResolver, error) {\n    thunk := ctx.Value(postsLoaderKey).(*dataloader.Loader).Load(ctx, dataloader.StringKey(r.user.ID))\n    result, err := thunk()\n    // ...\n}\n```\n\n## Error Handling\n\nImplement `ResolverError` to attach structured extensions:\n\n```go\ntype ResolverError interface {\n    error\n    Extensions() map[string]any\n}\n\ntype AppError struct {\n    msg  string\n    code string\n}\n\nfunc (e *AppError) Error() string { return e.msg }\nfunc (e *AppError) Extensions() map[string]any {\n    return map[string]any{\"code\": e.code}\n}\n\n// Usage in resolver\nreturn nil, &AppError{msg: \"user not found\", code: \"NOT_FOUND\"}\n```\n\nPanics in resolvers are caught automatically and converted to GraphQL errors.\n\n## OpenTelemetry Tracing\n\n```go\nimport \"github.com/graph-gophers/graphql-go/trace/otel\"\n\nschema := graphql.MustParseSchema(sdl, &RootResolver{},\n    graphql.Tracer(otel.DefaultTracer()),\n)\n```\n\nEmits spans per request, validation, and field resolution with operation name and field path.\n\n## Subscriptions\n\n```go\nfunc (r *SubscriptionResolver) MessageAdded(ctx context.Context, args struct{ Room string }) <-chan *MessageResolver {\n    ch := make(chan *MessageResolver, 1)\n    go func() {\n        defer close(ch)\n        sub := r.pubsub.Subscribe(args.Room)\n        defer sub.Unsubscribe()\n        for {\n            select {\n            case <-ctx.Done():\n                return\n            case msg := <-sub.Chan():\n                select {\n                case ch <- &MessageResolver{msg: msg}:\n                case <-ctx.Done():\n                    return\n                }\n            }\n        }\n    }()\n    return ch\n}\n```\n\nWebSocket transport is not bundled — pair with `gorilla/websocket` or use the relay handler with a WebSocket-aware mux.\n\n## Disabling Introspection\n\n```go\nschema := graphql.MustParseSchema(sdl, &RootResolver{},\n    graphql.DisableIntrospection(),\n)\n```\n\n## Testing\n\nUse `gqltesting.RunTests`:\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{ \"user\": { \"name\": \"Alice\", \"email\": \"alice@example.com\" } }`,\n        },\n    })\n}\n```\n\nFor HTTP-level tests, drive `relay.Handler` with `httptest.NewRecorder()`.\n\n## graph-gophers vs gqlgen Summary\n\n| Concern          | graph-gophers         | gqlgen                      |\n| ---------------- | --------------------- | --------------------------- |\n| Type safety      | Parse-time reflection | Compile-time codegen        |\n| Build complexity | None                  | `go generate` step          |\n| Performance      | Slower (reflection)   | Faster (static dispatch)    |\n| Federation       | Manual                | First-class (v2)            |\n| File uploads     | Manual                | Built-in MultipartForm      |\n| Best for         | Small/medium schemas  | Large schemas, strict teams |\n\nFile v0.0.3:references/testing.md\n\n# Testing GraphQL in Go\n\n## gqlgen — Client Harness\n\nThe `github.com/99designs/gqlgen/client` package drives the full stack (directives, middleware, resolvers) via an `http.Handler`:\n\n```go\nfunc TestCreateUser(t *testing.T) {\n    // Build the full handler with real dependencies (use a test DB)\n    srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{\n        Resolvers: &graph.Resolver{\n            DB: testDB,\n        },\n    }))\n\n    c := client.New(srv)\n\n    var resp struct {\n        CreateUser struct {\n            User struct {\n                ID    string\n                Email string\n            }\n            Errors []struct{ Message string }\n        }\n    }\n\n    c.MustPost(`\n        mutation CreateUser($email: String!, $name: String!) {\n            createUser(input: {email: $email, name: $name}) {\n                user { id email }\n                errors { message }\n            }\n        }\n    `, &resp,\n        client.Var(\"email\", \"alice@example.com\"),\n        client.Var(\"name\", \"Alice\"),\n        client.AddHeader(\"Authorization\", \"Bearer test-token\"),\n    )\n\n    require.Empty(t, resp.CreateUser.Errors)\n    require.Equal(t, \"alice@example.com\", resp.CreateUser.User.Email)\n}\n```\n\nFor unit testing individual resolvers, call resolver methods directly with a constructed `Resolver` and a real `context.Context` — no HTTP overhead.\n\n## gqlgen — Testing with DataLoaders\n\nWrap the test server with the DataLoader middleware so resolver tests exercise the full batching path:\n\n```go\nsrv := handler.NewDefaultServer(es)\nh := dataloaders.Middleware(testDB, srv)\n\nc := client.New(h)\n```\n\n## gqlgen — Testing Subscriptions\n\nUse `client.Subscription` to test subscription resolvers:\n\n```go\nsub := c.Subscription(`subscription { messageAdded(room: \"general\") { content } }`)\ndefer sub.Close()\n\n// Trigger an event\npublishMessage(\"general\", \"hello\")\n\nvar event struct{ MessageAdded struct{ Content string } }\nerr := sub.Next(&event)\nrequire.NoError(t, err)\nrequire.Equal(t, \"hello\", event.MessageAdded.Content)\n```\n\n## graph-gophers — gqltesting\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{\"user\":{\"name\":\"Alice\",\"email\":\"alice@example.com\"}}`,\n        },\n        {\n            Schema:        schema,\n            Query:         `{ user(id: \"999\") { name } }`,\n            ExpectedErrors: []*gqlerrors.QueryError{\n                {Message: \"user not found\", Extensions: map[string]any{\"code\": \"NOT_FOUND\"}},\n            },\n        },\n    })\n}\n```\n\nFor HTTP-level tests:\n\n```go\nfunc TestRelayHandler(t *testing.T) {\n    body := `{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}`\n    req := httptest.NewRequest(http.MethodPost, \"/graphql\", strings.NewReader(body))\n    req.Header.Set(\"Content-Type\", \"application/json\")\n    w := httptest.NewRecorder()\n\n    relay.Handler{Schema: schema}.ServeHTTP(w, req)\n\n    require.Equal(t, http.StatusOK, w.Code)\n    require.Contains(t, w.Body.String(), `\"Alice\"`)\n}\n```\n\n## Testing Error Handling\n\nVerify error extensions reach the client:\n\n```go\nvar resp struct {\n    Errors []struct {\n        Message    string\n        Extensions struct{ Code string }\n    }\n}\nc.Post(`{ user(id: \"999\") { name } }`, &resp)\nrequire.Equal(t, \"NOT_FOUND\", resp.Errors[0].Extensions.Code)\n```\n\n## Testing Auth Directives (gqlgen)\n\nTest the directive function directly:\n\n```go\nfunc TestHasRoleDirective(t *testing.T) {\n    ctx := context.WithValue(context.Background(), userKey, &domain.User{Role: \"USER\"})\n    _, err := HasRole(ctx, nil, func(ctx context.Context) (any, error) {\n        return \"ok\", nil\n    }, model.RoleAdmin)\n    require.Error(t, err)\n\n    var gqlErr *gqlerror.Error\n    require.True(t, errors.As(err, &gqlErr))\n    require.Equal(t, \"FORBIDDEN\", gqlErr.Extensions[\"code\"])\n}\n```\n\n## Table-Driven Tests\n\n```go\nfunc TestUserQueries(t *testing.T) {\n    tests := []struct {\n        name     string\n        query    string\n        vars     map[string]any\n        wantCode string\n        wantName string\n    }{\n        {\"existing user\", `query($id:ID!){user(id:$id){name}}`, map[string]any{\"id\": \"1\"}, \"\", \"Alice\"},\n        {\"missing user\", `query($id:ID!){user(id:$id){name}}`, map[string]any{\"id\": \"999\"}, \"NOT_FOUND\", \"\"},\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            var resp struct {\n                User   *struct{ Name string }\n                Errors []struct {\n                    Extensions struct{ Code string }\n                }\n            }\n            c.Post(tt.query, &resp, client.Var(\"id\", tt.vars[\"id\"]))\n            if tt.wantCode != \"\" {\n                require.Equal(t, tt.wantCode, resp.Errors[0].Extensions.Code)\n            } else {\n                require.Equal(t, tt.wantName, resp.User.Name)\n            }\n        })\n    }\n}\n```\n\nFor testing patterns across the codebase, see the `samber/cc-skills-golang@golang-testing` skill.\n\nFile v0.0.3:skill-card.md\n\n## Description: <br>\nImplements GraphQL APIs in Go using gqlgen or graph-gophers/graphql-go for schema design, resolver work, subscriptions, and integration with existing Go HTTP services. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[samber](https://clawhub.ai/user/samber) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill to build or review Go GraphQL servers, including schema design, resolver implementation, DataLoader batching, subscriptions, testing, and production safety controls. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Proposed file edits, generated code, dependency changes, or Go commands such as go get and go generate could alter application behavior or supply chain state. <br>\nMitigation: Review diffs, dependency changes, generated files, and commands before applying them, especially in sensitive repositories. <br>\nRisk: GraphQL implementation guidance affects production controls such as query limits, introspection exposure, authorization checks, error handling, subscription cancellation, and DataLoader scoping. <br>\nMitigation: Verify complexity or depth limits, production introspection gating, per-request DataLoaders, safe error formatting, authorization policy, and subscription cancellation against project requirements. <br>\n\n\n## Reference(s): <br>\n- [ClawHub Skill Page](https://clawhub.ai/samber/golang-graphql) <br>\n- [Source Homepage](https://github.com/samber/cc-skills-golang) <br>\n- [gqlgen Reference](references/gqlgen.md) <br>\n- [graph-gophers/graphql-go Reference](references/graphql-go.md) <br>\n- [Testing GraphQL in Go](references/testing.md) <br>\n- [gqlgen](https://github.com/99designs/gqlgen) <br>\n- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go) <br>\n- [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Markdown, Code, Shell commands, Configuration] <br>\n**Output Format:** [Markdown guidance with Go, GraphQL SDL, YAML configuration, and shell command snippets] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May propose file edits, dependency updates, gqlgen code generation, tests, and Go tooling commands for review before execution.] <br>\n\n## Skill Version(s): <br>\n0.0.3 (source: artifact SKILL.md frontmatter and server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v0.0.3:evals/evals.json\n\n{\n  \"skill_name\": \"golang-graphql\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"I have a gqlgen project with a User type and a Post type. Users have many posts. Write the Go resolver for User.posts. We fetch posts from a PostgreSQL database. The project is set up with a standard gqlgen layout.\",\n      \"expected_output\": \"A resolver that uses a per-request DataLoader (not direct DB calls) to batch-fetch posts by user IDs. Must NOT query the database directly inside the resolver. Must NOT use a global DataLoader. Should use context to access the per-request loader.\",\n      \"assertions\": [\n        \"Uses a DataLoader or batch loader to fetch posts, not a direct db.Query/QueryContext call inside the resolver method\",\n        \"Accesses the DataLoader from context (not a package-level or global variable)\",\n        \"The resolver function signature uses obj *model.User as a parameter to access the parent user's ID\",\n        \"Does not query the database directly inside the Posts resolver body\",\n        \"Mentions that the DataLoader must be injected per-request via HTTP middleware\",\n        \"DataLoader middleware creates a new loader instance per request, not a shared global\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"We have a graph-gophers/graphql-go project. I need to add a resolver that returns the total comment count for a post. The field is declared as `commentCount: Int!` in the SDL. Write the Go resolver method.\",\n      \"expected_output\": \"Resolver method using int32 (not int) as the return type for the Int! scalar field. Must use int32, since graph-gophers requires this specific type.\",\n      \"assertions\": [\n        \"Returns int32 (not int, int64, or uint) for the Int! scalar field\",\n        \"Method signature matches the SDL field name (case-insensitive: CommentCount or commentCount)\",\n        \"Does not return plain Go int — which causes a type mismatch at parse time with graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"Set up a production-ready gqlgen HTTP handler. The app will be deployed publicly. We need to make sure it's safe to expose.\",\n      \"expected_output\": \"Handler setup that gates introspection (disabled or ENV-checked in production) and adds a query complexity limit. Must not leave introspection unconditionally enabled.\",\n      \"assertions\": [\n        \"Introspection is gated — either disabled in production or guarded by an environment variable check\",\n        \"A complexity limit is set using extension.FixedComplexityLimit or equivalent\",\n        \"Does NOT call srv.Use(extension.Introspection{}) unconditionally without an env guard\",\n        \"Uses handler.New or handler.NewDefaultServer from github.com/99designs/gqlgen/graphql/handler\",\n        \"Mentions MaxDepth or complexity limiting as a protection against deeply nested queries\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"Implement a messageAdded subscription resolver in gqlgen. Messages are published via an in-memory pub/sub system. The resolver should stream new messages to subscribers in a given room.\",\n      \"expected_output\": \"Subscription resolver that closes the channel on context cancellation (defer close(ch) + ctx.Done() in a select). Must handle client disconnect to avoid goroutine leaks.\",\n      \"assertions\": [\n        \"Uses defer close(ch) to close the output channel when done\",\n        \"Uses a select statement with ctx.Done() to detect client disconnection\",\n        \"Returns a receive-only channel (<-chan *model.Message or similar)\",\n        \"Does not use a plain for-range loop without ctx.Done() check — this would cause a goroutine leak on disconnect\",\n        \"The goroutine terminates when ctx is cancelled\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"I'm using gqlgen and want to customize the User type to reuse my existing domain.User struct instead of having gqlgen generate a new one. The domain struct has an Email field. How do I configure this?\",\n      \"expected_output\": \"Uses autobind or models.<T>.model in gqlgen.yml to map the GraphQL User type to domain.User. Must NOT instruct editing models_gen.go directly.\",\n      \"assertions\": [\n        \"Uses gqlgen.yml configuration (autobind or models.<T>.model) to bind the existing struct\",\n        \"Does NOT suggest editing models_gen.go or generated.go directly\",\n        \"Shows the correct gqlgen.yml syntax for either autobind or models.<T>.model\",\n        \"Mentions that generated files are overwritten on next go generate\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"In a graph-gophers/graphql-go resolver, I need to return a user profile that includes a nullable bio field. The SDL has: bio: String. Write the Go struct field or return type for bio.\",\n      \"expected_output\": \"Uses *string (pointer to string) for the nullable String field. Non-pointer string would imply non-null in graph-gophers.\",\n      \"assertions\": [\n        \"Uses *string (pointer) for the nullable bio field, not plain string\",\n        \"Explains that non-pointer = non-null and pointer = nullable in graph-gophers type mapping\",\n        \"Does not use sql.NullString or other DB-specific nullable types for the GraphQL resolver layer\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"My gqlgen resolver calls a database function that can return sql.ErrNoRows or other database errors. Write the resolver for GetUser(id: ID!): User! that handles these cases correctly for clients.\",\n      \"expected_output\": \"Resolver wraps or transforms errors before returning them. Uses ErrorPresenter or gqlerror.Error with extensions code. Must NOT return raw sql.ErrNoRows to the client.\",\n      \"assertions\": [\n        \"Does NOT return raw sql.ErrNoRows or other internal errors directly to the client\",\n        \"Translates sql.ErrNoRows to a GraphQL error with a meaningful message or extension code (e.g. NOT_FOUND)\",\n        \"Uses gqlerror.Error or gqlerror.Errorf to format the client error\",\n        \"Mentions the ErrorPresenter as the right place to centralize error sanitization\",\n        \"Wraps internal errors so they don't expose SQL messages to API consumers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"Design a GraphQL mutation for creating a user where the email is always required but the bio is optional. The mutation should handle validation errors gracefully without returning HTTP errors.\",\n      \"expected_output\": \"Uses a mutation envelope payload type with a user field and an errors field. Email is String! (non-null) in input; bio is String (nullable). Errors are returned in the payload, not as GraphQL top-level errors.\",\n      \"assertions\": [\n        \"Input type has email: String! (non-null) for required email\",\n        \"Input type has bio: String (nullable, no !) for optional bio\",\n        \"Mutation returns a payload envelope type with both user and errors fields\",\n        \"Validation errors are returned inside the payload errors field, not as GraphQL top-level errors\",\n        \"The payload errors field uses a non-null list type like [UserError!]! or similar\",\n        \"Does NOT use HTTP 400/422 errors for validation — uses the GraphQL payload pattern\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"Write a gqlgen subscription resolver for order status updates. Use an in-memory pub/sub broker. The resolver should subscribe to a topic named after the order ID and stream status events.\",\n      \"expected_output\": \"Resolver subscribes to the pub/sub topic ONCE before launching the goroutine, not inside the goroutine loop. The channel is closed when the context is cancelled.\",\n      \"assertions\": [\n        \"Calls the pub/sub Subscribe (or equivalent) method BEFORE the go func() call, not inside the goroutine\",\n        \"Does NOT call Subscribe() inside the for-select loop body (which would create a new subscription per iteration)\",\n        \"Uses defer close(ch) to signal iteration end\",\n        \"Uses select with ctx.Done() to handle client disconnect\",\n        \"The subscription/topic handle is captured in a variable before the goroutine starts\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"In a graph-gophers/graphql-go project, add a search query: `search(query: String!, first: Int, after: ID): [User!]!`. The first and after arguments are optional pagination parameters. Write the Go args struct for this resolver.\",\n      \"expected_output\": \"Uses *int32 (not *int or int32) for the nullable Int argument 'first', and graphql.ID (or *graphql.ID) for the after argument. Nullable args use pointer types.\",\n      \"assertions\": [\n        \"Uses *int32 (pointer to int32) for the optional 'first: Int' argument — NOT *int or int32\",\n        \"Uses graphql.ID or *graphql.ID for the 'after: ID' argument\",\n        \"Optional arguments are represented as pointers to allow nil (absent) values\",\n        \"Does NOT use plain int or *int for an Int field in graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"Write a dataloadgen batch function for a gqlgen project that fetches all posts for a list of user IDs. Each user can have zero or more posts.\",\n      \"expected_output\": \"Batch function returns [][]*domain.Post (a slice of post slices, one per user ID), not []*domain.Post. The outer slice length must match the input IDs length.\",\n      \"assertions\": [\n        \"Batch function return type is [][]*domain.Post (2D slice) or equivalent — NOT []*domain.Post\",\n        \"The outer slice has exactly one element per input user ID (same length as the ids parameter)\",\n        \"Handles users with zero posts by including an empty slice (not nil or missing entry)\",\n        \"Does NOT return a flat []*domain.Post that collapses all posts into one slice\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"Add OpenTelemetry tracing to a graph-gophers/graphql-go server. Show the import statement and the ParseSchema call with the tracer configured.\",\n      \"expected_output\": \"Uses github.com/graph-gophers/graphql-go/trace/otel import and otel.DefaultTracer() — not an external otelgraphql package. Passed as graphql.Tracer() option.\",\n      \"assertions\": [\n        \"Imports github.com/graph-gophers/graphql-go/trace/otel (the bundled tracer package)\",\n        \"Uses otel.DefaultTracer() to construct the tracer — NOT otelgraphql.DefaultTracer() or similar hallucinated package\",\n        \"Passes the tracer as graphql.Tracer(otel.DefaultTracer()) option to MustParseSchema or ParseSchema\",\n        \"Does NOT import an external third-party otelgraphql package that does not exist in graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"We're building a federated GraphQL system with gqlgen. The User service owns the User type and must be resolvable by its id field from other services. What configuration and code changes are needed?\",\n      \"expected_output\": \"Configures federation.version: 2 in gqlgen.yml, adds @key(fields: 'id') to the User schema type, and implements a FindUserByID entity resolver. Mentions Apollo Router or Cosmo as the gateway.\",\n      \"assertions\": [\n        \"Adds federation block with version: 2 to gqlgen.yml\",\n        \"Adds @key(fields: \\\"id\\\") directive to the User type in the SDL schema\",\n        \"Implements or mentions the FindUserByID entity resolver (generated by gqlgen's federation support)\",\n        \"Imports or links the Apollo Federation v2 spec URL in the schema extend block\",\n        \"Does NOT describe a manual federation approach without gqlgen.yml config\"\n      ],\n      \"files\": []\n    }\n  ]\n}\n\nArchive v0.0.2: 6 files, 17785 bytes\n\nFiles: evals/evals.json (11719b), references/gqlgen.md (7448b), references/graphql-go.md (7166b), references/testing.md (5023b), SKILL.md (12561b), _meta.json (133b)\n\nFile v0.0.2:SKILL.md\n\n---\nname: golang-graphql\ndescription: \"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`.\"\nuser-invocable: false\nlicense: MIT\ncompatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.\nmetadata:\n  author: samber\n  version: \"0.0.2\"\n  openclaw:\n    emoji: \"🔮\"\n    homepage: https://github.com/samber/cc-skills-golang\n    requires:\n      bins:\n        - go\n    install: []\n    skill-library-version: \"0.17.89\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs Bash(curl:*)\n---\n\n**Persona:** You are a Go GraphQL engineer. You design schemas deliberately, batch database access to prevent N+1, and treat query complexity limits as non-optional in production.\n\n**Modes:**\n\n- **Build mode** — generating new schemas, resolvers, or server setup: follow the skill's sequential instructions; launch a background agent to grep for existing resolver patterns and naming conventions before generating new code.\n- **Review mode** — auditing a GraphQL codebase or PR: use a sub-agent to scan for N+1 resolver patterns, missing complexity caps, global DataLoaders, and introspection enabled in production, in parallel with reading the business logic.\n\n> **Community default.** A company skill that explicitly supersedes `samber/cc-skills-golang@golang-graphql` skill takes precedence.\n\n# Go GraphQL Best Practices\n\nBoth major libraries are schema-first: write SDL (`.graphql` files), bind Go resolvers. Choose based on project size and team preferences.\n\nThis skill is not exhaustive. Refer to each library's official documentation and code examples for current API signatures. Context7 can help as a discoverability platform.\n\n## Library Choice\n\n| Library | Approach | Type safety | Build step | Best for |\n| --- | --- | --- | --- | --- |\n| `github.com/99designs/gqlgen` | Codegen | Compile-time | `go generate` | Large schemas, federation, strict types |\n| `github.com/graph-gophers/graphql-go` | Reflection | Parse-time | None | Simple schemas, fast iteration |\n| `github.com/graphql-go/graphql` | Code-first | Runtime | None | **Avoid** — verbose, no SDL |\n\nPick **gqlgen** when: Apollo Federation is required, schema is large (100+ types), or the team wants generated stubs and zero reflection overhead.\n\nPick **graph-gophers** when: schema is small/medium, the build pipeline should stay simple, or a dynamic schema is needed.\n\nFor deep-dive on each library, see [gqlgen reference](./references/gqlgen.md) and [graphql-go reference](./references/graphql-go.md).\n\n## Schema Design\n\n```graphql\n# ✓ Good — explicit nullability; ID scalar for opaque identifiers\ntype User {\n  id: ID!\n  email: String! # non-null: the server can always return this\n  bio: String # nullable: may be unset\n  posts(first: Int = 10, after: String): PostConnection!\n}\n\n# ✗ Bad — Int ID leaks implementation details, breaks client caching\ntype Post {\n  id: Int!\n}\n```\n\n**Nullability rule:** mark a field `!` only when the server can _always_ return a value. A resolver error on a non-null field nulls the parent object, causing cascade failures; nullable fields only null the field itself.\n\n**Pagination:** use Relay cursor connections (`Connection`/`Edge`/`PageInfo`) for list fields. Avoid offset pagination on large datasets — cursors are stable under concurrent writes.\n\n**Mutations:** wrap results in an envelope type so clients receive business errors alongside partial results without polluting the GraphQL `errors` array:\n\n```graphql\ntype CreateUserPayload {\n  user: User\n  errors: [UserError!]!\n}\n```\n\n## Resolver Patterns\n\nKeep resolvers thin — they translate GraphQL inputs to domain calls and domain responses to GraphQL outputs.\n\n```go\n// ✓ Good — resolver delegates to service layer\nfunc (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error) {\n    user, err := r.userService.Create(ctx, input.Email, input.Name)\n    if err != nil {\n        return nil, formatError(err)\n    }\n    return &model.CreateUserPayload{User: toGQLUser(user)}, nil\n}\n\n// ✗ Bad — SQL in resolver, no separation of concerns\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {\n    row := r.db.QueryRowContext(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    // ...\n}\n```\n\nUse per-type resolver structs (`userResolver`, `postResolver`) rather than one monolithic resolver for all fields.\n\n## N+1 Prevention (DataLoaders)\n\nEach `User.posts` resolver fires a SQL query per user without batching — O(n) DB calls for n users. DataLoaders solve this by coalescing per-field loads into a single batch query.\n\n**Critical rule: DataLoaders MUST be created per-request in HTTP middleware, never globally.** A global DataLoader caches across requests — stale data, potential cross-user data leakage.\n\n```go\n// ✓ Good — per-request DataLoader in middleware\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: newPostsByUserIDLoader(r.Context(), db),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// ✗ Bad — global DataLoader shared across all requests\nvar globalLoader = newPostsByUserIDLoader(context.Background(), db)\n```\n\nIn gqlgen, mark batched fields with `resolver: true` in `gqlgen.yml` to force a dedicated resolver method. See [gqlgen reference](./references/gqlgen.md) for full DataLoader wiring.\n\n## Authentication and Authorization\n\nTwo-layer model:\n\n1. **HTTP middleware** — extract and validate tokens, stash identity in `context.Context`.\n2. **Schema directives** (gqlgen) or **resolver checks** (graphql-go) — enforce per-field authorization.\n\n```go\n// HTTP middleware layer (both libraries)\nfunc AuthMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        token := r.Header.Get(\"Authorization\")\n        user, err := validateToken(token)\n        if err != nil {\n            http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n            return\n        }\n        ctx := context.WithValue(r.Context(), userKey, user)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n```\n\nIn gqlgen, use `@hasRole` schema directives for field-level authorization — authorization policy lives in the schema, not scattered across resolvers. See [gqlgen reference](./references/gqlgen.md).\n\n## Error Handling\n\nNever return raw internal errors — they leak SQL messages, stack traces, or service internals to clients.\n\n```go\n// gqlgen — custom ErrorPresenter strips internal details\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr // already formatted\n    }\n    // log internal err here\n    return gqlerror.Errorf(\"internal error\") // safe client message\n})\n\n// Add extension codes for client-side error handling\nreturn nil, &gqlerror.Error{\n    Message: \"user not found\",\n    Extensions: map[string]any{\"code\": \"NOT_FOUND\"},\n}\n```\n\nFor graph-gophers, implement the `ResolverError` interface to attach `Extensions()`. See [graphql-go reference](./references/graphql-go.md).\n\nUse `graphql.AddError(ctx, err)` in gqlgen for non-fatal field errors where the resolver can still return partial data.\n\nFor error wrapping patterns, see the `samber/cc-skills-golang@golang-error-handling` skill.\n\n## Subscriptions\n\nSubscriptions use long-lived WebSocket connections. The critical discipline: **always respect context cancellation** — a leaked goroutine per disconnected client exhausts resources silently.\n\n```go\n// ✓ Good — closes channel when client disconnects\nfunc (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {\n    ch := make(chan *model.Message, 1)\n    sub := r.pubsub.Subscribe(room) // subscribe once before the goroutine\n    go func() {\n        defer close(ch) // always close; signals iteration to stop\n        for {\n            select {\n            case <-ctx.Done():\n                return // client disconnected\n            case msg := <-sub:\n                ch <- msg\n            }\n        }\n    }()\n    return ch, nil\n}\n\n// ✗ Bad — goroutine leaks forever when client disconnects\nfunc (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {\n    ch := make(chan *model.Message, 1)\n    go func() {\n        for msg := range r.pubsub.Subscribe(room) {\n            ch <- msg // blocks forever after client gone\n        }\n    }()\n    return ch, nil\n}\n```\n\n## Performance and Safety\n\nProduction GraphQL servers require explicit limits. Without them, a single deeply nested query exhausts CPU and memory.\n\n```go\n// gqlgen — wire these into every production handler\nsrv := handler.NewDefaultServer(es)\nsrv.Use(extension.FixedComplexityLimit(200)) // max cost per query\n\n// Gate introspection — only in non-production environments\nif os.Getenv(\"ENV\") != \"production\" {\n    srv.Use(extension.Introspection{})\n}\n```\n\nFor graph-gophers: `graphql.MaxDepth(10)` and `graphql.MaxParallelism(10)` options at `ParseSchema` time.\n\n**Query allow-listing:** in production, consider persisted queries (gqlgen APQ extension) to reject arbitrary query strings.\n\n## Common Mistakes\n\n| Mistake | Why it matters | Fix |\n| --- | --- | --- |\n| N+1 queries in child resolvers | One SQL per parent row → O(n) DB calls | Use per-request DataLoader |\n| Global DataLoader | Cross-request cache — stale data, data leaks | Create DataLoader in request middleware |\n| Editing `models_gen.go` directly | Next `go generate` wipes hand edits | Use `autobind` or `models.<T>.model` in `gqlgen.yml` |\n| Forgetting `go generate` after schema change | Resolver interface mismatch at compile time | Re-run `go run github.com/99designs/gqlgen generate` |\n| `int` field in graph-gophers resolver | Library requires `int32` for `Int` scalar | Use `int32` (or `float64` for `Float`) |\n| Introspection enabled in production | Exposes full schema to attackers | Gate with `ENV` check |\n| No complexity cap | Deeply nested query → CPU/memory DoS | `extension.FixedComplexityLimit(N)` |\n| Leaking DB errors from resolvers | Exposes SQL internals to clients | Wrap in `ErrorPresenter` / `ResolverError` |\n| Subscription goroutine leak | Client disconnect → goroutine runs forever | `defer close(ch)` + `select ctx.Done()` |\n| Nullable field for always-required data | Clients must null-check everywhere | Mark `!` in schema; return error from resolver |\n\n## Deep Dives\n\n- **[gqlgen reference](./references/gqlgen.md)** — codegen workflow, `gqlgen.yml`, DataLoaders, Federation v2, directives\n- **[graphql-go reference](./references/graphql-go.md)** — reflection resolver model, type mapping, tracing\n- **[Testing](./references/testing.md)** — gqlgen client harness, gqltesting, httptest patterns\n\n## Cross-References\n\n- → See `samber/cc-skills-golang@golang-context` skill for context propagation in resolvers and subscriptions\n- → See `samber/cc-skills-golang@golang-error-handling` skill for error wrapping and sentinel patterns\n- → See `samber/cc-skills-golang@golang-testing` skill for table-driven and integration test patterns\n- → See `samber/cc-skills-golang@golang-observability` skill for tracing and metrics in resolvers\n- → See `samber/cc-skills-golang@golang-security` skill for input validation and injection prevention\n- → See `samber/cc-skills-golang@golang-database` skill for N+1 query patterns and DataLoader database batching\n\n## References\n\n- [gqlgen](https://github.com/99designs/gqlgen)\n- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)\n- [Relay cursor connections spec](https://relay.dev/graphql/connections.htm)\n\nIf you encounter a bug or unexpected behavior in gqlgen, open an issue at https://github.com/99designs/gqlgen/issues.\n\nIf you encounter a bug or unexpected behavior in graph-gophers/graphql-go, open an issue at https://github.com/graph-gophers/graphql-go/issues.\n\nFile v0.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn72rhnkwjfeex9wr1n7y24qa983cjn3\",\n  \"slug\": \"golang-graphql\",\n  \"version\": \"0.0.2\",\n  \"publishedAt\": 1777741125549\n}\n\nFile v0.0.2:references/gqlgen.md\n\n# gqlgen Reference\n\ngqlgen is a schema-first, code-generation library. Write SDL, run `go generate`, fill in resolver bodies.\n\n## Project Setup\n\n```bash\n# Bootstrap a new project\ngo run github.com/99designs/gqlgen init\n\n# Pin the tool in tools.go so go mod keeps it\n```\n\n```go\n//go:build tools\n// +build tools\n\npackage tools\n\nimport _ \"github.com/99designs/gqlgen\"\n```\n\n```bash\n# Regenerate after every schema change\ngo run github.com/99designs/gqlgen generate\n```\n\nNever hand-edit generated files (`generated.go`, `models_gen.go`) — `generate` overwrites them.\n\n## gqlgen.yml\n\n```yaml\nschema:\n  - graph/schema/*.graphql\n\nexec:\n  filename: graph/generated.go\n  package: graph\n\nmodel:\n  filename: graph/model/models_gen.go\n  package: model\n\nresolver:\n  layout: follow-schema # one resolvers file per schema file\n  dir: graph\n  package: graph\n  filename_template: \"{name}.resolvers.go\"\n\nautobind:\n  - github.com/me/app/internal/domain # reuse existing structs\n\nmodels:\n  # ID: graphql.IntID  # legacy only — use opaque string IDs for new schemas\n  User:\n    model: github.com/me/app/internal/domain.User\n    fields:\n      posts:\n        resolver: true # force a custom resolver (required for DataLoader fields)\n\nomit_slice_element_pointers: true\nstruct_fields_always_pointers: false\nresolvers_always_return_pointers: true\n```\n\nKey knobs:\n\n- `autobind` — maps Go structs to GraphQL types; fields must match by name (case-insensitive)\n- `models.<T>.model` — override which Go type backs a GraphQL type\n- `fields.<f>.resolver: true` — force a custom resolver instead of struct field access; required for any field that should batch via DataLoader\n- `struct_fields_always_pointers` / `resolvers_always_return_pointers` — controls `*T` vs `T` in generated signatures; match your domain model conventions\n\n## Resolver Structure\n\nThe generated `Config` holds a `Resolvers` field of the generated interface. You implement it:\n\n```go\n// graph/resolver.go — you own this file, not generated\ntype Resolver struct {\n    db          *sql.DB\n    userService *service.UserService\n    loaders     *dataloaders.Loaders // injected per-request\n}\n```\n\nPer-type resolvers implement the generated interface split by GraphQL type:\n\n```go\ntype queryResolver struct{ *Resolver }\ntype mutationResolver struct{ *Resolver }\ntype userResolver struct{ *Resolver }\n\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { ... }\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) { ... }\n```\n\n`obj` is the parent object — the entry point for walking the graph.\n\n## DataLoaders (gqlgen)\n\nUse `github.com/vikstrous/dataloadgen` (generics, fast) or `github.com/graph-gophers/dataloader`:\n\n```go\n// Inject per-request via middleware\nfunc Middleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: dataloadgen.NewLoader(func(ctx context.Context, ids []string) ([][]*domain.Post, []error) {\n                return batchPostsByUserID(ctx, db, ids) // returns one []Post per user ID\n            }, dataloadgen.WithWait(1*time.Millisecond)),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// Resolver uses the loader — never the DB directly\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) {\n    return loaders.For(ctx).PostsByUserID.Load(ctx, obj.ID)\n}\n```\n\nSet `wait` to 1–2ms — allows multiple concurrent resolvers to register keys before the batch fires.\n\n## Authentication Directives\n\n```graphql\ndirective @hasRole(role: Role!) on FIELD_DEFINITION\n\ntype Query {\n  adminStats: Stats! @hasRole(role: ADMIN)\n}\n```\n\n```go\n// Implement the directive function\nfunc HasRole(ctx context.Context, obj any, next graphql.Resolver, role model.Role) (any, error) {\n    user := auth.UserFromContext(ctx)\n    if user == nil || user.Role != role {\n        return nil, &gqlerror.Error{\n            Message:    \"access denied\",\n            Extensions: map[string]any{\"code\": \"FORBIDDEN\"},\n        }\n    }\n    return next(ctx)\n}\n\n// Register at server bootstrap\nc := generated.Config{\n    Resolvers: &graph.Resolver{...},\n    Directives: generated.DirectiveRoot{\n        HasRole: HasRole,\n    },\n}\n```\n\n## Middleware Hooks\n\n```go\nsrv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {\n    // log operation name, add trace span\n    return next(ctx)\n})\nsrv.AroundFields(func(ctx context.Context, next graphql.Resolver) (any, error) {\n    // per-field tracing, timing\n    return next(ctx)\n})\n```\n\n## Error Presenter\n\n```go\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr\n    }\n    log.Ctx(ctx).Error(\"resolver error\", \"err\", err)\n    return gqlerror.Errorf(\"internal server error\")\n})\n\nsrv.SetRecoverFunc(func(ctx context.Context, err any) error {\n    log.Ctx(ctx).Error(\"panic in resolver\", \"err\", err)\n    return fmt.Errorf(\"internal server error\")\n})\n```\n\n## Subscriptions\n\n```go\nsrv.AddTransport(transport.Websocket{\n    KeepAlivePingInterval: 10 * time.Second,\n    Upgrader: websocket.Upgrader{\n        // Restrict to your own origin in production; true here is dev-only.\n        CheckOrigin: func(r *http.Request) bool {\n            return r.Header.Get(\"Origin\") == \"https://app.example.com\"\n        },\n    },\n    InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {\n        // auth at connection time\n        token := initPayload.Authorization()\n        user, err := validateToken(token)\n        if err != nil {\n            return ctx, nil, err\n        }\n        return context.WithValue(ctx, userKey, user), &initPayload, nil\n    },\n})\n```\n\ngqlgen supports both `graphql-ws` (legacy) and `graphql-transport-ws` (current) subprotocols.\n\n## File Uploads\n\n```go\nsrv.AddTransport(transport.MultipartForm{\n    MaxUploadSize: 10 << 20, // 10 MB total\n    MaxMemory:     5 << 20,  // 5 MB in memory; rest spills to disk\n})\n```\n\nSchema:\n\n```graphql\nscalar Upload\n\ntype Mutation {\n  uploadAvatar(file: Upload!): User!\n}\n```\n\nResolver receives `graphql.Upload{File io.Reader, Filename string, Size int64, ContentType string}`.\n\n## Apollo Federation v2\n\n`gqlgen.yml`:\n\n```yaml\nfederation:\n  filename: graph/federation.go\n  version: 2\n```\n\nSchema:\n\n```graphql\nextend schema\n  @link(\n    url: \"https://specs.apollo.dev/federation/v2.3\"\n    import: [\"@key\", \"@shareable\", \"@external\"]\n  )\n\ntype User @key(fields: \"id\") {\n  id: ID!\n  name: String!\n}\n```\n\nImplement `FindUserByID` in the generated entity resolver. Works with Apollo Router and Cosmo.\n\n## Production Handler Setup\n\n```go\nsrv := handler.New(es)\nsrv.AddTransport(transport.Options{})\nsrv.AddTransport(transport.GET{})\nsrv.AddTransport(transport.POST{})\nsrv.AddTransport(transport.MultipartForm{MaxUploadSize: 10 << 20, MaxMemory: 5 << 20})\nsrv.AddTransport(transport.Websocket{KeepAlivePingInterval: 10 * time.Second})\n\nsrv.SetQueryCache(lru.New[*ast.QueryDocument](1000))\nif os.Getenv(\"ENV\") != \"production\" {\n    srv.Use(extension.Introspection{})\n}\nsrv.Use(extension.AutomaticPersistedQuery{Cache: lru.New[string](100)})\nsrv.Use(extension.FixedComplexityLimit(200))\n```\n\nFile v0.0.2:references/graphql-go.md\n\n# graph-gophers/graphql-go Reference\n\nSchema-first, reflection-based — no codegen. Write SDL, bind Go resolver structs. Parse-time validation gives a fail-fast contract.\n\n## Setup\n\n```go\nimport (\n    \"github.com/graph-gophers/graphql-go\"\n    \"github.com/graph-gophers/graphql-go/relay\"\n    \"github.com/graph-gophers/graphql-go/trace/otel\"\n)\n\nschema := graphql.MustParseSchema(sdlString, &RootResolver{},\n    graphql.MaxDepth(10),\n    graphql.MaxParallelism(10),\n    graphql.UseFieldResolvers(), // expose exported struct fields without explicit methods\n    graphql.Tracer(otel.DefaultTracer()),\n)\n\nhttp.Handle(\"/graphql\", &relay.Handler{Schema: schema})\n```\n\n`MustParseSchema` panics on invalid SDL or resolver mismatch — catch it at startup, not at request time.\n\n## Resolver Structure\n\nOne exported method per schema field; name match is case-insensitive:\n\n```go\ntype RootResolver struct {\n    db *sql.DB\n}\n\ntype QueryResolver struct {\n    db *sql.DB\n}\n\nfunc (r *RootResolver) Query() *QueryResolver { return &QueryResolver{db: r.db} }\n\n// Args struct for field arguments\nfunc (r *QueryResolver) User(ctx context.Context, args struct{ ID graphql.ID }) (*UserResolver, error) {\n    user, err := r.db.GetUser(ctx, string(args.ID))\n    if err != nil {\n        return nil, err\n    }\n    return &UserResolver{user: user}, nil\n}\n```\n\nReturn resolver wrapper structs, not domain models directly — keeps GraphQL projection separate from persistence.\n\n## Type Mapping\n\n| GraphQL type | Go type | Notes |\n| --- | --- | --- |\n| `ID` | `graphql.ID` | string alias |\n| `Int` | `int32` | **NOT `int`** — mismatch is a parse-time error |\n| `Float` | `float64` |  |\n| `String` | `string` |  |\n| `Boolean` | `bool` |  |\n| `[T]` | `[]*T` or `[]T` |  |\n| Nullable `T` | `*T` | pointer = nullable |\n| Non-null `T!` | `T` | non-pointer |\n| Custom scalar | implement `UnmarshalGraphQL(input any) error` + `MarshalJSON() ([]byte, error)` |  |\n| Enum | typed string alias |  |\n| Input | exported struct with field tags optional |  |\n| Interface/Union | Go interface returned; `ToConcreteType() (*T, bool)` discriminators |  |\n\nCommon mistake: using `int` for an `Int!` field — the parser rejects it with a type mismatch error.\n\n## Nullable vs Non-null Arguments\n\n```go\n// ✓ Good — pointer arg = nullable in schema\nfunc (r *QueryResolver) Users(ctx context.Context, args struct {\n    Role *string // nullable: Role in SDL\n    Limit int32  // non-null: Limit! in SDL\n}) ([]*UserResolver, error) { ... }\n```\n\nForgetting `*` on a nullable argument causes unmarshal failure when clients send `null`.\n\n## Custom Scalar\n\n```go\ntype DateTime struct{ time.Time }\n\nfunc (d *DateTime) UnmarshalGraphQL(input any) error {\n    s, ok := input.(string)\n    if !ok {\n        return fmt.Errorf(\"DateTime must be a string\")\n    }\n    t, err := time.Parse(time.RFC3339, s)\n    if err != nil {\n        return err\n    }\n    d.Time = t\n    return nil\n}\n\nfunc (d DateTime) MarshalJSON() ([]byte, error) {\n    return json.Marshal(d.Time.Format(time.RFC3339))\n}\n```\n\n## Interfaces and Unions\n\n```graphql\ninterface Node {\n  id: ID!\n}\nunion SearchResult = User | Post\n```\n\n```go\n// Interface — implement ToUser, ToPost discriminators\ntype SearchResultResolver struct{ result any }\n\nfunc (r *SearchResultResolver) ToUser() (*UserResolver, bool) {\n    u, ok := r.result.(*domain.User)\n    return &UserResolver{u}, ok\n}\n\nfunc (r *SearchResultResolver) ToPost() (*PostResolver, bool) {\n    p, ok := r.result.(*domain.Post)\n    return &PostResolver{p}, ok\n}\n```\n\n## DataLoaders\n\nUse `github.com/graph-gophers/dataloader` per-request:\n\n```go\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loader := dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {\n            ids := make([]string, len(keys))\n            for i, k := range keys {\n                ids[i] = k.String()\n            }\n            posts, err := batchPostsByUserID(ctx, db, ids)\n            // map results back to keys order ...\n            return results\n        })\n        ctx := context.WithValue(r.Context(), postsLoaderKey, loader)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// In resolver\nfunc (r *UserResolver) Posts(ctx context.Context) ([]*PostResolver, error) {\n    thunk := ctx.Value(postsLoaderKey).(*dataloader.Loader).Load(ctx, dataloader.StringKey(r.user.ID))\n    result, err := thunk()\n    // ...\n}\n```\n\n## Error Handling\n\nImplement `ResolverError` to attach structured extensions:\n\n```go\ntype ResolverError interface {\n    error\n    Extensions() map[string]any\n}\n\ntype AppError struct {\n    msg  string\n    code string\n}\n\nfunc (e *AppError) Error() string { return e.msg }\nfunc (e *AppError) Extensions() map[string]any {\n    return map[string]any{\"code\": e.code}\n}\n\n// Usage in resolver\nreturn nil, &AppError{msg: \"user not found\", code: \"NOT_FOUND\"}\n```\n\nPanics in resolvers are caught automatically and converted to GraphQL errors.\n\n## OpenTelemetry Tracing\n\n```go\nimport \"github.com/graph-gophers/graphql-go/trace/otel\"\n\nschema := graphql.MustParseSchema(sdl, &RootResolver{},\n    graphql.Tracer(otel.DefaultTracer()),\n)\n```\n\nEmits spans per request, validation, and field resolution with operation name and field path.\n\n## Subscriptions\n\n```go\nfunc (r *SubscriptionResolver) MessageAdded(ctx context.Context, args struct{ Room string }) <-chan *MessageResolver {\n    ch := make(chan *MessageResolver, 1)\n    go func() {\n        defer close(ch)\n        sub := r.pubsub.Subscribe(args.Room)\n        defer sub.Unsubscribe()\n        for {\n            select {\n            case <-ctx.Done():\n                return\n            case msg := <-sub.Chan():\n                ch <- &MessageResolver{msg: msg}\n            }\n        }\n    }()\n    return ch\n}\n```\n\nWebSocket transport is not bundled — pair with `gorilla/websocket` or use the relay handler with a WebSocket-aware mux.\n\n## Disabling Introspection\n\n```go\nschema := graphql.MustParseSchema(sdl, &RootResolver{},\n    graphql.DisableIntrospection(),\n)\n```\n\n## Testing\n\nUse `gqltesting.RunTests`:\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{ \"user\": { \"name\": \"Alice\", \"email\": \"alice@example.com\" } }`,\n        },\n    })\n}\n```\n\nFor HTTP-level tests, drive `relay.Handler` with `httptest.NewRecorder()`.\n\n## graph-gophers vs gqlgen Summary\n\n| Concern          | graph-gophers         | gqlgen                      |\n| ---------------- | --------------------- | --------------------------- |\n| Type safety      | Parse-time reflection | Compile-time codegen        |\n| Build complexity | None                  | `go generate` step          |\n| Performance      | Slower (reflection)   | Faster (static dispatch)    |\n| Federation       | Manual                | First-class (v2)            |\n| File uploads     | Manual                | Built-in MultipartForm      |\n| Best for         | Small/medium schemas  | Large schemas, strict teams |\n\nFile v0.0.2:references/testing.md\n\n# Testing GraphQL in Go\n\n## gqlgen — Client Harness\n\nThe `github.com/99designs/gqlgen/client` package drives the full stack (directives, middleware, resolvers) via an `http.Handler`:\n\n```go\nfunc TestCreateUser(t *testing.T) {\n    // Build the full handler with real dependencies (use a test DB)\n    srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{\n        Resolvers: &graph.Resolver{\n            DB: testDB,\n        },\n    }))\n\n    c := client.New(srv)\n\n    var resp struct {\n        CreateUser struct {\n            User struct {\n                ID    string\n                Email string\n            }\n            Errors []struct{ Message string }\n        }\n    }\n\n    c.MustPost(`\n        mutation CreateUser($email: String!, $name: String!) {\n            createUser(input: {email: $email, name: $name}) {\n                user { id email }\n                errors { message }\n            }\n        }\n    `, &resp,\n        client.Var(\"email\", \"alice@example.com\"),\n        client.Var(\"name\", \"Alice\"),\n        client.AddHeader(\"Authorization\", \"Bearer test-token\"),\n    )\n\n    require.Empty(t, resp.CreateUser.Errors)\n    require.Equal(t, \"alice@example.com\", resp.CreateUser.User.Email)\n}\n```\n\nFor unit testing individual resolvers, call resolver methods directly with a constructed `Resolver` and a real `context.Context` — no HTTP overhead.\n\n## gqlgen — Testing with DataLoaders\n\nWrap the test server with the DataLoader middleware so resolver tests exercise the full batching path:\n\n```go\nsrv := handler.NewDefaultServer(es)\nh := dataloaders.Middleware(testDB, srv)\n\nc := client.New(h)\n```\n\n## gqlgen — Testing Subscriptions\n\nUse `client.Subscription` to test subscription resolvers:\n\n```go\nsub := c.Subscription(`subscription { messageAdded(room: \"general\") { content } }`)\ndefer sub.Close()\n\n// Trigger an event\npublishMessage(\"general\", \"hello\")\n\nvar event struct{ MessageAdded struct{ Content string } }\nerr := sub.Next(&event)\nrequire.NoError(t, err)\nrequire.Equal(t, \"hello\", event.MessageAdded.Content)\n```\n\n## graph-gophers — gqltesting\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{\"user\":{\"name\":\"Alice\",\"email\":\"alice@example.com\"}}`,\n        },\n        {\n            Schema:        schema,\n            Query:         `{ user(id: \"999\") { name } }`,\n            ExpectedErrors: []*gqlerrors.QueryError{\n                {Message: \"user not found\", Extensions: map[string]any{\"code\": \"NOT_FOUND\"}},\n            },\n        },\n    })\n}\n```\n\nFor HTTP-level tests:\n\n```go\nfunc TestRelayHandler(t *testing.T) {\n    body := `{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}`\n    req := httptest.NewRequest(http.MethodPost, \"/graphql\", strings.NewReader(body))\n    req.Header.Set(\"Content-Type\", \"application/json\")\n    w := httptest.NewRecorder()\n\n    relay.Handler{Schema: schema}.ServeHTTP(w, req)\n\n    require.Equal(t, http.StatusOK, w.Code)\n    require.Contains(t, w.Body.String(), `\"Alice\"`)\n}\n```\n\n## Testing Error Handling\n\nVerify error extensions reach the client:\n\n```go\nvar resp struct {\n    Errors []struct {\n        Message    string\n        Extensions struct{ Code string }\n    }\n}\nc.Post(`{ user(id: \"999\") { name } }`, &resp)\nrequire.Equal(t, \"NOT_FOUND\", resp.Errors[0].Extensions.Code)\n```\n\n## Testing Auth Directives (gqlgen)\n\nTest the directive function directly:\n\n```go\nfunc TestHasRoleDirective(t *testing.T) {\n    ctx := context.WithValue(context.Background(), userKey, &domain.User{Role: \"USER\"})\n    _, err := HasRole(ctx, nil, func(ctx context.Context) (any, error) {\n        return \"ok\", nil\n    }, model.RoleAdmin)\n    require.Error(t, err)\n\n    var gqlErr *gqlerror.Error\n    require.True(t, errors.As(err, &gqlErr))\n    require.Equal(t, \"FORBIDDEN\", gqlErr.Extensions[\"code\"])\n}\n```\n\n## Table-Driven Tests\n\n```go\nfunc TestUserQueries(t *testing.T) {\n    tests := []struct {\n        name     string\n        query    string\n        vars     map[string]any\n        wantCode string\n        wantName string\n    }{\n        {\"existing user\", `query($id:ID!){user(id:$id){name}}`, map[string]any{\"id\": \"1\"}, \"\", \"Alice\"},\n        {\"missing user\", `query($id:ID!){user(id:$id){name}}`, map[string]any{\"id\": \"999\"}, \"NOT_FOUND\", \"\"},\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            var resp struct {\n                User   *struct{ Name string }\n                Errors []struct {\n                    Extensions struct{ Code string }\n                }\n            }\n            c.Post(tt.query, &resp, client.Var(\"id\", tt.vars[\"id\"]))\n            if tt.wantCode != \"\" {\n                require.Equal(t, tt.wantCode, resp.Errors[0].Extensions.Code)\n            } else {\n                require.Equal(t, tt.wantName, resp.User.Name)\n            }\n        })\n    }\n}\n```\n\nFor testing patterns across the codebase, see the `samber/cc-skills-golang@golang-testing` skill.\n\nFile v0.0.2:evals/evals.json\n\n{\n  \"skill_name\": \"golang-graphql\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"I have a gqlgen project with a User type and a Post type. Users have many posts. Write the Go resolver for User.posts. We fetch posts from a PostgreSQL database. The project is set up with a standard gqlgen layout.\",\n      \"expected_output\": \"A resolver that uses a per-request DataLoader (not direct DB calls) to batch-fetch posts by user IDs. Must NOT query the database directly inside the resolver. Must NOT use a global DataLoader. Should use context to access the per-request loader.\",\n      \"assertions\": [\n        \"Uses a DataLoader or batch loader to fetch posts, not a direct db.Query/QueryContext call inside the resolver method\",\n        \"Accesses the DataLoader from context (not a package-level or global variable)\",\n        \"The resolver function signature uses obj *model.User as a parameter to access the parent user's ID\",\n        \"Does not query the database directly inside the Posts resolver body\",\n        \"Mentions that the DataLoader must be injected per-request via HTTP middleware\",\n        \"DataLoader middleware creates a new loader instance per request, not a shared global\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"We have a graph-gophers/graphql-go project. I need to add a resolver that returns the total comment count for a post. The field is declared as `commentCount: Int!` in the SDL. Write the Go resolver method.\",\n      \"expected_output\": \"Resolver method using int32 (not int) as the return type for the Int! scalar field. Must use int32, since graph-gophers requires this specific type.\",\n      \"assertions\": [\n        \"Returns int32 (not int, int64, or uint) for the Int! scalar field\",\n        \"Method signature matches the SDL field name (case-insensitive: CommentCount or commentCount)\",\n        \"Does not return plain Go int — which causes a type mismatch at parse time with graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"Set up a production-ready gqlgen HTTP handler. The app will be deployed publicly. We need to make sure it's safe to expose.\",\n      \"expected_output\": \"Handler setup that gates introspection (disabled or ENV-checked in production) and adds a query complexity limit. Must not leave introspection unconditionally enabled.\",\n      \"assertions\": [\n        \"Introspection is gated — either disabled in production or guarded by an environment variable check\",\n        \"A complexity limit is set using extension.FixedComplexityLimit or equivalent\",\n        \"Does NOT call srv.Use(extension.Introspection{}) unconditionally without an env guard\",\n        \"Uses handler.New or handler.NewDefaultServer from github.com/99designs/gqlgen/handler\",\n        \"Mentions MaxDepth or complexity limiting as a protection against deeply nested queries\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"Implement a messageAdded subscription resolver in gqlgen. Messages are published via an in-memory pub/sub system. The resolver should stream new messages to subscribers in a given room.\",\n      \"expected_output\": \"Subscription resolver that closes the channel on context cancellation (defer close(ch) + ctx.Done() in a select). Must handle client disconnect to avoid goroutine leaks.\",\n      \"assertions\": [\n        \"Uses defer close(ch) to close the output channel when done\",\n        \"Uses a select statement with ctx.Done() to detect client disconnection\",\n        \"Returns a receive-only channel (<-chan *model.Message or similar)\",\n        \"Does not use a plain for-range loop without ctx.Done() check — this would cause a goroutine leak on disconnect\",\n        \"The goroutine terminates when ctx is cancelled\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"I'm using gqlgen and want to customize the User type to reuse my existing domain.User struct instead of having gqlgen generate a new one. The domain struct has an Email field. How do I configure this?\",\n      \"expected_output\": \"Uses autobind or models.<T>.model in gqlgen.yml to map the GraphQL User type to domain.User. Must NOT instruct editing models_gen.go directly.\",\n      \"assertions\": [\n        \"Uses gqlgen.yml configuration (autobind or models.<T>.model) to bind the existing struct\",\n        \"Does NOT suggest editing models_gen.go or generated.go directly\",\n        \"Shows the correct gqlgen.yml syntax for either autobind or models.<T>.model\",\n        \"Mentions that generated files are overwritten on next go generate\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"In a graph-gophers/graphql-go resolver, I need to return a user profile that includes a nullable bio field. The SDL has: bio: String. Write the Go struct field or return type for bio.\",\n      \"expected_output\": \"Uses *string (pointer to string) for the nullable String field. Non-pointer string would imply non-null in graph-gophers.\",\n      \"assertions\": [\n        \"Uses *string (pointer) for the nullable bio field, not plain string\",\n        \"Explains that non-pointer = non-null and pointer = nullable in graph-gophers type mapping\",\n        \"Does not use sql.NullString or other DB-specific nullable types for the GraphQL resolver layer\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"My gqlgen resolver calls a database function that can return sql.ErrNoRows or other database errors. Write the resolver for GetUser(id: ID!): User! that handles these cases correctly for clients.\",\n      \"expected_output\": \"Resolver wraps or transforms errors before returning them. Uses ErrorPresenter or gqlerror.Error with extensions code. Must NOT return raw sql.ErrNoRows to the client.\",\n      \"assertions\": [\n        \"Does NOT return raw sql.ErrNoRows or other internal errors directly to the client\",\n        \"Translates sql.ErrNoRows to a GraphQL error with a meaningful message or extension code (e.g. NOT_FOUND)\",\n        \"Uses gqlerror.Error or gqlerror.Errorf to format the client error\",\n        \"Mentions the ErrorPresenter as the right place to centralize error sanitization\",\n        \"Wraps internal errors so they don't expose SQL messages to API consumers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"Design a GraphQL mutation for creating a user where the email is always required but the bio is optional. The mutation should handle validation errors gracefully without returning HTTP errors.\",\n      \"expected_output\": \"Uses a mutation envelope payload type with a user field and an errors field. Email is String! (non-null) in input; bio is String (nullable). Errors are returned in the payload, not as GraphQL top-level errors.\",\n      \"assertions\": [\n        \"Input type has email: String! (non-null) for required email\",\n        \"Input type has bio: String (nullable, no !) for optional bio\",\n        \"Mutation returns a payload envelope type with both user and errors fields\",\n        \"Validation errors are returned inside the payload errors field, not as GraphQL top-level errors\",\n        \"The payload errors field uses a non-null list type like [UserError!]! or similar\",\n        \"Does NOT use HTTP 400/422 errors for validation — uses the GraphQL payload pattern\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"Write a gqlgen subscription resolver for order status updates. Use an in-memory pub/sub broker. The resolver should subscribe to a topic named after the order ID and stream status events.\",\n      \"expected_output\": \"Resolver subscribes to the pub/sub topic ONCE before launching the goroutine, not inside the goroutine loop. The channel is closed when the context is cancelled.\",\n      \"assertions\": [\n        \"Calls the pub/sub Subscribe (or equivalent) method BEFORE the go func() call, not inside the goroutine\",\n        \"Does NOT call Subscribe() inside the for-select loop body (which would create a new subscription per iteration)\",\n        \"Uses defer close(ch) to signal iteration end\",\n        \"Uses select with ctx.Done() to handle client disconnect\",\n        \"The subscription/topic handle is captured in a variable before the goroutine starts\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"In a graph-gophers/graphql-go project, add a search query: `search(query: String!, first: Int, after: ID): [User!]!`. The first and after arguments are optional pagination parameters. Write the Go args struct for this resolver.\",\n      \"expected_output\": \"Uses *int32 (not *int or int32) for the nullable Int argument 'first', and graphql.ID (or *graphql.ID) for the after argument. Nullable args use pointer types.\",\n      \"assertions\": [\n        \"Uses *int32 (pointer to int32) for the optional 'first: Int' argument — NOT *int or int32\",\n        \"Uses graphql.ID or *graphql.ID for the 'after: ID' argument\",\n        \"Optional arguments are represented as pointers to allow nil (absent) values\",\n        \"Does NOT use plain int or *int for an Int field in graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"Write a dataloadgen batch function for a gqlgen project that fetches all posts for a list of user IDs. Each user can have zero or more posts.\",\n      \"expected_output\": \"Batch function returns [][]*domain.Post (a slice of post slices, one per user ID), not []*domain.Post. The outer slice length must match the input IDs length.\",\n      \"assertions\": [\n        \"Batch function return type is [][]*domain.Post (2D slice) or equivalent — NOT []*domain.Post\",\n        \"The outer slice has exactly one element per input user ID (same length as the ids parameter)\",\n        \"Handles users with zero posts by including an empty slice (not nil or missing entry)\",\n        \"Does NOT return a flat []*domain.Post that collapses all posts into one slice\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"Add OpenTelemetry tracing to a graph-gophers/graphql-go server. Show the import statement and the ParseSchema call with the tracer configured.\",\n      \"expected_output\": \"Uses github.com/graph-gophers/graphql-go/trace/otel import and otel.DefaultTracer() — not an external otelgraphql package. Passed as graphql.Tracer() option.\",\n      \"assertions\": [\n        \"Imports github.com/graph-gophers/graphql-go/trace/otel (the bundled tracer package)\",\n        \"Uses otel.DefaultTracer() to construct the tracer — NOT otelgraphql.DefaultTracer() or similar hallucinated package\",\n        \"Passes the tracer as graphql.Tracer(otel.DefaultTracer()) option to MustParseSchema or ParseSchema\",\n        \"Does NOT import an external third-party otelgraphql package that does not exist in graph-gophers\"\n      ],\n      \"files\": []\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"We're building a federated GraphQL system with gqlgen. The User service owns the User type and must be resolvable by its id field from other services. What configuration and code changes are needed?\",\n      \"expected_output\": \"Configures federation.version: 2 in gqlgen.yml, adds @key(fields: 'id') to the User schema type, and implements a FindUserByID entity resolver. Mentions Apollo Router or Cosmo as the gateway.\",\n      \"assertions\": [\n        \"Adds federation block with version: 2 to gqlgen.yml\",\n        \"Adds @key(fields: \\\"id\\\") directive to the User type in the SDL schema\",\n        \"Implements or mentions the FindUserByID entity resolver (generated by gqlgen's federation support)\",\n        \"Imports or links the Apollo Federation v2 spec URL in the schema extend block\",\n        \"Does NOT describe a manual federation approach without gqlgen.yml config\"\n      ],\n      \"files\": []\n    }\n  ]\n}","readmeExcerpt":"Skill: golang-graphql Owner: samber Summary: Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports github.com/99designs/gqlgen or github.com/graph-gophers/graphql-go. Tags: latest:0.2.0 Version history: v0.2.0 | 2026-08-2","codeSnippets":[],"executableExamples":[{"language":"graphql","snippet":"# ✓ Good — explicit nullability; ID scalar for opaque identifiers\ntype User {\n  id: ID!\n  email: String! # non-null: the server can always return this\n  bio: String # nullable: may be unset\n  posts(first: Int = 10, after: String): PostConnection!\n}\n\n# ✗ Bad — Int ID leaks implementation details, breaks client caching\ntype Post {\n  id: Int!\n}"},{"language":"graphql","snippet":"type CreateUserPayload {\n  user: User\n  errors: [UserError!]!\n}"},{"language":"go","snippet":"// ✓ Good — resolver delegates to service layer\nfunc (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error) {\n    user, err := r.userService.Create(ctx, input.Email, input.Name)\n    if err != nil {\n        return nil, formatError(err)\n    }\n    return &model.CreateUserPayload{User: toGQLUser(user)}, nil\n}\n\n// ✗ Bad — SQL in resolver, no separation of concerns\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {\n    row := r.db.QueryRowContext(ctx, \"SELECT * FROM users WHERE id = $1\", id)\n    // ...\n}"},{"language":"go","snippet":"// ✓ Good — per-request DataLoader in middleware\nfunc DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: newPostsByUserIDLoader(r.Context(), db),\n        }\n        ctx := context.WithValue(r.Context(), loadersKey, loaders)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}\n\n// ✗ Bad — global DataLoader shared across all requests\nvar globalLoader = newPostsByUserIDLoader(context.Background(), db)"},{"language":"go","snippet":"// HTTP middleware layer (both libraries)\nfunc AuthMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        token := r.Header.Get(\"Authorization\")\n        user, err := validateToken(token)\n        if err != nil {\n            http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n            return\n        }\n        ctx := context.WithValue(r.Context(), userKey, user)\n        next.ServeHTTP(w, r.WithContext(ctx))\n    })\n}"},{"language":"go","snippet":"// gqlgen — custom ErrorPresenter strips internal details\nsrv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {\n    var gqlErr *gqlerror.Error\n    if errors.As(err, &gqlErr) {\n        return gqlErr // already formatted\n    }\n    // log internal err here\n    return gqlerror.Errorf(\"internal error\") // safe client message\n})\n\n// Add extension codes for client-side error handling\nreturn nil, &gqlerror.Error{\n    Message: \"user not found\",\n    Extensions: map[string]any{\"code\": \"NOT_FOUND\"},\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: golang-graphql\ndescription: \"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`.\"\nuser-invocable: false\nlicense: MIT\ncompatibility: Designed for Claude Code, Codex or similar harness, and for projects using Golang.\nmetadata:\n  author: samber\n  version: \"0.2.0\"\n  openclaw:\n    emoji: \"🔮\"\n    homepage: https://github.com/samber/cc-skills-golang\n    requires:\n      bins:\n        - go\n    install: []\n    skill-library-version: \"0.17.89\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs Bash(curl:*) Bash(godig:*) Bash(gopls:*) LSP mcp__gopls__*\npaths:\n  - \"**/*.go\"\n---\n\n**Persona:** You are a Go GraphQL engineer. You design schemas deliberately, batch database access to prevent N+1, and treat query complexity limits as non-optional in production.\n\n**Modes:**\n\n- **Build mode** — generating new schemas, resolvers, or server setup: follow the skill's sequential instructions; launch a background agent to grep for existing resolver patterns and naming conventions before generating new code.\n- **Review mode** — auditing a GraphQL codebase or PR: use a sub-agent to scan for N+1 resolver patterns, missing complexity caps, global DataLoaders, and introspection enabled in production, in parallel with reading the business logic.\n\n> **Community default.** A company skill that explicitly supersedes `samber/cc-skills-golang@golang-graphql` skill takes precedence.\n\n# Go GraphQL Best Practices\n\nBoth major libraries are schema-first: write SDL (`.graphql` files), bind Go resolvers. Choose based on project size and team preferences.\n\nThis skill is not exhaustive. Refer to each library's official documentation and code examples for current API signatures. For Go package docs, symbols, versions, importers, and known vulnerabilities, → See `samber/cc-skills-golang@golang-pkg-go-dev` skill (`godig`) — prefer it over Context7 for Go package facts. To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See `samber/cc-skills-golang@golang-gopls` skill (`gopls`). Context7 remains a fallback for docs not indexed on pkg.go.dev.\n\n## Library Choice\n\n| Library | Approach | Type safety | Build step | Best for |\n| --- | --- | --- | --- | --- |\n| `github.com/99designs/gqlgen` | Codegen | Compile-time | `go generate` | Large schemas, federation, strict types |\n| `github.com/graph-gophers/graphql-go` | Reflection | Parse-time | None | Simple schemas, fast iteration |\n| `github.com/graphql-go/graphql` | Code-first | Runtime | None | **Avoid** — verbose, no SDL |\n\nPick **gqlgen** when: Apollo Federation is required, schema is large (100+ types), or the team wants g"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn72rhnkwjfeex9wr1n7y24qa983cjn3\",\n  \"slug\": \"golang-graphql\",\n  \"version\": \"0.2.0\",\n  \"publishedAt\": 1787318461360\n}"},{"path":"references/gqlgen.md","content":"# gqlgen Reference\n\ngqlgen is a schema-first, code-generation library. Write SDL, run `go generate`, fill in resolver bodies.\n\n## Project Setup\n\n```bash\n# Bootstrap a new project\ngo run github.com/99designs/gqlgen init\n\n# Pin the tool in go.mod for reproducible generation (Go 1.24+)\ngo get -tool github.com/99designs/gqlgen@latest\n```\n\nFor Go <1.24 modules, use the legacy `tools.go` blank-import workaround instead.\n\n```bash\n# Regenerate after every schema change\ngo tool gqlgen generate\n```\n\nNever hand-edit generated files (`generated.go`, `models_gen.go`) — `generate` overwrites them.\n\n## gqlgen.yml\n\n```yaml\nschema:\n  - graph/schema/*.graphql\n\nexec:\n  filename: graph/generated.go\n  package: graph\n\nmodel:\n  filename: graph/model/models_gen.go\n  package: model\n\nresolver:\n  layout: follow-schema # one resolvers file per schema file\n  dir: graph\n  package: graph\n  filename_template: \"{name}.resolvers.go\"\n\nautobind:\n  - github.com/me/app/internal/domain # reuse existing structs\n\nmodels:\n  # ID: graphql.IntID  # legacy only — use opaque string IDs for new schemas\n  User:\n    model: github.com/me/app/internal/domain.User\n    fields:\n      posts:\n        resolver: true # force a custom resolver (required for DataLoader fields)\n\nomit_slice_element_pointers: true\nstruct_fields_always_pointers: false\nresolvers_always_return_pointers: true\n```\n\nKey knobs:\n\n- `autobind` — maps Go structs to GraphQL types; fields must match by name (case-insensitive)\n- `models.<T>.model` — override which Go type backs a GraphQL type\n- `fields.<f>.resolver: true` — force a custom resolver instead of struct field access; required for any field that should batch via DataLoader\n- `struct_fields_always_pointers` / `resolvers_always_return_pointers` — controls `*T` vs `T` in generated signatures; match your domain model conventions\n\n## Resolver Structure\n\nThe generated `Config` holds a `Resolvers` field of the generated interface. You implement it:\n\n```go\n// graph/resolver.go — you own this file, not generated\ntype Resolver struct {\n    db          *sql.DB\n    userService *service.UserService\n    loaders     *dataloaders.Loaders // injected per-request\n}\n```\n\nPer-type resolvers implement the generated interface split by GraphQL type:\n\n```go\ntype queryResolver struct{ *Resolver }\ntype mutationResolver struct{ *Resolver }\ntype userResolver struct{ *Resolver }\n\nfunc (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { ... }\nfunc (r *userResolver) Posts(ctx context.Context, obj *model.User) ([]*model.Post, error) { ... }\n```\n\n`obj` is the parent object — the entry point for walking the graph.\n\n## DataLoaders (gqlgen)\n\nUse `github.com/vikstrous/dataloadgen` (generics, fast) or `github.com/graph-gophers/dataloader`:\n\n```go\n// Inject per-request via middleware\nfunc Middleware(db *sql.DB, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        loaders := &Loaders{\n            PostsByUserID: dataloadgen.New"},{"path":"references/graphql-go.md","content":"# graph-gophers/graphql-go Reference\n\nSchema-first, reflection-based — no codegen. Write SDL, bind Go resolver structs. Parse-time validation gives a fail-fast contract.\n\n## Setup\n\n```go\nimport (\n    \"github.com/graph-gophers/graphql-go\"\n    \"github.com/graph-gophers/graphql-go/relay\"\n    \"github.com/graph-gophers/graphql-go/trace/otel\"\n)\n\nschema := graphql.MustParseSchema(sdlString, &RootResolver{},\n    graphql.MaxDepth(10),\n    graphql.MaxParallelism(10),\n    graphql.UseFieldResolvers(), // expose exported struct fields without explicit methods\n    graphql.Tracer(otel.DefaultTracer()),\n)\n\nhttp.Handle(\"/graphql\", &relay.Handler{Schema: schema})\n```\n\n`MustParseSchema` panics on invalid SDL or resolver mismatch — catch it at startup, not at request time.\n\n## Resolver Structure\n\nOne exported method per schema field; name match is case-insensitive:\n\n```go\ntype RootResolver struct {\n    db *sql.DB\n}\n\ntype QueryResolver struct {\n    db *sql.DB\n}\n\nfunc (r *RootResolver) Query() *QueryResolver { return &QueryResolver{db: r.db} }\n\n// Args struct for field arguments\nfunc (r *QueryResolver) User(ctx context.Context, args struct{ ID graphql.ID }) (*UserResolver, error) {\n    user, err := r.db.GetUser(ctx, string(args.ID))\n    if err != nil {\n        return nil, err\n    }\n    return &UserResolver{user: user}, nil\n}\n```\n\nReturn resolver wrapper structs, not domain models directly — keeps GraphQL projection separate from persistence.\n\n## Type Mapping\n\n<!-- prettier-ignore -->\n|GraphQL type|Go type|Notes|\n|---|---|---|\n|`ID`|`graphql.ID`|string alias|\n|`Int`|`int32`|**NOT `int`** — mismatch is a parse-time error|\n|`Float`|`float64`||\n|`String`|`string`||\n|`Boolean`|`bool`||\n|`[T]`|`[]*T` or `[]T`||\n|Nullable `T`|`*T`|pointer = nullable|\n|Non-null `T!`|`T`|non-pointer|\n|Custom scalar|implement `UnmarshalGraphQL(input any) error` + `MarshalJSON() ([]byte, error)`||\n|Enum|typed string alias||\n|Input|exported struct with field tags optional||\n|Interface/Union|Go interface returned; `ToConcreteType() (*T, bool)` discriminators||\n\nCommon mistake: using `int` for an `Int!` field — the parser rejects it with a type mismatch error.\n\n## Nullable vs Non-null Arguments\n\n```go\n// ✓ Good — pointer arg = nullable in schema\nfunc (r *QueryResolver) Users(ctx context.Context, args struct {\n    Role *string // nullable: Role in SDL\n    Limit int32  // non-null: Limit! in SDL\n}) ([]*UserResolver, error) { ... }\n```\n\nForgetting `*` on a nullable argument causes unmarshal failure when clients send `null`.\n\n## Custom Scalar\n\n```go\ntype DateTime struct{ time.Time }\n\nfunc (d *DateTime) UnmarshalGraphQL(input any) error {\n    s, ok := input.(string)\n    if !ok {\n        return fmt.Errorf(\"DateTime must be a string\")\n    }\n    t, err := time.Parse(time.RFC3339, s)\n    if err != nil {\n        return err\n    }\n    d.Time = t\n    return nil\n}\n\nfunc (d DateTime) MarshalJSON() ([]byte, error) {\n    return json.Marshal(d.Time.Format(time.RFC3339))\n}\n```\n\n## Interfaces and Unions\n\n```graphql\nin"},{"path":"references/testing.md","content":"# Testing GraphQL in Go\n\n## gqlgen — Client Harness\n\nThe `github.com/99designs/gqlgen/client` package drives the full stack (directives, middleware, resolvers) via an `http.Handler`:\n\n```go\nfunc TestCreateUser(t *testing.T) {\n    // Build the full handler with real dependencies (use a test DB)\n    srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{\n        Resolvers: &graph.Resolver{\n            DB: testDB,\n        },\n    }))\n\n    c := client.New(srv)\n\n    var resp struct {\n        CreateUser struct {\n            User struct {\n                ID    string\n                Email string\n            }\n            Errors []struct{ Message string }\n        }\n    }\n\n    c.MustPost(`\n        mutation CreateUser($email: String!, $name: String!) {\n            createUser(input: {email: $email, name: $name}) {\n                user { id email }\n                errors { message }\n            }\n        }\n    `, &resp,\n        client.Var(\"email\", \"alice@example.com\"),\n        client.Var(\"name\", \"Alice\"),\n        client.AddHeader(\"Authorization\", \"Bearer test-token\"),\n    )\n\n    require.Empty(t, resp.CreateUser.Errors)\n    require.Equal(t, \"alice@example.com\", resp.CreateUser.User.Email)\n}\n```\n\nFor unit testing individual resolvers, call resolver methods directly with a constructed `Resolver` and a real `context.Context` — no HTTP overhead.\n\n## gqlgen — Testing with DataLoaders\n\nWrap the test server with the DataLoader middleware so resolver tests exercise the full batching path:\n\n```go\nsrv := handler.NewDefaultServer(es)\nh := dataloaders.Middleware(testDB, srv)\n\nc := client.New(h)\n```\n\n## gqlgen — Testing Subscriptions\n\nUse `client.Subscription` to test subscription resolvers:\n\n```go\nsub := c.Subscription(`subscription { messageAdded(room: \"general\") { content } }`)\ndefer sub.Close()\n\n// Trigger an event\npublishMessage(\"general\", \"hello\")\n\nvar event struct{ MessageAdded struct{ Content string } }\nerr := sub.Next(&event)\nrequire.NoError(t, err)\nrequire.Equal(t, \"hello\", event.MessageAdded.Content)\n```\n\n## graph-gophers — gqltesting\n\n```go\nfunc TestUser(t *testing.T) {\n    gqltesting.RunTests(t, []*gqltesting.Test{\n        {\n            Schema: schema,\n            Query: `{ user(id: \"1\") { name email } }`,\n            ExpectedResult: `{\"user\":{\"name\":\"Alice\",\"email\":\"alice@example.com\"}}`,\n        },\n        {\n            Schema:        schema,\n            Query:         `{ user(id: \"999\") { name } }`,\n            ExpectedErrors: []*gqlerrors.QueryError{\n                {Message: \"user not found\", Extensions: map[string]any{\"code\": \"NOT_FOUND\"}},\n            },\n        },\n    })\n}\n```\n\nFor HTTP-level tests:\n\n```go\nfunc TestRelayHandler(t *testing.T) {\n    body := `{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}`\n    req := httptest.NewRequest(http.MethodPost, \"/graphql\", strings.NewReader(body))\n    req.Header.Set(\"Content-Type\", \"application/json\")\n    w := httptest.NewRecorder()\n\n    relay.Handler{Schema: schema}.ServeHTTP(w, req)\n\n    require.Equal"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports `github.com/99designs/gqlgen` or `github.com/graph-gophers/graphql-go`. Skill: golang-graphql Owner: samber Summary: Implements GraphQL APIs in Golang using gqlgen or graphql-go. Apply when building GraphQL servers, designing schemas, writing resolvers, handling subscriptions, or integrating GraphQL with existing Go HTTP services. Also apply when the codebase imports github.com/99designs/gqlgen or github.com/graph-gophers/graphql-go. Tags: latest:0.2.0 Version history: v0.2.0 | 2026-08-2","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1462,"uniquenessScore":47,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T15:03:48.149Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T15:03:48.149Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:44:02.514Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}