defederator

module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 19, 2026 License: MIT

README

defederator

defederator is a code generator that produces typed Go GraphQL clients that bypass a gateway entirely. Given a Federation v2 supergraph SDL and .graphql operation files, it generates a Go client whose methods execute federation query plans against subgraph services directly — resolving cross-subgraph joins, entity fetches, @requires, and @provides in process — without a running Apollo Router or similar gateway.

How it works

supergraph.graphql + queries/*.graphql
        │
        ▼
  defederator (code generator)
        │
        ├── strips federation metadata from the SDL
        ├── uses gqlgenc's type pipeline to generate typed response structs
        ├── compiles each operation into a URL-keyed execution plan (JSON)
        └── renders client.go + federation_exec.go
        │
        ▼
  generated/
    client.go           ← typed methods; plan specs baked in as string constants
    federation_exec.go  ← copy of the execution engine (stdlib-only, no imports)
        │
        ▼  at runtime
  generated.NewClient(httpClient) → *Client
        │
        └── each method: ResolveURLSpec → Execute → unmarshal typed response

Generated code imports only the Go standard library. No defederator or gorouter packages are imported at runtime.

Quick start

1. Install the generator
go install github.com/StevenACoffman/defederator/cmd/defederator@latest
2. Write a config file
# .defederator.yml
schema: supergraph.graphql      # Federation v2 supergraph SDL

query:
  - queries/**/*.graphql        # GraphQL operation files (named operations only)

client:
  filename: ./generated/client.go
  package:  generated

generate:
  clientInterfaceName: FederationClient   # optional; generates a named interface for mocking
  optional: pointer                        # "pointer" (default) or "value"
  export_operations: operations.json       # optional; JSON manifest for APQ pre-registration
3. Write named operations
# queries/products.graphql
query GetProduct($id: ID!) {
  product(id: $id) {
    id
    sku
    createdBy {
      email
      name
    }
  }
}
4. Generate
defederator                        # reads .defederator.yml in current or parent dir
defederator -c path/to/.defederator.yml

The generator writes two files into the output directory:

  • client.go — typed request/response structs and methods for each operation
  • federation_exec.go — the execution engine, renamed to the output package
5. Use the generated client
import "yourmodule/generated"

// URLs are baked in from the supergraph SDL at generation time.
// Pass nil to use http.DefaultClient.
client, err := generated.NewClient(nil)
if err != nil { ... }

res, err := client.GetProduct(ctx, &generated.GetProductRequest{ID: "apollo-federation"})
if err != nil { ... }
fmt.Println(res.Product.Sku)

NewClient resolves the embedded plan specs once and caches them. It is safe for concurrent use across goroutines.

Package layout

defederator/
├── cmd/defederator/        # CLI binary: reads config, calls generator.Generate
├── config/
│   ├── config.go           # .defederator.yml loader and Config type
│   └── genqlient.go        # genqlient.yaml loader → *Config adapter
├── execengine/             # Execution engine (source-embedded into generated code)
│   ├── execengine.go       #   Execute, ResolveURLSpec, ExecuteAndUnmarshal, ApplyProjection
│   └── source.go           #   //go:embed execengine.go — lets generator copy it
├── generator/
│   ├── generator.go        # Orchestrates schema strip → gqlgenc pipeline → render
│   ├── schema.go           # StripFederationTypes: supergraph SDL → clean SDL
│   ├── template.go         # RenderFederationTemplate
│   ├── template.gotpl      # Federation-specific Go template
│   ├── urlspec.go          # MarshalURLPlanSpec, WriteExecFile
│   ├── glob.go             # Doublestar glob expansion with deduplication
│   ├── goextract.go        # Extracts # @genqlient-annotated queries from Go literals
│   ├── export.go           # Writes JSON manifest of generated operations
│   └── *_test.go           # Unit + codegen compile tests for all 5 scenarios
├── migrate/                # defederator migrate subcommand
│   ├── migrate.go          # CLI entry point; orchestrates file generation
│   ├── convert.go          # DefederatorYAML: genqlient.yaml → .defederator.yml text
│   ├── subgraphs.go        # ParseSubgraphs, ParseInputObjectsForService
│   ├── clientgen.go        # Render: Data → cross_service/client.go text
│   ├── client.gotpl        # Go template for the client scaffold
│   └── *_test.go           # Unit tests + golden file for client scaffold
├── federationclient/       # Legacy runtime package (kept for graphqlcompat)
├── graphqlcompat/
│   ├── client.go           # genqlient graphql.Client adapter
│   └── client_test.go
└── gqlgencfed/
    ├── plugin.go           # gqlgenc plugin (replaces "clientgen" via api.ReplacePlugin)
    └── plugin_test.go      # End-to-end plugin path test

Config files

.defederator.yml (native format)
schema: supergraph.graphql

query:
  - queries/**/*.graphql

client:
  filename: ./generated/client.go
  package:  generated

generate:
  clientInterfaceName: FederationClient
  optional: pointer          # "pointer" (default) | "value"
  export_operations: ops.json
genqlient.yaml (drop-in for existing genqlient projects)

defederator reads genqlient.yaml directly, converting its fields to the equivalent defederator config.

schema: supergraph.graphql
operations:
  - queries/**/*.graphql
generated: ./generated/client.go
package: generated

Query sources

.graphql files

Standard named GraphQL operations:

query GetUser($id: ID!) {
  user(id: $id) { name email }
}
Embedded queries in Go files (genqlient style)

Go string literals beginning with # @genqlient are extracted and treated as query sources. Source positions are preserved so parse errors point back to the original Go file and line.

var getUserQuery = `# @genqlient
query GetUser($id: ID!) {
  user(id: $id) { name email }
}`

Include the Go file in query::

query:
  - queries/**/*.graphql
  - internal/api/**/*.go
Glob patterns

Both .graphql and .go path entries support doublestar globs. A glob that matches no files is an error rather than a silent no-op.

generate: options

Option Default Description
clientInterfaceName (none) Emit a named interface; useful for mocking in tests
optional "pointer" Nullable field representation: "pointer"*T, "value"T
export_operations (none) Path to write a JSON manifest of all generated operations

Generated code architecture

Plan specs baked at generation time

For each operation, the generator calls federation.BuildPlan against the supergraph SDL and serializes the result to a URL-keyed JSON string that is embedded as a constant in client.go:

const GetProductPlanSpec = `{"fetches":[{"url":"https://products.svc/graphql","query":"..."}],"entityFetches":[...]}`

The URLs come from the supergraph SDL. There is no runtime SDL parsing.

federation_exec.go

The generator copies execengine.go into the output package with the package declaration renamed. This gives the generated client a self-contained execution engine with no external imports. The file provides:

  • ResolveURLSpec(specJSON string) (*Plan, error) — decodes a URL-keyed plan spec
  • Execute(ctx, plan, vars, client) (map[string]any, []GraphQLError, error) — runs the plan
  • ExecuteAndUnmarshal(ctx, plan, vars, client, dest) error — Execute + JSON unmarshal into a typed struct
  • ApplyProjection(data, projection) map[string]any — strips planner-added fields from the response
NewClient initialization
func NewClient(httpClient *http.Client) (*Client, error) {
    if httpClient == nil { httpClient = http.DefaultClient }
    // Resolve each operation's plan spec once at startup.
    plans := map[string]*Plan{
        "GetProduct": must(ResolveURLSpec(GetProductPlanSpec)),
        ...
    }
    return &Client{http: httpClient, plans: plans}, nil
}

If any plan spec fails to parse (malformed JSON), NewClient returns an error rather than panicking later during execution.

Two generation paths

defederator -c .defederator.yml

Calls generator.Generate directly. No dependency on gqlgenc's generator.

Path B — gqlgenc plugin

For existing gqlgenc projects that want to swap the transport layer:

import (
    "github.com/99designs/gqlgen/api"
    "github.com/gqlgo/gqlgenc/generator"
    "github.com/StevenACoffman/defederator/gqlgencfed"
)

err = generator.Generate(ctx, cfg,
    api.ReplacePlugin(gqlgencfed.NewWithFilePaths(queryPaths, cfg.Client, cfg.Generate)),
)

defederator migrate — scaffold from genqlient.yaml

defederator migrate reads an existing genqlient.yaml and the Federation v2 supergraph SDL to generate two files that bootstrap defederator adoption for a service:

  • .defederator.yml — native defederator config derived from genqlient.yaml, with scalar bindings substituted for graphql.String and INPUT_OBJECT bindings for types owned by this subgraph.
  • cross_service/client.go — Go scaffold with a newFederationClient constructor and a <service>SubgraphURLs helper wired to service discovery.
# Preview without writing
defederator migrate --dry-run ./services/districts

# Write (skips existing files)
defederator migrate ./services/districts

# Overwrite existing files
defederator migrate --force ./services/districts

The tool reads the supergraph SDL from the path in genqlient.yaml's schema: field, so no extra flags are needed when the schema path is ../../gengraphql/composed_schema.graphql (the webapp convention).

Flags
Flag Description
--dry-run Print generated content to stdout; write nothing
--force Overwrite existing .defederator.yml and cross_service/client.go
What migrate leaves incomplete
  • Subgraph list_subgraphServices in the generated client lists every subgraph in the supergraph. Prune it to only those this service's operations actually touch.
  • Auth factory pattern — services that issue federation calls under multiple auth roles (user / admin / locale) need separate constructor variants. The tool generates a single newFederationClient(ctx _federationCtx).
  • INPUT_OBJECT bindings — may include types owned by this subgraph that no operation actually passes as arguments. Remove unused ones after confirming compilation.
  • Scalar bindings — non-standard scalars are bound to graphql.String as a placeholder. Replace with the real Go type if the field is read by your code.
  • Code generation — migrate only writes config and scaffold. Run defederator in the service directory afterward to generate the typed client and execution engine.

For step-by-step instructions covering all 31 webapp services, see migrate_webapp.md.

Migration from genqlient (graphqlcompat adapter)

If you have an existing genqlient project, graphqlcompat.NewClient provides a drop-in adapter that implements genqlient's graphql.Client interface backed by the federation-aware execution engine.

import (
    "github.com/StevenACoffman/defederator/federationclient"
    "github.com/StevenACoffman/defederator/graphqlcompat"
    "yourmodule/generated" // your existing genqlient-generated package
)

sg, err := federationclient.ParseSupergraphSDL(sdl)
if err != nil { ... }

// Drop-in replacement for graphql.NewClient(url, httpClient)
client := graphqlcompat.NewClient(sg, httpClient)

// Existing genqlient-generated code works unchanged
resp, err := generated.GetUser(ctx, client, userID)

Schema stripping

The supergraph SDL contains federation metadata (@join__type, @join__field, join__Graph enum, etc.) that gqlgen's type mapper does not understand. generator.StripFederationTypes removes all federation-specific directives, type definitions, and per-field annotations before passing the schema to gqlgenc's pipeline. User-defined directives are preserved.

Verification

The test suite verifies generated client correctness at three layers:

Layer 1 — GraphQL protocol (execengine/protocol_test.go): The executor correctly handles all legal HTTP response shapes — data: null with and without errors, malformed JSON, pre-cancelled contexts — without panicking or silently discarding information.

Layer 2 — Federation entity resolution protocol (execengine/entities_test.go, execengine/golden_test.go): _entities calls carry the correct representations (__typename + key fields + @requires fields, no extras). Variables are validated against Apollo-captured fixture files.

Layer 3 — Cross-subgraph merge correctness (execengine/golden_test.go): The merged output matches expected.json from Apollo's reference implementation exactly.

Golden fixtures

All five federation patterns are covered:

Fixture Pattern
01_product_id_sku Single-subgraph lookup
02_product_delivery Cross-subgraph join + @requires
03_product_creator_name Three-subgraph join
04_product_creator_requires @requires (same-subgraph, multi-step plan)
05_product_creator_provides @provides (key pre-fetched, no extra entity fetch)

Fixture responses were captured from Apollo's reference implementation and live in gorouter/federation/testdata/golden/.

Dependencies

Dependency Role
github.com/StevenACoffman/gorouter/federation Query planner (BuildPlan, ParseSchema) — used at generation time only
github.com/gqlgo/gqlgenc Type-generation pipeline (clientgenv2, parsequery, querydocument)
github.com/99designs/gqlgen Code rendering (codegen/templates)
github.com/vektah/gqlparser/v2 SDL parsing and formatting (schema stripping)
github.com/goccy/go-yaml Config file parsing
github.com/Khan/genqlient graphql.Client interface (graphqlcompat adapter)
github.com/bmatcuk/doublestar/v4 Doublestar glob expansion for query paths

Generated code has no runtime dependencies beyond the Go standard library.

Workspace setup

agent-orange/
├── go.work             # use ./defederator ./gorouter ./gqlgenc
├── defederator/        # this module
├── gorouter/
└── gqlgenc/

Directories

Path Synopsis
Package check detects orphaned genqlient call sites in a service directory.
Package check detects orphaned genqlient call sites in a service directory.
cmd
defederator command
Command defederator generates typed Go federation clients from a supergraph SDL.
Command defederator generates typed Go federation clients from a supergraph SDL.
Package config loads and validates .defederator.yml configuration files.
Package config loads and validates .defederator.yml configuration files.
Package execengine executes a pre-built federation Plan against subgraph HTTP endpoints.
Package execengine executes a pre-built federation Plan against subgraph HTTP endpoints.
generated
defederator
Package defederator executes a pre-built federation Plan against subgraph HTTP endpoints.
Package defederator executes a pre-built federation Plan against subgraph HTTP endpoints.
Package generator orchestrates the defederator code-generation pipeline.
Package generator orchestrates the defederator code-generation pipeline.
Package gqlgencfed provides a gqlgenc plugin that generates typed Go clients whose methods call the Federation v2 query planner directly instead of an HTTP gateway.
Package gqlgencfed provides a gqlgenc plugin that generates typed Go clients whose methods call the Federation v2 query planner directly instead of an HTTP gateway.
Package graphqlcompat provides a graphql.Client (genqlient's interface) backed by the defederator federation planner.
Package graphqlcompat provides a graphql.Client (genqlient's interface) backed by the defederator federation planner.
Package migrate generates a .defederator.yml plus a stub cross_service/client.go from an existing genqlient.yaml, so a service can switch from genqlient to defederator with minimal manual edits.
Package migrate generates a .defederator.yml plus a stub cross_service/client.go from an existing genqlient.yaml, so a service can switch from genqlient to defederator with minimal manual edits.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL