Hooks and plugins
Lifecycle hooks for logging, tracing, auth, and other cross-cutting work on every request.
Plugins are how grx exposes the request lifecycle. They're the right place for logging, tracing, metrics, request-scoped state, and policy decisions that shouldn't live inside resolvers.
Plugins vs middleware
Host HTTP middleware wraps the GraphQL handler before a request becomes GraphQL. It works
with http.Request and http.ResponseWriter, so it is the right place for
transport concerns: headers, cookies, CORS, request IDs, IP allowlists,
authentication tokens, and request size checks.
Plugins run after a transport has decoded the GraphQL operation and handed it to the executor. They see GraphQL lifecycle phases and field metadata, so they are the right place for tracing, validation/execution metrics, field-level access logging, response hooks, and reporting execution errors.
| Concern | Use | Why |
|---|---|---|
| Request ID header | Host HTTP middleware | It reads/writes HTTP headers before GraphQL execution. |
| Bearer token parsing | Middleware | Tokens live in HTTP headers, not core.Request. |
| Field tracing | Plugin | It needs GraphQL field names, return types, and response paths. |
| Validation/execution metrics | Plugin | It follows executor lifecycle hooks. |
| CORS / Origin checks | Middleware or transport config | It is an HTTP/WebSocket boundary concern. |
The interface
// plugins/plugins.go
type Plugin interface {
RequestStart(ctx context.Context, req core.Request) (context.Context, error)
ParsingStart(ctx context.Context, req core.Request) error
ValidationStart(ctx context.Context, req core.Request) error
ExecutionStart(ctx context.Context, req core.Request) error
FieldResolveStart(ctx context.Context, field FieldContext) error
ResponseSend(ctx context.Context, res core.Response) error
Error(ctx context.Context, err error)
}Two things make this interface comfortable to implement:
RequestStartis the only hook that returns a derivedcontext.Context. Add request-scoped values here.- All hooks but
Errormay return an error. A non-nil error short-circuits the request - useful for auth, rate-limiting, and budget checks.
plugins.Base
Embed plugins.Base to inherit no-op defaults for every hook. New hooks
added in future releases will keep your plugin compiling:
type RequestID struct{ plugins.Base }
func (RequestID) RequestStart(ctx context.Context, _ core.Request) (context.Context, error) {
return context.WithValue(ctx, requestIDKey{}, uuid.NewString()), nil
}Registration
Plugins run in the order they're registered. Put plugins that produce request-scoped values (request ids, trace spans) before plugins that consume them.
import (
"log/slog"
"os"
"github.com/grx-gql/grx"
"github.com/grx-gql/grx/plugins/logger"
)
loggerPlugin, _ := logger.New(logger.Config{
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})),
})
srv, _ := grx.NewServer(
grx.WithSchema(graph.Schema()),
grx.WithPlugins(loggerPlugin),
)Concurrency rules
- Plugins are shared across requests. Do not store per-request state on
the plugin struct - put it in the
context.ContextfromRequestStartand read it back from later hooks. - Hooks run on the request goroutine. If you need to do I/O, don't block unnecessarily; plugins are part of the latency budget.
FieldResolveStartfires for every resolved field. Keep it cheap, or gate expensive work behind a sampling decision made inRequestStart.
HTTP auth and middleware
core.Request does not expose HTTP headers. Parse Authorization in host HTTP middleware wrapping the GraphQL handler, attach identity with context.WithValue, and reuse that context for the whole GraphQL pipeline.
Pair that with WithFieldAuthorizer / WithOperationAuthorizer, or enforce policy inside a plugin hook. The authentication guide shows a Bearer token, context-scoped identity, and a field authorizer guarding a viewer field - the same wiring as examples/auth in this repository.
Built-in plugins
The plugins/logger subpackage ships a
structured logger that emits a log/slog event for each lifecycle hook
with operation name, payload sizes, and any error. See
Custom Plugin for a worked example of writing
your own.
Last updated on