go-graphql
Implementing GraphQL APIs in Golang. Covers schema-first design, resolvers, and N+1 prevention with DataLoaders.
SKILL.mdskills/go/go-graphql/SKILL.md
GitHubCore Principles
- Schema-First Design — Write your GraphQL Schema Definition (SDL) first, then generate or map Go types to it.
- N+1 Prevention — Always use DataLoaders for nested fields to batch database queries.
- Thin Resolvers — Resolvers should only translate GraphQL inputs to business logic calls and format the output.
- Opaque IDs — Use the
IDscalar and avoid leaking internal database primary keys. - Pagination — Favor cursor-based pagination (Relay spec) over offset-based for large lists.
Library Comparison
| Library | Type | Features |
|---|---|---|
99designs/gqlgen |
Codegen | High performance, type-safe, best for large schemas. Recommended. |
graph-gophers/graphql-go |
Reflection | Simple setup, no code generation. |
graphql-go/graphql |
Code-first | Verbose, harder to maintain. Avoid for new projects. |
DataLoaders (N+1 Prevention)
DataLoaders batch multiple individual requests into a single batch request (e.g., SELECT * FROM users WHERE id IN (...)).
Important: DataLoaders MUST be created per-request (e.g., in a middleware), never globally, to avoid cross-user data leakage and stale caches.
func DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), loadersKey, NewLoaders(db))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Error Handling
Don't leak internal errors (like SQL strings) to the client. Use a custom error presenter to strip sensitive details and add extension codes.
// gqlgen example
srv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
if _, ok := err.(MySafeError); ok {
return gqlerror.Errorf(err.Error())
}
log.Printf("Internal error: %v", err)
return gqlerror.Errorf("internal server error")
})
Security
- Introspection — Disable GraphQL introspection in production to prevent API discovery.
- Complexity Limits — Enforce query complexity limits to prevent Denial of Service (DoS) attacks via deeply nested queries.
- Depth Limits — Restrict the maximum depth of a query.
Anti-patterns
- ❌ N+1 Queries — Fetching child objects one by one in a loop inside a resolver.
- ❌ Global DataLoaders — Sharing cache between different users (Security Risk!).
- ❌ SQL in Resolvers — Business logic and data access should live in a separate service layer.
- ❌ Leaking DB IDs — Exposing internal
intIDs. Use UUIDs or Base64-encoded IDs. - ❌ No Complexity Caps — Allowing clients to send arbitrarily deep/expensive queries.