Apollo Federation¶
What is Apollo Federation¶
Apollo Federation is a technology to implement distributed GraphQL by composing multiple subgraphs into one supergraph.
In the terms of Apollo Federation:
A subgraph is a standalone GraphQL server with enabled federation support. Hiku supports Apollo Federation and can be used to implement a subgraph.
A supergraph is a composition of multiple subgraphs into a single data graph. To run a supergraph, you need to use special gateway (or sometimes called router) that will route requests to the appropriate subgraph and combine the results into a unified response.
Hiku supports both Federation v1 and v2, but it is recommended to use v2 as v1 is deprecated.
For more details about Apollo Federation v2, please refer to Apollo GraphQL Federation v2 Docs.
The specification for subgraphs can be found here: Apollo GraphQL Subgraph Spec.
How it Works¶
In a federated architecture, there are two main components: the gateway/router and the subgraphs.
In order to run supergraph you need to compose one.
There are two primary methods to compose a supergraph:
Using schema registry (managed federation in Apollo Studio is one of them), where composition happens automatically when we push new subgraph schema
Manually, using the Rover CLI, by providing a configuration file with subgraph URLs and routing URLs. The Rover CLI will then fetch remote services and compose schema into a file.
Then you can run gateway/router with the composed schema and router should start routing requests to the appropriate subgraph and combine the results into a unified response.
For more detailed information, please refer to the Apollo GraphQL Federation Setup Guide.
Setup Federation Subgraph¶
Note
You can find the source code for this example on GitHub.
Let’s start with a simple example of a federated subgraph using the following GraphQL schema:
Order Service
type Order @key(fields: "id") {
id: ID!
status: Int!
cartId: Int!
}
type Query {
order: [Order!]!
}
Shopping Cart Service
type ShoppingCart @key(fields: "id") {
id: ID!
items: [ShoppingCartItem!]!
}
type ShoppingCartItem {
id: ID!
productName: String!
price: Int!
quantity: Int!
}
type Query {
cart(id: ID!): ShoppingCart
}
Now let’s implement the Order service using Hiku:
from flask import Flask, request, jsonify
from hiku.federation.graph import Graph, FederatedNode
from hiku.federation.schema import Schema
from hiku.federation.directive import Key
from hiku.graph import Root, Field, Link, Option
from hiku.types import ID, Integer, TypeRef, Optional
from hiku.executors.sync import SyncExecutor
from hiku.utils import to_immutable_dict
app = Flask(__name__)
def resolve_order_reference(representations):
return [to_immutable_dict(r) for r in representations]
def order_fields_resolver(fields, representations):
orders = []
for rep in representations:
if 'id' in rep:
orders.append(get_order_by_id(rep['id']))
elif 'cartId' in rep:
orders.append(get_order_by_cart_id(rep['cartId']))
def gen_fields(field, order):
if field == 'id':
return order.id
elif field == 'status':
return order.status
elif field == 'cartId':
return order.cart_id
return [[gen_fields(f.name, o) for f in fields] for o in orders]
def direct_link(ids):
return ids
QUERY_GRAPH = Graph([
FederatedNode(
'Order', [
Field('id', ID, order_fields_resolver),
Field('status', Integer, order_fields_resolver),
Field('cartId', Integer, order_fields_resolver),
],
directives=[Key('id'), Key('cartId')],
resolve_reference=resolve_order_reference
),
Root([
Link(
'order',
Optional[TypeRef['Order']],
direct_link,
requires=None,
options=[
Option('id', Integer)
],
),
]),
])
schema = Schema(SyncExecutor(), QUERY_GRAPH)
@app.route('/graphql', methods={'POST'})
def handle_graphql():
data = request.get_json()
result = schema.execute_sync(data)
resp = jsonify({"data": result.data})
return resp
if __name__ == '__main__':
app.run(host='0.0.0.0', port=4001)
Using
hiku.federation.graph.Graphinstead ofhiku.graph.GraphUsing
hiku.federation.graph.FederatedNodeinstead ofhiku.graph.NodeAlso using
hiku.federation.schema.Schemainstead ofhiku.schema.SchemaBy default, the federation version is set to 2. To enable v1, you need to pass
federation_version=1to theSchemaconstructor.
We define the Order type with the @key directive. This directive specifies the primary key of the type.
In our case, id is the primary key of the Order type.
Router now knows, that in order for it to fetch an order, it needs to provide the id field value when requesting Order service.
Router then joins different parts of data from different subgraphs into one response using the Key.
A type can have many Key directives. We define cartId as another key.
This will allow us to join Order and ShoppingCart types together.
Also we define resolve_reference function which in our case is resolve_order_reference.
A resolve_reference function works very similar to Link resolver -
it returns a list of values that will be passed to Node as a root values.
Note
Since we want to pass representations as is to Node`, we need to convert each representation to ``ImmutableDict because resolve_reference function must return hashable values just like Link resolver.
Then in order_fields_resolver we fetch orders either by id or by cartId.
Note
In the example above, we fetch orders by one. In real-world applications, you would fetch orders in butches to avoid N+1.
Next, let’s implement the ShoppingCart service using Hiku:
from flask import Flask, request, jsonify
from hiku.federation.graph import Graph, FederatedNode
from hiku.federation.schema import Schema
from hiku.federation.directive import Key
from hiku.graph import Root, Node, Field, Link, Option
from hiku.types import ID, Integer, Sequence, String, TypeRef, Optional
from hiku.executors.sync import SyncExecutor
from hiku.utils import to_immutable_dict
app = Flask(__name__)
def resolve_reference_by(key):
def resolver(representations):
return [r[key] for r in representations]
return resolver
def cart_fields_resolver(fields, cart_ids):
carts = get_carts_by_ids(cart_ids)
def gen_fields(field, cart):
if field == 'id':
return cart.id
return [[gen_fields(f.name, c) for f in fields] for c in carts]
def link_cart_items(cart_ids):
return [get_cart_items_ids(cart_id) for cart_id in cart_ids]
def cart_item_fields_resolver(fields, cart_item_ids):
items = get_cart_items_by_ids(cart_item_ids)
def gen_fields(field, item):
if field == 'id':
return item.id
elif field == 'productName':
return item.product_name
elif field == 'price':
return item.price
elif field == 'quantity':
return item.quantity
return [[gen_fields(f.name, i) for f in fields] for i in items]
def order_fields_resolver(fields, cart_ids):
def gen_fields(field, cart_id):
if field == 'cartId':
return cart_id
return [[gen_fields(f.name, cid) for f in fields] for cid in cart_ids]
def direct_link(ids):
return ids
QUERY_GRAPH = Graph([
Node('ShoppingCart', [
Field('id', ID, cart_fields_resolver),
Link(
'items',
Sequence[TypeRef['ShoppingCartItem']],
link_cart_items,
requires='id'
),
]),
Node('ShoppingCartItem', [
Field('id', ID, cart_item_fields_resolver),
Field('productName', String, cart_item_fields_resolver),
Field('price', Integer, cart_item_fields_resolver),
Field('quantity', Integer, cart_item_fields_resolver),
]),
FederatedNode(
'Order', [
Field('cartId', ID, order_fields_resolver),
Link(
'cart',
TypeRef['ShoppingCart'],
direct_link,
requires='cartId'
),
],
directives=[Key('cartId')],
resolve_reference=resolve_reference_by("cartId")
),
Root([
Link(
'cart',
Optional[TypeRef['ShoppingCart']],
direct_link,
requires=None,
options=[
Option('id', Integer)
],
),
]),
])
schema = Schema(SyncExecutor(), QUERY_GRAPH)
@app.route('/graphql', methods={'POST'})
def handle_graphql():
data = request.get_json()
result = schema.execute_sync(data)
resp = jsonify({"data": result.data})
return resp
if __name__ == '__main__':
app.run(host='0.0.0.0', port=4001)
In the ShoppingCart service, we define the ShoppingCart and ShoppingCartItem types.
But also, we define a stub Order type. This is needed because we want to extend the Order type with a cart field.
In the `Order type, we specify cartId as a key. This will allow us to join Order and ShoppingCart types together.
Now we need to compose subgraph schemas into a supergraph schema and run an instance of the router.
Start the Order service on port 4001 and the ShoppingCart service on port 4002.
Apollo Router¶
With our services up and running, we need to configure a gateway to consume our services. Apollo provides a router for this purpose.
Before proceeding, install the Apollo Router by following their installation guide. Also, install Apollo’s CLI (rover) here to compose the schema.
Create a file named supergraph.yaml with the following contents:
federation_version: 2.3
subgraphs:
order:
routing_url: http://localhost:4001/graphql
schema:
subgraph_url: http://localhost:4001/graphql
shopping_cart:
routing_url: http://localhost:4002/graphql
schema:
subgraph_url: http://localhost:4002/graphql
This file will be used by Rover to compose the schema, which can be done with the following command:
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql
With the composed schema, we can now start the router:
./router --supergraph supergraph-schema.graphql
With the router running, visit http://localhost:4000 and try running the following query:
{
order(id: 1) {
id
status
cart {
id
items {
id
productName
price
}
}
}
}