golang-graphql ยท v0.2.2 ยท 2026-09-02 ยท sha256 98c62367ab8bd4e5
golang-graphql v0.2.2A
Immutable. This exact content is served forever at /api/v1/blob/98c62367ab8bd4e5.
---
name: golang-graphql
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`."
user-invocable: false
license: MIT
compatibility: Designed for Claude Code, Codex or similar harness, and for projects using Golang.
metadata:
author: samber
version: "0.2.2"
openclaw:
emoji: "๐ฎ"
homepage: https://github.com/samber/cc-skills-golang
requires:
bins:
- go
install: []
skill-library-version: "0.17.89"
allowed-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__*
paths:
- "**/*.go"
---
**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.
**Modes:**
- **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.
- **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.
> **Community default.** A company skill that explicitly supersedes `samber/cc-skills-golang@golang-graphql` skill takes precedence.
# Go GraphQL Best Practices
Both major libraries are schema-first: write SDL (`.graphql` files), bind Go resolvers. Choose based on project size and team preferences.
This 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`), preferred 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.
## Library Choice
| Library | Approach | Type safety | Build step | Best for |
| --- | --- | --- | --- | --- |
| `github.com/99designs/gqlgen` | Codegen | Compile-time | `go generate` | Large schemas, federation, strict types |
| `github.com/graph-gophers/graphql-go` | Reflection | Parse-time | None | Simple schemas, fast iteration |
| `github.com/graphql-go/graphql` | Code-first | Runtime | None | **Avoid** โ verbose, no SDL |
Pick **gqlgen** when: Apollo Federation is required, schema is large (100+ types), or the team wants generated stubs and zero reflection overhead.
Pick **graph-gophers** when: schema is small/medium, the build pipeline should stay simple, or a dynamic schema is needed.
For deep-dive on each library, see [gqlgen reference](./references/gqlgen.md) and [graphql-go reference](./references/graphql-go.md).
## Schema Design
```graphql
# โ Good โ explicit nullability; ID scalar for opaque identifiers
type User {
id: ID!
email: String! # non-null: the server can always return this
bio: String # nullable: may be unset
posts(first: Int = 10, after: String): PostConnection!
}
# โ Bad โ Int ID leaks implementation details, breaks client caching
type Post {
id: Int!
}
```
**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.
**Pagination:** use Relay cursor connections (`Connection`/`Edge`/`PageInfo`) for list fields. Avoid offset pagination on large datasets โ cursors are stable under concurrent writes.
**Mutations:** wrap results in an envelope type so clients receive business errors alongside partial results without polluting the GraphQL `errors` array:
```graphql
type CreateUserPayload {
user: User
errors: [UserError!]!
}
```
## Resolver Patterns
Keep resolvers thin โ they translate GraphQL inputs to domain calls and domain responses to GraphQL outputs.
```go
// โ Good โ resolver delegates to service layer
func (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*model.CreateUserPayload, error) {
user, err := r.userService.Create(ctx, input.Email, input.Name)
if err != nil {
return nil, formatError(err)
}
return &model.CreateUserPayload{User: toGQLUser(user)}, nil
}
// โ Bad โ SQL in resolver, no separation of concerns
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
// ...
}
```
Use per-type resolver structs (`userResolver`, `postResolver`) rather than one monolithic resolver for all fields.
## N+1 Prevention (DataLoaders)
Each `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.
**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.
```go
// โ Good โ per-request DataLoader in middleware
func DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
loaders := &Loaders{
PostsByUserID: newPostsByUserIDLoader(r.Context(), db),
}
ctx := context.WithValue(r.Context(), loadersKey, loaders)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// โ Bad โ global DataLoader shared across all requests
var globalLoader = newPostsByUserIDLoader(context.Background(), db)
```
In 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.
## Authentication and Authorization
Two-layer model:
1. **HTTP middleware** โ extract and validate tokens, stash identity in `context.Context`.
2. **Schema directives** (gqlgen) or **resolver checks** (graphql-go) โ enforce per-field authorization.
```go
// HTTP middleware layer (both libraries)
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := validateToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
In 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).
## Error Handling
Never return raw internal errors โ they leak SQL messages, stack traces, or service internals to clients.
```go
// gqlgen โ custom ErrorPresenter strips internal details
srv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
var gqlErr *gqlerror.Error
if errors.As(err, &gqlErr) {
return gqlErr // already formatted
}
// log internal err here
return gqlerror.Errorf("internal error") // safe client message
})
// Add extension codes for client-side error handling
return nil, &gqlerror.Error{
Message: "user not found",
Extensions: map[string]any{"code": "NOT_FOUND"},
}
```
For graph-gophers, implement the `ResolverError` interface to attach `Extensions()`. See [graphql-go reference](./references/graphql-go.md).
Use `graphql.AddError(ctx, err)` in gqlgen for non-fatal field errors where the resolver can still return partial data.
For error wrapping patterns, see the `samber/cc-skills-golang@golang-error-handling` skill.
## Subscriptions
Subscriptions use long-lived WebSocket connections. The critical discipline: **always respect context cancellation** โ a leaked goroutine per disconnected client exhausts resources silently.
```go
// โ Good โ closes channel when client disconnects
func (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {
ch := make(chan *model.Message, 1)
sub := r.pubsub.Subscribe(room) // subscribe once before the goroutine
go func() {
defer close(ch) // always close; signals iteration to stop
for {
select {
case <-ctx.Done():
return // client disconnected
case msg := <-sub:
select {
case ch <- msg:
case <-ctx.Done():
return
}
}
}
}()
return ch, nil
}
// โ Bad โ goroutine leaks forever when client disconnects
func (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *model.Message, error) {
ch := make(chan *model.Message, 1)
go func() {
for msg := range r.pubsub.Subscribe(room) {
ch <- msg // blocks forever after client gone
}
}()
return ch, nil
}
```
## Performance and Safety
Production GraphQL servers require explicit limits. Without them, a single deeply nested query exhausts CPU and memory.
```go
// gqlgen โ wire these into every production handler
srv := handler.NewDefaultServer(es)
srv.Use(extension.FixedComplexityLimit(200)) // max cost per query
// Gate introspection โ only in non-production environments
if os.Getenv("ENV") != "production" {
srv.Use(extension.Introspection{})
}
```
For graph-gophers: `graphql.MaxDepth(10)` and `graphql.MaxParallelism(10)` options at `ParseSchema` time.
**Query allow-listing:** in production, consider persisted queries (gqlgen APQ extension) to reject arbitrary query strings.
## Common Mistakes
| Mistake | Why it matters | Fix |
| --- | --- | --- |
| N+1 queries in child resolvers | One SQL per parent row โ O(n) DB calls | Use per-request DataLoader |
| Global DataLoader | Cross-request cache โ stale data, data leaks | Create DataLoader in request middleware |
| Editing `models_gen.go` directly | Next `go generate` wipes hand edits | Use `autobind` or `models.<T>.model` in `gqlgen.yml` |
| Forgetting `go generate` after schema change | Resolver interface mismatch at compile time | Re-run `go tool gqlgen generate` |
| `int` field in graph-gophers resolver | Library requires `int32` for `Int` scalar | Use `int32` (or `float64` for `Float`) |
| Introspection enabled in production | Exposes full schema to attackers | Gate with `ENV` check |
| No complexity cap | Deeply nested query โ CPU/memory DoS | `extension.FixedComplexityLimit(N)` |
| Leaking DB errors from resolvers | Exposes SQL internals to clients | Wrap in `ErrorPresenter` / `ResolverError` |
| Subscription goroutine leak | Client disconnect โ goroutine runs forever | `defer close(ch)` + `select ctx.Done()` |
| Nullable field for always-required data | Clients must null-check everywhere | Mark `!` in schema; return error from resolver |
## Deep Dives
- **[gqlgen reference](./references/gqlgen.md)** โ codegen workflow, `gqlgen.yml`, DataLoaders, Federation v2, directives
- **[graphql-go reference](./references/graphql-go.md)** โ reflection resolver model, type mapping, tracing
- **[Testing](./references/testing.md)** โ gqlgen client harness, gqltesting, httptest patterns
## Cross-References
- โ See `samber/cc-skills-golang@golang-context` skill for context propagation in resolvers and subscriptions
- โ See `samber/cc-skills-golang@golang-error-handling` skill for error wrapping and sentinel patterns
- โ See `samber/cc-skills-golang@golang-testing` skill for table-driven and integration test patterns
- โ See `samber/cc-skills-golang@golang-observability` skill for tracing and metrics in resolvers
- โ See `samber/cc-skills-golang@golang-security` skill for input validation and injection prevention
- โ See `samber/cc-skills-golang@golang-database` skill for N+1 query patterns and DataLoader database batching
## References
- [gqlgen](https://github.com/99designs/gqlgen)
- [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go)
- [Relay cursor connections spec](https://relay.dev/graphql/connections.htm)
If you encounter a bug or unexpected behavior in gqlgen, open an issue at <https://github.com/99designs/gqlgen/issues>.
If you encounter a bug or unexpected behavior in graph-gophers/graphql-go, open an issue at <https://github.com/graph-gophers/graphql-go/issues>.