grx

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.

ConcernUseWhy
Request ID headerHost HTTP middlewareIt reads/writes HTTP headers before GraphQL execution.
Bearer token parsingMiddlewareTokens live in HTTP headers, not core.Request.
Field tracingPluginIt needs GraphQL field names, return types, and response paths.
Validation/execution metricsPluginIt follows executor lifecycle hooks.
CORS / Origin checksMiddleware or transport configIt 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:

  • RequestStart is the only hook that returns a derived context.Context. Add request-scoped values here.
  • All hooks but Error may 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.Context from RequestStart and 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.
  • FieldResolveStart fires for every resolved field. Keep it cheap, or gate expensive work behind a sampling decision made in RequestStart.

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.

Edit this page on GitHub

Last updated on

On this page