examples/

directory
v0.0.0-...-8018d94 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: MIT

README

Examples

Directory Read it for
quickstart The smallest thing that runs: one SDL file, hand-written bindings, no codegen, no layers.
blog A layered service: generated bindings, a domain isolated from the wire format, a DataLoader, subscriptions.
storefront Authorization: scopes, an argument site, per-row instance checks, and the server wiring a deployment needs.
federation Two Apollo Federation subgraphs and the fetch a router performs across them.
relaynode The Relay contract: global ids, Query.node re-fetch, and cursor connections.
echo Serving blog through Echo v5 — routing and shutdown wiring only.
fiber Serving blog through Fiber v3 — routing and shutdown wiring only.
veloxfx A real service: velox writes the ORM and the SDL for seven entities in three domains, gqlc binds them, uber/fx assembles each domain like a gRPC service behind Echo. GUIDE.md is the developer guide. Its own module.

storefront is the one to read for anything about authorization, limits or shutdown; blog is the one to read for layering and codegen. They do not overlap on purpose.

echo and fiber build the same schema blog/cmd/server does. One schema over three frameworks is the point: nothing in blog knows which transport is serving it.

Two ways to bind Go types

quickstart binds its own type straight to the schema:

graphql.Object[Note]("Note", graphql.Field("id", ...))

blog does not. Its internal/domain types never reach the GraphQL layer; internal/transport/graphql maps them to the generated models at the boundary, so editing the SDL cannot change the type the repository stores.

The library does not require this. Object[domain.User] works, and for a small service it is the better trade. blog pays for the separation to show what it costs as well as what it buys — and it does cost. Post.author is a resolver field, so the generated model.Post has no AuthorID, and PostAuthor has to fetch the post back out of the store to recover an author id the domain object was already holding, before the DataLoader can batch anything. Binding the domain type directly would hand it over for free.

Layers in blog

Package Holds
internal/domain Entities. Imports neither graphql-go nor the generated models.
internal/repository Storage. Stores what it is given.
internal/app The rules: a post needs a real author; title cannot be cleared.
internal/transport/graphql Implements the generated Resolver, maps domain to model, owns the DataLoader.
graph/ Generated by gqlc. Do not edit.

Most of app forwards to the repository, which is the honest shape of a service layer over an in-memory store. It earns its place by being the only path to the two rules — a repository will happily store a post whose author does not exist.

blog exports one function, NewSchema. Everything else is under internal/, which is what lets examples/echo use the schema and not its internals.

Editing the schema

blog/schema.graphql is the source. blog/graph/schema/schema.graphql is a copy gqlc writes for the //go:embed; editing it is undone by the next go generate.

cd examples/blog && go generate

Authorization in storefront

blog shows how to build a schema. storefront shows what has to be true before one is deployed: every position declares who may reach it, and RequireAuthCoverage fails the build for a field that declares nothing.

Mechanism Where What it is for
@requiresScopes Customer.email, Order.margin, Mutation.refundOrder A scope on one position.
MarkerDirective @authenticated on Order, Customer, the roots A no-argument directive whose presence is the requirement. Type-level, so it is ANDed into every field below.
@authorizeInput(kind: FILTER) Query.orders(where:) Filtering by minMargin reads margin, so it needs the same scope as selecting it.
@authorizeInput(kind: WRITE) Mutation.placeOrder(input:) Naming someone else's customerId is a staff action.
@authorizeObject Order, Customer Which rows a principal may see, decided per value during execution and batched per list.
RequireAuthCoverage NewSchema A field with neither a requirement nor @public fails the build.

Four principals read the same graph and see different parts of it. Run the server and try them:

go run ./examples/storefront/cmd/server

# The header stands in for a verified token. Anything unrecognised is
# anonymous, because a credential the server cannot read is not one.
GQL='curl -s localhost:8080/graphql -H content-type:application/json'

# anonymous: Query.health and nothing else
$GQL -d '{"query":"{ health }"}'
$GQL -d '{"query":"{ orders { reference } }"}'

# staff: everything
$GQL -H authorization:staff -d '{"query":"{ orders { reference margin customer { name email } } }"}'

# support: every order, masked addresses, margin refused
$GQL -H authorization:support -d '{"query":"{ orders { reference customer { email } } }"}'
$GQL -H authorization:support -d '{"query":"{ orders { margin } }"}'

# a customer: their own two orders, and no sign that a third exists
$GQL -H authorization:customer:c1 -d '{"query":"{ orders { reference } }"}'

The four withholding outcomes are each used where they are right, which is a product decision and not a mechanical one:

  • Allow — the row is yours.
  • Redact — Customer.email is masked for a support agent, because an agent has to tell two customers apart. The field still resolves; Redact rewrites a result rather than preventing one, so it is wrong for anything whose computation is itself the secret.
  • Deny — Order.margin is refused outright. A zeroed margin is a number, and a wrong number is worse than a refusal. The field is non-null, so refusing it bubbles the order away; that is the visible cost of the choice.
  • Drop — a customer's listing omits rows that are not theirs, so it does not report how many exist. Valid only at a list element position, which AuthSite.ListElement is what tells the policy.

One limit the example states rather than hides: on a subscription the row check withholds the payload, not the event. A subscriber who may not see an order still receives an event carrying the refusal, so timing leaks even though contents do not. Withholding the event itself is the source's job.

Federation in federation

fed/'s own tests prove one subgraph answers _service and _entities correctly. They cannot show the thing federation actually is: an entity owned by one service and extended by another, joined at request time by a router neither service knows about.

products owns Product. reviews owns Review and contributes reviews and averageRating to a Product it knows nothing about except the key -- its ProductRef is one field wide, which is the point and not an oversight.

federation_test.go plays the router for the query neither service can answer, { topProducts { name reviews { rating } } }:

  1. Ask the owner, adding __typename and the key to what the client wanted.
  2. Turn each row into a representation: typename and key, nothing else.
  3. Ask the other subgraph by _entities.
  4. Merge by position.

Every step is asserted, because every step is where a subgraph is usually wrong: the __typename it forgets, the key it returns in the wrong scalar type, the row it drops instead of returning null. That last one is the worst of the three -- _entities answers positionally, so a dropped row shifts every row after it onto the wrong product, which is a wrong answer rather than a failed one. A test pins it.

What this does and does not prove. It proves both subgraphs hold up their half of the contract for a real cross-subgraph query, without either tool installed. It does not prove composition: that is rover supergraph compose's job, and cmd/subgraphs/supergraph.yaml is there to run it.

go run ./examples/federation/cmd/subgraphs

# what the router reads first
curl -s localhost:8080/products/graphql -H content-type:application/json -d '{"query":"{ _service { sdl } }"}'

# the second hop, by hand
curl -s localhost:8080/reviews/graphql -H content-type:application/json -d '{"query":"query($r:[_Any!]!){ _entities(representations:$r){ ... on Product { averageRating } } }","variables":{"r":[{"__typename":"Product","sku":"kb-01"}]}}'

Both subgraphs run in one process on two paths. That is not how they would be deployed -- it is the opposite of the point -- but a router reads each endpoint's _service and cannot tell.

Relay in relaynode

The relay package had no example before this one. Its own tests exercise every function, but a test shows one call at a time; what a reader needs is the three calls a connection takes sitting beside the SDL they bind to.

relay.Pagination()                                  // once per schema
relay.Bind[*Repository]("RepositoryConnection", "RepositoryEdge")  // once per connection
relay.FromSlice(rows, args)                         // in the resolver

relay.IDField is the one that is easy to get wrong. It writes base64("Type:id") for the id field -- byte for byte what graphql-relay-js and graphql-java produce. Using a plain Field("id", ...) that returns the local id compiles, validates, and is a perfectly good ID!, and then node(id:) fails for every object. A test pins the encoding for that reason.

node(id:) has three ways to find nothing and they are deliberately not the same answer: a well-formed id naming a missing row, and one naming a type this server does not serve, are both a null Node -- the id decoded, it just named nothing. Text that is not a global id at all is an error, because the client sent something it could not have received from this server.

examples/blog also has a Node interface, hand-written and without this package. Both are worth reading: blog shows that Query.node needs no engine support at all, since an unbound interface resolves from the dynamic Go type. relaynode shows what you get for not writing it yourself.

go run ./examples/relaynode/cmd/server

GQL='curl -s localhost:8080/graphql -H content-type:application/json'

# a connection hands back global ids and a cursor
$GQL -d '{"query":"{ users(first:1){ edges{ cursor node{ id name } } pageInfo{ hasNextPage endCursor } } }"}'

# the id from that response, handed straight back
$GQL -d '{"query":"query($id:ID!){ node(id:$id){ __typename ... on User { name } } }","variables":{"id":"VXNlcjox"}}'

velox, fx and Echo in veloxfx

blog writes its SDL by hand and generates models from it. veloxfx goes the other way, which is how an ORM-backed service is usually built: the velox schema in schema/ is the only source, and both the ORM and the SDL are output. Seven entities in three domains, served through Echo and assembled by uber/fx.

veloxfx/GUIDE.md is the developer guide, laid out like the gRPC basics tutorial: define, generate, implement, register, run, then the everyday loop of adding an entity, with every output recorded from a real run. In short:

schema/*.go + sdl/*.graphql --go generate--> velox/ (ORM), graph/<group>/ (interfaces, bindings),
                                             internal/<domain>/<group>.resolvers.go (stubs)
  • Nothing is converted by hand. id binds to velox's ID int (models: ID: int), and velox's order fields and cursors bind as they are (graphql.EnumMarshaler, graphql.ScalarMarshaler), so connection edges are velox's own methods and a resolver takes velox's types as arguments.
  • It is laid out like a gRPC service. Each group has a Resolver interface and a Bindings function; a domain package implements its groups and registers each with one gqlfx.Register line in its fx Module; nothing lists every entity.
  • The API offers operations, not the ORM's raw writes. Order and stock change only through placeOrder, cancelOrder, adjustStock and the like, each a conditional update or a transaction, and twenty concurrent orders for five units place exactly five.
  • Errors carry extensions.code, mapped in one presenter; a driver's message never reaches a client.
  • Adding an entity is 35 lines by hand, none of them a signature or a conversion.

It is a separate module because velox, fx and SQLite are dependencies of the example, not of the library. scripts/gate.sh finds it like every other go.mod.

cd examples/veloxfx
go generate            # after editing schema/ or sdl/
go run ./cmd/server

Running

go run ./examples/quickstart            # :8080 /graphql, /graphql/stream
go run ./examples/blog/cmd/server       # :8080 + /graphql/ws
go run ./examples/storefront/cmd/server # :8080, all three, with limits and a drain
go run ./examples/echo                  # same schema, Echo v5
go run ./examples/fiber                 # same schema, Fiber v3
go run ./examples/federation/cmd/subgraphs # two subgraphs on two paths
go run ./examples/relaynode/cmd/server   # global ids and connections
(cd examples/veloxfx && go run ./cmd/server) # velox + fx + Echo, own module

Directories

Path Synopsis
Package blog is the composition root for the layered example.
Package blog is the composition root for the layered example.
cmd/server command
Command server serves the example blog schema on every transport:
Command server serves the example blog schema on every transport:
graph
Package graph wires the generated GraphQL schema and its bindings.
Package graph wires the generated GraphQL schema and its bindings.
graph/model
Package model holds the types generated from the GraphQL schema.
Package model holds the types generated from the GraphQL schema.
internal/app
Package app holds the use cases: the work that is neither storage nor protocol.
Package app holds the use cases: the work that is neither storage nor protocol.
internal/domain
Package domain holds the blog's business entities.
Package domain holds the blog's business entities.
internal/repository
Package repository is the in-memory store standing in for a database.
Package repository is the in-memory store standing in for a database.
internal/transport/graphql
Package graphql adapts the domain to the generated GraphQL bindings.
Package graphql adapts the domain to the generated GraphQL bindings.
Command echo serves the examples/blog schema through Echo v5, on every transport:
Command echo serves the examples/blog schema through Echo v5, on every transport:
Package federation is two Apollo Federation subgraphs and the fetch a router performs across them.
Package federation is two Apollo Federation subgraphs and the fetch a router performs across them.
cmd/subgraphs command
Command subgraphs serves both federation subgraphs so a real router can be pointed at them:
Command subgraphs serves both federation subgraphs so a real router can be pointed at them:
Command fiber serves the examples/blog schema through Fiber v3, on every transport:
Command fiber serves the examples/blog schema through Fiber v3, on every transport:
Command quickstart is the smallest thing in this repository that runs: one SDL file, hand-written bindings, no code generation and no layers.
Command quickstart is the smallest thing in this repository that runs: one SDL file, hand-written bindings, no code generation and no layers.
Package relaynode is the Relay server contract end to end: global object identification and cursor connections, with nothing hand-rolled.
Package relaynode is the Relay server contract end to end: global object identification and cursor connections, with nothing hand-rolled.
cmd/server command
Command server serves the Relay example on /graphql, so the global-id round trip can be run against a real endpoint:
Command server serves the Relay example on /graphql, so the global-id round trip can be run against a real endpoint:
Package storefront is a back-office API written the way a deployment has to write one: every position says what it requires, and nothing is visible because nobody thought to ask for it.
Package storefront is a back-office API written the way a deployment has to write one: every position says what it requires, and nothing is visible because nobody thought to ask for it.
cmd/server command
Command server serves the storefront schema with the wiring a deployment needs and the other examples leave out: request limits, a cost budget per caller, persisted queries, OpenTelemetry, and a shutdown that winds down streams the HTTP server cannot.
Command server serves the storefront schema with the wiring a deployment needs and the other examples leave out: request limits, a cost budget per caller, persisted queries, OpenTelemetry, and a shutdown that winds down streams the HTTP server cannot.

Jump to

Keyboard shortcuts

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