Introduction
In our previous post, we had an overview of GraphQL, its pros and cons, an example of a GraphQL server implemented in Go, and some tinkering with it.
In this post, we take the basic example a step further: how multiple GraphQL servers (also called subgraph in the context of GraphQL federation) work together with the orchestration of a central router (also called gateway). We will walk through a GraphQL federation setup that can run in our local machine, examining the subgraphs, composing a supergraph from their schema, configuring the router before running them all together and trying a query to see how they all work in tandem.
We will only walk through the important bits, but the entire project can be browsed here.
Project structure
The project contains different components that consistute a federated GraphQL architecture, including:
- Apollo Router: The entry-point of all queries, orchestrating requests cross various subgraphs.
- Product Subgraph: A subgraph responsible for fetching product related data.
- Review Subgraph: A subgraph responsible for fetching reviews for specified products.
- Shipping Subgraph: A subgraph responsible for calculating shipping details for a product.
Subgraphs setup
All subgraphs can be set up the same as the subgraphs in GraphQL basics post, with the additional config to enable federation in gqlgen.yml
in each subgraph:
federation:
filename: graph/federation.go
package: graph
version: 2
options:
computed_requires: true
For simplicity, all subgraphs resolve data using hard-coded data in their resolvers. In a production system, the data usually come from external sources like a database, or a backend API.
Apollo Router setup
The Apollo Router, at the time this is written, is a Rust application, and can be run using a Docker image. We enable it as one of the services in docker-compose.yml
services:
router:
image: ghcr.io/apollographql/router:v2.3.0
ports:
- "4000:4000"
volumes:
- ./router/supergraph.graphql:/dist/config/supergraph.graphql
- ./router/router-config.yaml:/dist/config/router.yaml
command: [
"--dev",
"-c",
"config/router.yaml",
"-s",
"config/supergraph.graphql"
]
depends_on:
- product
- review
- shipping
// product, review and shipping subgraphs containers definition...
To have a better experience in local development, we need to enable some extra configurations in our router config, which can be seen in router/router-config.yaml
. One important configuration is:
sandbox:
enable: true
This enables us to access http:localhost:4000/graphql
to access a sandbox environment to send a query to our router. The alternative is to use cURL
, but the sandbox UI provides a much more user friendly experience.
In addition, we also need to tell the router how to reach out to our subgraphs. This is done in router/supergraph-config.yaml
.
Lastly, we have to specify the supergraph schema for the router. The supergraph schema is composed from product subgraph schema, review subgraph schema, and shipping subgraph schema. We add a helper command to help compose this subgraph schema more easily:
make compose-subgraph
We need to re-compose supergraph schema everytime we make change to any of the subgraphs’ schema before restarting the router.
Testing the setup
After composing the supergraph schema by make compose-subgraph
, we can start running all our containers with a helper command make start
(or if we want to stop all containers, simply run make stop
). When all containers have started, we can explore our federation setup through http://localhost:4000/graphql
.
The sandbox environment provides a few useful utilities:
- Schema provides an overview of our supergraph schema, the subgraphs and their types can also be visualized to give us a more holistic view of our entire federation.
- Schema Diff allows us to see the difference between two selected schemas in a fashion similar to Github PRs diff. This requires an Apollo account registration though.
- Checks allows us to see schema check operation, which runs every time schema composition happens to make sure the syntax is correct, no errors between subgraphs’ schemas, and no breaking changes introduced.
- Explorer allows us to run queries against the router, and get the results. Optionally, we can see the query plan to know how the router distributes the workload among subgraphs.
The Explorer is the most interesting part of the sandbox, so let’s dive right in to see how it works. Copy and paste this query to the Operation window:
query GetProductList {
productList {
id
name
reviews {
body
}
estimatedDeliveryTime
}
}
If everything works well, we should see something like this in the response window
{
"data": {
"productList": [
{
"id": "1",
"name": "sample-product-1",
"reviews": [
{
"body": "very good product"
},
{
"body": "Decent product, but I found better on other platforms"
}
],
"estimatedDeliveryTime": "2025-06-24T01:36:18Z"
},
{
"id": "2",
"name": "sample-product-2",
"reviews": [
{
"body": "Very mediocre, should not buy"
}
],
"estimatedDeliveryTime": "2025-06-24T01:36:18Z"
},
{
"id": "3",
"name": "sample-product-3",
"reviews": null,
"estimatedDeliveryTime": "2025-07-06T01:36:18Z"
}
]
}
}
We can see the plan of this query by changing to a Query plan view.
In this case, the router is smart enough to determine that fetching review and fetching shipping information are two independant tasks, and therefore parallelize these tasks. All of this is done on the router level without our intervention!
Apollo Federation directives
In this section we explore each subgraph’s schema, focusing on the federation directives that Apollo provides. They’re very important, being the glue across our subgraphs and the router.
Product Subgraph
The product subgraph schema is fairly simple: only a query, and the Product
entity:
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
weight: Float
}
An entity is defined in Apollo documentation:
In Apollo Federation, an entity is an object type that you define canonically in one subgraph and can then reference and extend in other subgraphs.
In other words, if we want other subgraphs to contribute to another type in another subgraph, we need to make that type an entity.
To make a type an entity, we use the @key
directive, which accept at least 1 field to construct the entity key.
Review Subgraph
We extend Product
entity in Review subgraph so that we can contribute reviews data to a product:
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]
}
Since we’re only extending Product
, we don’t have to repeat all the other fields like name
and price
of Product
. We do have to repeat id
because it’s the Product
entity’s key. With this schema, Review Subgraph is telling the Apollo Router: “I can resolve reviews data for a product entity”, which is why the Router knows to route our sample request to Review Subgraph to resolve reviews for each product.
Shipping Subgraph
Similar to Review Subgraph, we also extend Product
entity in Shipping Subgraph so that we can contribute estimatedDeliveryTime
to a product:
type Product @key(fields: "id") {
id: ID!
price: Float! @external
weight: Float @external
estimatedDeliveryTime: DateTime @requires(fields: "price weight")
}
However, the contribution is done differently here. Because Shipping Subgraph needs product weight and price to determine estimatedDeliveryTime
, we need to use the @external
and @requires
directives here. Fields like estimatedDeliveryTime
are often called computed fields, because their values are computed by taking in other field values.
- The
@external
directive lets Shipping Subgraph tell the Router: “I need product price and weight in our schema, but I’m not responsible for resolving these fields” - The
@requires
directive lets Shipping Subgraph tell the Router: “In order to compute estimatedDeliveryTime for a product, I need you to resolve product price and weight first”
price
and weight
in this case are federated fields, which can be obtained in Shipping Subgraph resolver through federationRequires
map:
// EstimatedDeliveryTime is the resolver for the estimatedDeliveryTime field.
func (r *productResolver) EstimatedDeliveryTime(ctx context.Context, obj *model.Product, federationRequires map[string]any) (*string, error) {
var productPrice float64
if price, ok := federationRequires["price"]; ok {
convertedPrice := price.(json.Number)
productPrice, _ = convertedPrice.Float64()
}
var productWeight *float64
if weight, ok := federationRequires["weight"]; ok {
convertedWeight, ok := weight.(json.Number)
if ok {
weight, _ := convertedWeight.Float64()
productWeight = &weight
}
}
// determine estimatedDeliveryTime using price and weight...
}
Conclusion
In this post, we have set up a GraphQL federation architecture with a central router and 3 subgraphs. We walked through the setup, how to use the Apollo sandbox environment to send a query to the router, and familiarized ourselves with some Apollo federation directives. Note that there are many other directives that can be helpful in other use cases.
In the next post, we’ll explore a few other useful directives of Apollo federation.