In this quick guide we are going to implement the example Apollo Federation server in gqlgen. You can find the finished result in the examples directory.
Enable federation
Uncomment federation configuration in your gqlgen.yml
# Uncomment to enable federation
federation:
filename: graph/federation.go
package: graph
Federation 2
If you are using Apollo’s Federation 2 standard, your schema should automatically be upgraded so long as you include the required @link directive within your schema. If you want to force Federation 2 composition, the federation configuration supports a version flag to override that. For example:
federation:
filename: graph/federation.go
package: graph
version: 2
Create the federated servers
For each server to be federated we will create a new gqlgen project.
go tool gqlgen generate
Update the schema to reflect the federated example
type Review {
body: String
author: User @provides(fields: "username")
product: Product
}
extend type User @key(fields: "id") {
id: ID! @external # External directive not required for key fields in federation v2
reviews: [Review]
}
extend type Product @key(fields: "upc") {
upc: String! @external # External directive not required for key fields in federation v2
reviews: [Review]
}
and regenerate
go tool gqlgen generate
then implement the resolvers
// These two methods are required for gqlgen to resolve the internal id-only wrapper structs.
// This boilerplate might be removed in a future version of gqlgen that can no-op id only nodes.
func (r *entityResolver) FindProductByUpc(ctx context.Context, upc string) (*model.Product, error) {
return &model.Product{
Upc: upc,
}, nil
}
func (r *entityResolver) FindUserByID(ctx context.Context, id string) (*model.User, error) {
return &model.User{
ID: id,
}, nil
}
// Here we implement the stitched part of this service, returning reviews for a product. Of course normally you would
// go back to the database, but we are just making some data up here.
func (r *productResolver) Reviews(ctx context.Context, obj *model.Product) ([]*model.Review, error) {
switch obj.Upc {
case "top-1":
return []*model.Review{{
Body: "A highly effective form of birth control.",
}}, nil
case "top-2":
return []*model.Review{{
Body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.",
}}, nil
case "top-3":
return []*model.Review{{
Body: "This is the last straw. Hat you will wear. 11/10",
}}, nil
}
return nil, nil
}
func (r *userResolver) Reviews(ctx context.Context, obj *model.User) ([]*model.Review, error) {
if obj.ID == "1234" {
return []*model.Review{{
Body: "Has an odd fascination with hats.",
}}, nil
}
return nil, nil
}
Note
Repeat this step for each of the services in the apollo doc (accounts, products, reviews)
Create the federation gateway
npm install --save @apollo/gateway apollo-server graphql
const { ApolloServer } = require("apollo-server");
const { ApolloGateway, IntrospectAndCompose } = require("@apollo/gateway");
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "accounts", url: "http://localhost:4001/query" },
{ name: "products", url: "http://localhost:4002/query" },
{ name: "reviews", url: "http://localhost:4003/query" },
],
}),
});
const server = new ApolloServer({
gateway,
subscriptions: false,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Start all the services
In separate terminals:
go run accounts/server.go
go run products/server.go
go run reviews/server.go
node gateway/index.js
Query the federated gateway
The examples from the apollo doc should all work, eg
query {
me {
username
reviews {
body
product {
name
upc
}
}
}
}
should return
{
"data": {
"me": {
"username": "Me",
"reviews": [
{
"body": "A highly effective form of birth control.",
"product": {
"name": "Trilby",
"upc": "top-1"
}
},
{
"body": "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.",
"product": {
"name": "Trilby",
"upc": "top-1"
}
}
]
}
}
}
Using @requires
@requires enables you to define computed fields. In order for this to work, you need to be able to reference the values injected by the selection set inside the fields property of @requires.
gqlgen offers several mutually-exclusive strategies for delivering @requires
data to your code. They all answer the same question — how does the required
data reach the resolver? — so each entity uses exactly one:
| Strategy | Package option | requires: value |
How @requires data is delivered |
|---|---|---|---|
| Default | (none) | "default" |
Unmarshaled onto the returned entity, after the resolver runs. |
| Explicit | explicit_requires |
"explicit" |
A Populate<Entity>Requires function you implement, called on the returned entity after the resolver. Supports nested/array fields. |
| Computed | computed_requires |
@computedRequires (per field) |
Delivered to standalone field resolvers via a federationRequires argument (Federation 2 only). |
| Preloaded | preloaded_requires |
"preloaded" |
Unmarshaled onto the resolver’s input representation, before the resolver runs, so a multi resolver sees every entity’s @requires data at once. Per key, flat scalar/enum fields only, and requires multi; with one resolver per entity, any entity, nested and list fields included. |
The package option sets the default for the whole subgraph. The requires:
argument on @entityResolver overrides that default per entity, choosing
among default, explicit, and preloaded. (computed is not a requires:
value — it describes field-resolver delivery rather than how data reaches the
entity resolver, so it is selected by the computed_requires package option or,
per field, by @computedRequires.) Because @entityResolver is your own directive, add
the requires: String argument to its definition. With no requires: argument,
an entity falls back to the package default.
Computing individual @requires fields with @computedRequires
computed_requires computes every @requires field on its entities. To
compute just one field, annotate it with @computedRequires (a gqlgen-provided
directive; no declaration needed). That field is delivered to a standalone field
resolver, while the entity’s other @requires fields follow its strategy. This
is what lets a preloaded entity keep its scalar @requires on the batch input
and handle an object-typed @requires — which preloaded cannot reconstruct —
on the same entity:
type Product @key(fields: "id") @entityResolver(multi: true) {
id: ID! @external
category: String! @external
info: Info! @external
display: String! @requires(fields: "category") # preloaded onto the input
summary: String! @requires(fields: "info { label }") @computedRequires # computed field resolver
}
@computedRequires requires Federation 2 and call_argument_directives_with_null. It
only applies to @requires fields, and cannot be combined with the explicit
strategy (whose Populate<Entity>Requires hook already owns every @requires
field). See _examples/multi-entity-tests for a worked subgraph.
To mix whole-entity strategies instead — for example computed for an
object-typed entity and preloaded for a scalar one — make computed the
package default and override the scalar entity to preloaded:
directive @entityResolver(multi: Boolean, requires: String) on OBJECT
# with computed_requires: true, computed is the package default
type Planet @key(fields: "name") { ... }
type Product @key(fields: "id") @entityResolver(multi: true, requires: "preloaded") { ... }
In order to do this, you need to enable the federation.options.computed_requires flag. You also
need to enable call_argument_directives_with_null.
federation:
filename: graph/federation.go
package: graph
version: 2
options:
computed_requires: true
call_argument_directives_with_null: true
Once you do this, if you have @requires declared anywhere on your schema, you’ll see updates to the
genrated resolver functions that include a new argument, federationRequires, that will contain the
fields you requested in your @requires.fields selection set.
Note: currently it’s represented as a map[string]any where the contained values are encoded with
encoding/json. Eventually we will generate a typesafe model that represents these models, however that is a large lift. This typesafe support will be added in the future.
Example
Take a simple todo app schema that needs to provide a formatted status text to be used across all clients by referencing the assignee’s name.
type Todo @key(fields: "id") {
id: ID!
text: String!
statusText: String! @requires(fields: "assignee { name }")
status: String!
owner: User!
assignee: User! @external
}
type User @key(fields: "id") {
id: ID!
name: String! @external
}
The statusText resolver function is updated and can be modified accordingly to use the todo representation with the assignee name.
func (r *todoResolver) StatusText(ctx context.Context, entity *model.Todo, federationRequires map[string]interface{} /* new argument generated onto your resolver function */) (string, error) {
if federationRequires["assignee"] == nil {
return "", nil
}
// federationRequires will contain the "assignee.name" field provided by the Federation router
statusText := entity.Status + " by " + federationRequires["assignee"].(map[string]interface{})["name"].(string)
return statusText, nil
}
[DEPRECATED] Alternate API
Note: it’s not recommended to use this API anymore. See the
Using @requiressection for the recommend API.
If you need to support nested or array fields in the @requires directive, this can be enabled in the configuration by setting federation.options.explicit_requires to true.
federation:
filename: graph/federation.go
package: graph
version: 2
options:
explicit_requires: true
Enabling this will generate corresponding functions with the entity representations received in the request. This allows for the entity model to be explicitly populated with the required data provided.
Example
Take a simple todo app schema that needs to provide a formatted status text to be used across all clients by referencing the assignee’s name.
type Todo @key(fields: "id") {
id: ID!
text: String!
statusText: String! @requires(fields: "assignee { name }")
status: String!
owner: User!
assignee: User!
}
type User @key(fields: "id") {
id: ID!
name: String! @external
}
A PopulateTodoRequires function is generated, and can be modified accordingly to use the todo representation with the assignee name.
// PopulateTodoRequires is the requires populator for the Todo entity.
func (ec *executionContext) PopulateTodoRequires(ctx context.Context, entity *model.Todo, reps map[string]interface{}) error {
if reps["assignee"] != nil {
entity.StatusText = entity.Status + " by " + reps["assignee"].(map[string]interface{})["name"].(string)
}
return nil
}
One resolver per entity
By default gqlgen generates one entity resolver per @key. With the
entity_resolver_per_entity package option it generates a single resolver for
each entity, which receives the keys the router sent as one representation:
federation:
filename: graph/federation.go
package: graph
version: 2
options:
entity_resolver_per_entity: true
type Port @key(fields: "_id") @key(fields: "id") {
_id: String! @goField(name: "LegacyID")
id: ID!
}
func (r *entityResolver) FindPort(ctx context.Context, rep model.PortRepresentation) (*model.Port, error) {
switch rep.MatchedKey {
case "_id":
return r.portByLegacyID(ctx, *rep.LegacyID)
case "id":
return r.portByID(ctx, *rep.ID)
}
return nil, fmt.Errorf("unexpected key %q", rep.MatchedKey)
}
-
PortRepresentationis a generated input holding every key field ofPort, all nullable: a key the router did not send isnil. A nested key such asowner { id }gets a representation of the nested type (OwnerRepresentation), shared by every entity whose keys select into it. -
MatchedKeyholds the fields of the@keythe representation matched: the first key, in declaration order, whose fields are all present and not all null. gqlgen prints it in a standard form: single spaces, and a nested selection in braces, such as"id owner { id }". gqlgen rejects a representation that matches no key, so a resolver is never called without one. -
Representation fields take the entity’s Go field names, so
@goField(name: ...)orfieldNameconfig onPortalso applies toPortRepresentation. AfieldNameset onPortRepresentationitself takes precedence. As for any generated model, generation fails if two fields of one representation have the same Go name, such as_idandidwithout@goField. -
With
multi: truethe resolver takesreps []*model.PortRepresentation, and each representation in a batch can use a different key. -
The resolver name comes from the entity name alone (
findPort), so keys whose per-key resolver names collide, such asidandi { d }, need no distinct names (compare Entity resolver names). -
An interface entity gets a resolver that returns the interface, such as
findNode(rep: NodeRepresentation!): Node!, and serves representations whose__typenameis the interface.@requireson such an interface is rejected, as federation does not allow it. -
With the
preloadedstrategy,PortRepresentationalso holds the entity’s@requiresfields, other than@computedRequiresones, nullable like the keys: a field the router did not send isnil. This works for single and multi resolvers, for nested selections such asorbit { period }, and for lists:ships { tonnage }givesships: [ShipRepresentation!]. gqlgen does not copy them onto the returned entity. A selection with an inline fragment, such asowner { ... on Person { id } }, is not supported, because inputs have no unions. -
A key that selects a list or union field, such as
@key(fields: "owners { id }"), is rejected, as federation does not allow it. A key through an interface field, such asholder { id }, is accepted, as per-key resolvers serve it, and gets a representation of the interface’s fields; federation does not allow it either, so composition rejects the subgraph. -
When a schema type has a representation’s Go name, such as
portRepresentationwithPortRepresentation, name the representation instead, keyed by the type it represents. Its Go name follows from the name, and itsmodelssettings, such asfieldName, move to that name:federation: representation_names: Port: PortKey
The option applies to every entity. Generation fails, keeping the whole schema
on per-key resolvers until the cause changes, when any entity has a preloaded
@requires with an inline fragment, which per-key preloaded does not support
either, or uses @requires on an interface or a key through a list or union
field, which federation does not allow.
Migrating
The option moves every entity at once; the router and supergraph need no change.
- Set
entity_resolver_per_entity: true. - Run
gqlgen generate. For each entity, its per-key resolvers, such asFindPortByIDandFindPortByLegacyID, become one stub, such asFindPort, and their bodies move to the!!! WARNING !!!comment at the end of the resolver file. - Move those bodies into helpers now: the comment holds only what the latest
run removed, so the next
gqlgen generatedeletes them. - Implement
FindPortby switching onrep.MatchedKeyand calling the helper for that key, as in the preceding example. Withmulti: true, each representation inrepshas its ownMatchedKey; return the entities in the order ofreps, and report per-entity errors with agraphql.BatchErrorList. - With
preloaded, read the@requiresfields from the representation instead of the…ByKeysInputbatch input. They are pointers now, sorep.Categoryisnilwhen the router did not send it. - Update code and tests that call the per-key methods directly. Tests that go
through
_entitiesneed no change.
_service { sdl } does not change: the representations and findPort are not
part of it. Explicit @requires keeps its Populate<Entity>Requires function.
To undo, remove the option, regenerate, and call the helpers from the per-key
stubs.
Roadmap — the default will change. The option is a temporary opt-in. A future release plans to make one resolver per entity the default, with
entity_resolver_per_entity: falsekeeping per-key resolvers (see Entity resolver names). A later release plans to remove per-key resolvers. Setting the option now means your schema won’t change when the default flips.
Using @entityResolver
The @entityResolver directive enables optimization for entity resolver generation in GraphQL federation.
Configuration
To use this feature, define the @entityResolver(multi: Boolean) directive on your OBJECT types. Federated entities must be annotated with this directive to enable the functionality.
Example:
type MultiHello @key(fields: "name") @entityResolver(multi: true)
Global Configuration
You can enable this feature by default by setting the federation.options.entity_resolver_multi flag in your configuration:
federation:
filename: graph/federation.go
package: graph
version: 2
options:
entity_resolver_multi: true
To generate one resolver per entity instead, see One resolver per entity.
Roadmap — the default will change. A future major version plans to make
multithe default (single-entity resolution becomes “multi with N = 1”) and to change the single-entity resolver signature so it takes the same input struct as the multi resolver —FindProductByID(ctx, rep *model.ProductByIDsInput)instead of positional key arguments (FindProductByID(ctx, id string)). To be unaffected when the default flips, declaremulti:explicitly on your entities now (multi: trueto opt into batching,multi: falseto keep single-entity resolution); entities that already state their choice see no change. See_examples/multi-entity-tests/PLAN.mdfor the sequencing.
Schema Example
directive @entityResolver(multi: Boolean) on OBJECT
type User @key(fields: "id") @entityResolver(multi: true) {
id: ID!
name: String!
}
After defining your schema, regenerate the code:
go run github.com/99designs/gqlgen
Implementation
Implement the generated resolver method:
// IMPORTANT: The output slice order is critical and must match the input slice order exactly!
func (r *entityResolver) FindUserByIDs(ctx context.Context, reps []*entity.UserByIDsInput) ([]*model.User, error) {
output := make([]*model.User, len(reps))
for i, user := range reps {
output[i] = &model.User{
ID: user.ID,
Name: "User " + user.ID,
}
}
return output, nil
}
When configured, the federation plugin creates an entity resolver that accepts a list of representations, improving performance by reducing the number of individual resolver calls.
Resolver contract. Return a newly-allocated slice of the same length and order as the input —
out[i]must correspond toreps[i](the runtime places results by position). Treat the input representations as read-only; don’t retain or mutate them.
Preloaded: @requires data in one scope
By default a multi entity resolver receives only the entity’s @key fields (in
a generated …ByKeysInput struct). Any @requires fields are unmarshaled onto
each returned entity after the resolver runs, one entity at a time. That is a
problem when the work you do with @requires data is naturally batched — for
example, one machine-learning inference call that scores every entity at once —
because the resolver never sees all entities’ @requires data together.
Enable federation.options.preloaded_requires to change the
multi resolver so each input element carries both the @key fields and the
entity’s @requires fields, populated before the resolver is called:
federation:
filename: graph/federation.go
package: graph
options:
preloaded_requires: true
With the option set, the input struct gains the @requires fields, so the whole
batch’s @requires data is available in a single call:
// reps[i].Category is a @requires field, populated before this call.
func (r *entityResolver) FindManyProductByIDs(
ctx context.Context,
reps []*model.ProductByIDsInput,
) ([]*model.Product, error) {
// All products' @requires data is visible here at once — score the batch
// in a single pass instead of once per product.
return scoreBatch(ctx, reps)
}
Notes and limitations:
- Per key, only flat scalar and enum
@requiresfields are supported, on multi entities. With one resolver per entity, the representation carries them instead, for single resolvers too, and nested and list paths work. Per key, gqlgen can only reconstruct scalar leaves of a representation, so a@requiresnaming an object or list field, or a nested path such as@requires(fields: "world { foo }"), is rejected at generation time. Require the scalar leaves instead (@requires(fields: "world { foo }")→ requirefoo). - It cannot be combined with
explicit_requiresorcomputed_requires: those strategies own@requireshandling in incompatible ways, so the generator rejects the combination rather than silently dropping data.
Per-entity errors in a batch
A multi entity resolver normally returns ([]*T, error); a non-nil error fails
the whole batch group. To fail only specific entities while the rest still
resolve, return a graphql.BatchErrorList — a slice the same length as the
batch, with a non-nil entry for each entity that failed and nil for the ones
that succeeded:
func (r *entityResolver) FindManyProductByIDs(
ctx context.Context,
reps []*model.ProductByIDsInput,
) ([]*model.Product, error) {
out := make([]*model.Product, len(reps))
errs := make([]error, len(reps))
var failed bool
for i, rep := range reps {
p, err := r.load(ctx, rep)
if err != nil {
errs[i] = err // this entity fails…
failed = true
continue
}
out[i] = p // …this one succeeds
}
if failed {
return out, graphql.BatchErrorList(errs)
}
return out, nil
}
The generated runtime nulls each failed entity, reports its error against the
_entities[index] response path, and still returns the entities that
succeeded. Returning any other (non-BatchErrors) error preserves the original
all-or-nothing behavior for the group. This works for every multi entity
resolver, with or without preloaded_requires.
Entity resolver names
Each @key gets a resolver named after the entity and its key fields:
@key(fields: "id") on Port gives FindPortByID, or FindManyPortByIDs
with its PortByIDsInput when the entity sets multi.
Different keys can produce the same name, because the Go form of a field name
drops underscores and case: _id and id both become ID. Such a model
needs @goField(name:) to compile anyway, and gqlgen names the colliding
resolvers from it:
directive @goField(name: String) on FIELD_DEFINITION
type Port @key(fields: "_id") @key(fields: "id") {
_id: String! @goField(name: "LegacyID")
id: ID!
}
This generates FindPortByLegacyID and FindPortByID. A fieldName set in
gqlgen.yml works the same way, and @goField takes precedence over it:
models:
Port:
fields:
_id:
fieldName: LegacyID
Keys whose names don’t collide keep their usual names, even when a key field has one of these. A model you bind yourself counts only through these two settings, not through struct tags or field-name matching.
When the names would still collide, generation fails with an error that says what to change:
- Two keys on one entity, such as
idandi { d }: give one of their key fields a distinct@goField(name:)orfieldName. If no two key fields of one type share a Go name, you can instead use one resolver per entity._idandidshare the Go nameID, so they need@goFieldorfieldNameeither way. - The same
@keydeclared twice: remove one. - Two entities, such as a single
ManyPort @key(fields: "IDs")and a multiPort @key(fields: "id"): you can’t rename either entity, so give both the same@entityResolver(multi:)setting, or use one resolver per entity. - A batch input with the Go name of one of your types: rename your type.
Check your resolver bodies when a collision first appears. Suppose
Porthad only@key(fields: "_id"), resolved byFindPortByID. Adding@key(fields: "id")with@goField(name: "LegacyID")on_idgives the_idkey the new resolverFindPortByLegacyID, while the existingFindPortByIDbody now serves theidkey. Move the body toFindPortByLegacyID.