grx

How it fits together

Layers, packages, startup vs request path, and where to plug in transports, middleware, plugins, and authorizers.

This page is the systems view: what owns the network edge, where GraphQL stops being HTTP, how packages depend on each other, and where you extend behavior without rewriting the executor. Pair it with Define your schema when you mostly care about resolver shape, or Execution pipeline when you want stage-by-stage detail.


Layers at runtime

Roughly everything in HTTP / WebSocket framing touches TCP, headers, or Match/Serve. Inside Transport-agnostic GraphQL the executor only sees core.Request - the intentional split so the same engine runs behind CLI tools, tests, WebSockets, SSE, or a bespoke gateway.

 Clients & gateways (browsers · apps · BFF)
                    │
                    ▼
 ┌─────────────────────────────────────────────────────────┐
 │ HTTP / WebSocket framing                                 │
 │  Host HTTP middleware → server.Server → transports       │
 └─────────────────────────────────────────────────────────┘
                    │ Execute(ctx, core.Request)
                    ▼
 ┌─────────────────────────────────────────────────────────┐
 │ Transport-agnostic GraphQL                               │
 │  exec.Executor  <->  compiled schema / schema.Schema      │
 └─────────────────────────────────────────────────────────┘

Important boundary: middleware works on http.Request (headers, cookies, trailers). core.Request deliberately carries only query, operationName, and variables - so JWT or Bearer parsing stays in middleware (or upstream proxies), attaching identity onto context.Context before the transport calls Execute. More on security wiring in Authentication & authorization.


Package layers (imports)

core is the thin ABI between transports and execution: Request / Response, the Executor interface, and Transport. The package deliberately imports nothing upward (exec, schema, server, … stay out), so wiring stays acyclic while every adapter shares the same types.

schema describes how Go types compile into GraphQL metadata (mostly at startup).

exec implements parsing, validation, execution, and subscription streaming.

plugins defines lifecycle hooks the executor invokes in order - logging, denial, enrichment.

server is the http.Handler: multiplexes playground, transports, timeouts, masking, persisted queries - then hands work to transports + executor.

Companion packages such as http, websocket, sse, and pubsub provide the official extension surfaces.

Import edges (→ reads “imports”):

  • plugins → core
  • exec → core, schema, plugins
  • server → core, exec, schema, plugins

grx is a thin facade: NewServer and options map onto server.Config.


Two clocks: startup vs each request

PhaseWhat happens
Process startupYour schema.Config (resolver roots + optional enums/unions/etc.) is reflected once into a schema.Schema artifact; an exec.Executor binds that metadata with plugins and policy hooks; server.New wraps the executor and registers transports (http is auto-appended on the GraphQL path unless you customise ordering).
Each GraphQL operationA transport parses the wire format → builds core.Request → invokes executor.Execute(ctx, req) or Subscribe → response bytes go back through the transport. No full schema rebuild; per-request CPU is dominated by lex/parse/validate/execute unless you explicitly cache fragments or APQ payloads.

That split is why resolvers receive frozen schema metadata via the executor and fresh context.Context values (deadlines, cancellations, middleware-scoped identity).


Path routing in one glance

server.Config:

  • GraphQLPath (default /graphql) - HTTP JSON queries/mutations (POST) always land here thanks to the default http transport placed at the tail of that chain unless you reorganise transports.
  • SubscriptionPath - when unset, WebSocket/SSE transports share GraphQLPath (usual GraphiQL + graphql-transport-ws setup). When it differs, only concrete bundled *websocket.WebSocketTransport / *sse.Transport registrations move - custom transports stay wherever you routed them unless you mimic that split yourself.

Ordering rule: transports are scanned registration order, first Match wins. See Routers & transports.


Where to intervene (extension points)

Pick the cheapest layer - don’t put HTTP concerns inside resolvers unless you truly need to.

NeedPreferNuance
CORS, auth headers, JWT on ctxHost-router middleware wrapping the final http.HandlerRuns before transports; propagate with r.WithContext.
New wire format or bespoke gatewayImplement core.TransportMatch must stay cheap & side‑effect‑free - see Custom transport.
Lifecycle logging, quotas, coarse denyWithPlugins implementing plugins.Plugin - HooksRequestStart can replace ctx; hooks can abort before expensive work.
“No mutations anonymous” blanket ruleWithOperationAuthorizer - also Authentication guideEvaluated during document validation (alongside trusted-doc / rate‑limit knobs on server config when enabled).
Per-field secrecy / tenancyWithFieldAuthorizerPaths are GraphQL paths; rejects before coercion + resolver.
Deadline on every operationWithRequestTimeoutDeadline applies inside built-in transports calling Execute / subscription setup - wire your own timeouts if you replace transports.

Operation flow (mental checklist)

Once a subscription or query reaches the executor, the condensed story matches the Execution pipeline: Lex → Parse → Validate → Execute (+ plugin taps between stages). Streaming responses simply repeat emission per subscription event according to transport rules documented under Subscriptions and realtime guides.

If you benchmark or profile, distinguish cold parse versus steady-state workloads - see the Benchmarks page and sibling benchmark/ module in the repository for comparisons with other Go GraphQL stacks.


Edit this page on GitHub

Last updated on

On this page