How to Implement Resolvers in gqlgen

Web
Implement gqlgen resolvers by defining Go structs matching your schema and registering them in resolver.go.

How to Implement Resolvers in gqlgen

You write a GraphQL schema, run a single command, and your project suddenly contains a resolver package filled with empty structs and method signatures. The code generator did the heavy lifting, but the actual work is yours. You need to connect those generated stubs to your database, your HTTP clients, and your business rules. This is where resolvers live. They are the glue between the declarative schema and the imperative Go code that actually moves data.

The resolver pattern in plain terms

A resolver is just a function that answers a single field in your GraphQL query. The GraphQL server parses the incoming request, walks the query tree, and calls the matching resolver for every field it needs. gqlgen automates the wiring. It reads your schema, generates Go types that mirror the GraphQL types, and creates method signatures that match the expected field resolvers. You only write the implementation.

Think of the schema as a blueprint for a house. The blueprint specifies where the doors, windows, and pipes go. The resolvers are the plumbers and electricians who actually run the wires and connect the water lines. gqlgen is the foreman who hands out the work orders. You do not write the blueprint. You write the plumbing.

The generator does not rewrite your code. It compiles alongside it. If your method signature does not match what the generator expects, the build fails. The compiler rejects the program with cannot use r.Hello as type func(context.Context) string in field value if the signature drifts. Keep the generated code in version control. Treat it like any other dependency. Trust gofmt. Argue logic, not formatting.

Minimal example

Here is the simplest resolver setup. You start with a schema that defines a query returning a string. The generator creates a Query struct and a method signature. You implement the method to return the value.

// resolver.go
package resolver

import (
	"context"
	"github.com/99designs/gqlgen/graphql"
)

// Query matches the root Query type in your GraphQL schema.
type Query struct{}

// Hello implements the resolver for the hello field.
func (r *Query) Hello(ctx context.Context) string {
	// The receiver is named r, following Go convention for short, type-matching names.
	// Context is always the first parameter. It tracks request lifecycle and cancellation.
	// Return the value directly. gqlgen handles JSON serialization automatically.
	return "world"
}

The generator also creates a registration function that wires your structs into the execution engine. You update gqlgen.yml to point to your resolver package, then run gqlgen generate. The tool scans your Go files, matches the struct names to schema types, and injects the method references into the generated execution plan.

When a request arrives, the engine parses the query, validates it against the schema, and walks the abstract syntax tree. For each node, it looks up the corresponding resolver method and invokes it. The return value gets marshaled into the final JSON response. The whole pipeline runs in a single process. There is no network hop between the schema and your code.

Realistic example

Real applications fetch data. They talk to databases, call external APIs, and handle failures. A production resolver needs a data source, proper error wrapping, and context awareness. Dependency injection is the standard pattern. You pass a configuration struct to the resolver constructor, and the resolvers hold references to the services they need.

// resolvers.go
package resolver

import (
	"context"
	"fmt"
)

// User represents a domain object returned by the resolver.
type User struct {
	ID   string
	Name string
}

// Resolver holds references to external services and data sources.
type Resolver struct {
	users map[string]*User
}
// User fetches a single user by ID from the in-memory store.
func (r *Resolver) User(ctx context.Context, id string) (*User, error) {
	// Check context early. If the client cancels or the deadline passes, bail out immediately.
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	default:
	}

	// Look up the user. Keep the resolver thin and focused on GraphQL mapping.
	user, ok := r.users[id]
	if !ok {
		// Wrap the error with context. gqlgen extracts the message for the response.
		return nil, fmt.Errorf("user %s not found", id)
	}

	// Return the pointer. gqlgen handles null values when the pointer is nil.
	return user, nil
}

Notice the error handling pattern. if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. gqlgen expects resolvers to return errors when something goes wrong. The framework catches them, formats them into the GraphQL error response, and stops execution for that field. You do not need to panic. You do not need to write custom HTTP status codes. The error type is enough.

Context propagation is equally important. The context carries the request deadline and cancellation signals. If your resolver blocks on a slow database query, the client might cancel the request, but your goroutine keeps running. Always pass ctx to your database drivers and HTTP clients. The worst goroutine bug is the one that never logs.

Common pitfalls and compiler traps

Type mismatches are the most frequent build blocker. If your schema says ID is a String but your resolver returns an int, the generator produces code that fails to compile. The compiler rejects the program with cannot use r.ID (type int) as type string in field value. Fix the schema or cast the value explicitly. gqlgen expects exact matches.

Another trap is treating resolvers like HTTP handlers. GraphQL executes fields in parallel when possible. If two fields trigger the same resolver, the method runs concurrently. Shared state must be thread-safe. Use mutexes or pass immutable data. Do not store request-scoped values in the resolver struct. Create a new resolver instance per request, or use a request-local context value.

Interface mismatches also cause silent runtime failures. If your schema defines a union or interface type, gqlgen generates a helper method called IsImplementingType. You must register your concrete types in gqlgen.yml. If you forget, the server returns null for every field of that type. The compiler will not catch it. Read the generated code. Follow the registration steps exactly.

Convention asides matter here too. Public names start with a capital letter. Private start lowercase. No keywords like public or private. Accept interfaces, return structs. This mantra keeps your resolver layer decoupled from your domain layer. Define a UserService interface in your resolver package, pass a concrete implementation at startup, and test with mocks. The resolver itself stays thin and focused on mapping GraphQL types to Go values.

When to reach for gqlgen

Use gqlgen when you want type-safe GraphQL resolvers that compile alongside your Go code and catch signature mismatches at build time. Use a manual graphql-go implementation when you need fine-grained control over the execution engine and do not mind writing more boilerplate. Use a REST API when your clients do not need flexible field selection or nested data fetching. Use a code-first library like go-graphql when you prefer defining types in Go and generating the schema automatically. Use a gateway pattern when you need to stitch multiple microservices behind a single GraphQL endpoint.

Resolvers are adapters. Keep them thin. Push business logic into service packages. Let the framework handle the wiring.

Where to go next