GraphQL Federation is a powerful mechanism for combining multiple GraphQL APIs into a single endpoint, creating a unified graph that spans multiple services, each with its own schema. This is useful when integrating multiple services with their own GraphQL APIs to combine them into a single API. These APIs are called subgraphs.
AWS AppSync is a managed GraphQL service that makes it easy to build GraphQL APIs. It allows you to define a schema and resolvers and plug them into data sources like DynamoDB, RDS, EventBridge, or Lambda functions.
In this post we'll demonstrate how to federate GraphQL APIs, including AWS AppSync subgraphs, using Grafbase to expose them as part of a Federated Graph.
But before we start, let's understand the basics of GraphQL Federation.
In a federated architecture, the Grafbase gateway serves the public API and sits in front of multiple GraphQL services, exposing a common API. Each service is called a subgraph. The gateway plans and executes requests to the subgraphs to fetch the necessary data, potentially in multiple steps due to dependencies.
Subgraphs are standard GraphQL APIs that comply with the federation specification by exposing additional special fields, one of which we will see below. For example, take the following schema for a product
with a Product
type:
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}
type Query {
allProducts: [Product!]!
}
The @key
directive on the Product
type defines it as an entity. Entities can be defined in multiple subgraphs, each contributing different fields, and referenced by their key. Let's continue this example with a second subgraph called inventory
, with the following schema:
type Product @key(fields: "id") {
id: ID!
stock: Int!
}
Here the stock
field is only defined in the inventory
subgraph. The gateway is responsible for fetching the stock
field from the inventory
subgraph when needed. Once both subgraph schemas are published to the schema registry, the gateway exposes an API that contains a unified Product
type with all of its fields (name
, price
, and stock
).
When presented with a query like this:
query {
allProducts {
name
price
stock
}
}
To execute the request, the gateway goes through the following steps:
- first request the
product
subgraph to fetch thename
andprice
fields. It will also request theid
field, although it wasn't part of the query (it won't be in the response), - then request the
inventory
subgraph using the specialQuery._entities
field (part of the federation subgraph specification) to fetch the Product by id as well as thestock
field. - The data is joined, and the products are returned.
All the calls are batched, meaning that no matter how many products are returned by Query.allProducts
, there will be only two internal HTTP requests to resolve the query: one to the products subgraph and one to the inventory subgraph.
Entities are one core mechanism of federation, but other directives enable very powerful composition of subgraph APIs. Additional directives like @requires
directive and @override
enable powerful compositions across subgraphs, allowing declaring data requirements from other subgraphs and seamless field migrations without hardcoding subgraph dependencies, respectively.
To integrate an AWS AppSync API with a federation gateway, your schema and resolvers must comply with the Apollo Federation specification. This requires defining special fields and directives in your schema.
To expose your AppSync schema to the Apollo Federation gateway, implement a _service
query that returns the schema definition. Additionally, define an _entities
query to resolve references to entities from other subgraphs.
Here is an example of how to define these queries in an example AppSync schema:
type Query {
_service: _Service!
_entities(representations: [_Any!]!): [_Entity]!
order(id: ID!): Order
}
type _Service {
sdl: String
}
type Order {
id: ID!
status: String!
}
type Product {
id: ID!
}
input _Any {
__typename: String!
id: String
}
union _Entity = Order | Product
Note that we only define the id
field on Product
. This is because Product
is an entity that we assume has fields defined in other subgraphs.
Next, add the @key(fields: "id")
directive on both Product
and Order
. This tells the gateway that these types are entities and can be resolved by the _entities
query.
The _service
query returns the schema definition as a string, while the _entities
query takes a list of representations and returns the corresponding entities.
Here is a sample Lambda resolver for the _service
and _entities
queries:
import { AppSyncResolverEvent } from "aws-lambda";
// Handler resolving the entities from the representations argument
export const handler = async (event: AppSyncResolverEvent<any>) => {
console.log(`event ${JSON.stringify(event)}`);
let result: any = [];
switch (event.info.parentTypeName) {
case "Query":
switch (event.info.fieldName) {
case "_service":
result = { sdl: process.env.SCHEMA };
break;
case "_entities":
const { representations } = event.arguments;
const entities: any[] = [];
for (const representation of representations) {
const { __typename, id } = representation;
switch (__typename) {
case "Order":
entities.push({ __typename, id, status: "shipped" });
break;
case "Product":
entities.push({ __typename, id });
break;
}
}
break;
}
break;
}
return result;
};
Note: this code was adapted from this AWS blog post.
Of course, the actual implementation of the _entities
resolver will depend on your data sources: we used hardcoded data in this example. You would fetch the relevant Order
or Product
fields from your data source in the branches of the switch
statement.
You would attach that resolver to the _service
and _entities
query fields in your AppSync API.
Once your AppSync API is configured with the necessary federation fields and resolvers, publish the subgraph's schema to the Grafbase schema registry. This allows the Grafbase gateway to include your AppSync API as part of the federated graph. If you haven't done so, log in on the Grafbase dashboard and create a graph. You can also publish the new AppSync subgraph in an existing graph, if you already have one.
To publish the schema, run the following command with the Grafbase CLI:
$ grafbase publish my-organization/my-graph --name orders --schema ./path/to/your/schema.graphql --url https://your-appsync-api-endpoint/graphql
With the appropriate values. The --name
argument identifies the subgraph for subsequent publishes and diagnostics.
You can now install and run the Grafbase gateway.