Composing multiple GraphQL schemas into a single endpoint gives developers the ability to develop, deploy, and scale their services independently while exposing them as a single GraphQL schema. There are many patterns and approaches to schema composition within the GraphQL community, including both build time approaches, such as Merged APIs in AWS AppSync, and runtime approaches, such as Schema Stitching and Apollo Federation. Merged APIs allow customers to develop, deploy and test their subgraphs independently while exposing a single AppSync endpoint. The AppSync service handles creating the combined schema as part of a merge operation. At runtime, the execution model is essentially the same as an individual AppSync API, meaning there is no additional query planning or multi step execution.
Merged APIs is a great solution for composing AppSync API subgraphs. If, however, your requirement is to compose AppSync APIs and non-AppSync APIs, you will need to implement a GraphQL gateway. A GraphQL gateway is responsible for combining multiple GraphQL subgraph endpoints into a single unified schema. At runtime, the gateway receives each GraphQL request and intelligently routes them across the different subgraphs. A single request to the gateway may result in multiple internal requests to the backend subgraphs to successfully retrieve the data.
While there are many existing approaches to composing GraphQL schema within a gateway, the GraphQL community has lacked an open specification for a GraphQL gateway that is designed from the ground up for extensibility and integration with diverse sets of tools. Recently at the GraphQLConf, AWS AppSync was proud to support the announcement of GraphQL-Fusion, a new open specification for a distributed GraphQL gateway under the MIT license. Started by Chillicream and The Guild, GraphQL-Fusion now has the support of a number of GraphQL related vendors and projects.
A gateway which conforms to the GraphQL Fusion spec will include logic for query planning and joining data, and be able to distribute requests to any GraphQL server or endpoint, including any serverless GraphQL API built on AWS AppSync, even a Merged API. The GraphQL-Fusion spec expands upon traditional federation approaches by providing the ability for the gateway to integrate APIs using GraphQL, REST, or gRPC under a single GraphQL schema.
In this blog post, we will discuss how the GraphQL-Fusion spec simplifies the composition of multiple schemas and walk through an example to compose subgraphs in a runtime approach. In this demo, we combine multiple AWS AppSync subgraphs and a subgraph built using GraphQL Yoga, an open source GraphQL server.
Below is the diagram of the demo architecture:
In this demo, we will build an example book reviews application, similar to Goodreads. The application will require three different subgraphs:
- Books Subgraph → This subgraph is responsible for storing the data related to each book in the sites catalog. The subgraph provides the ability to query the book metadata and add, update, and delete books from the catalog. The books subgraph is created using AWS AppSync and Amazon DynamoDB for storing the data.
- Authors Subgraph → This subgraph is responsible for storing the data related to authors for the books in the site catalog. It provides the ability to query information related to an author including name and email as well as mutations to add, update, and delete authors in the catalog. This subgraph is also created using AWS AppSync and Amazon DynamoDB.
- Reviews Subgraph → This subgraph is responsible for storing the data related to reviews for the books in the sites catalog. Users of the site can add reviews for a book including comments and ratings. The reviews subgraph provides the ability to query reviews by book or by author as well as add, update and delete reviews. For this subgraph, we have chosen to create a non-AppSync endpoint. Instead, we are adding an Amazon API Gateway endpoint which proxies each Graphql request to an AWS Lambdafunction written in Typescript. In this Lambda function, we use GraphQL Yoga, an open source GraphQL server and Amazon DynamoDB for storing the reviews data.
The GraphQL gateway will handle routing GraphQL requests to one or more of the subgraphs. Our GraphQL gateway is written using Hot Chocolate Fusion, an open source implementation of the GraphQL Fusion spec for the .Net platform. We will deploy the Gateway as a containerized application hosted in AWS Fargate.
Prerequisites
In order to deploy the demo, you will need the following:
- An active AWS account
- AWS CDK
- .NET 8.0 version 8.0.100-preview.7
- Docker
- NPM
- Yarn
- A git client.
Deploying the Subgraphs
First, let’s clone the demo repository and deploy the subgraphs.
$ git clone https://github.com/aws-samples/appsync-graphql-fusion-demo.git $ cd appsync-graphql-fusion-demo $ ./deploy-subgraphs
Now, that we have deployed the subgraphs let’s take a look at each GraphQL schema exposed by our subgraphs:
Subgraph 1: Authors Subgraph
schema { query: Query, mutation: Mutation } type Author { id: ID! name: String! bio: String contactEmail: String nationality: String } type AuthorConnection { items: [Author] nextToken: String } type Book { authorId: ID! author: Author } input CreateAuthorInput { name: String! bio: String contactEmail: String nationality: String } input DeleteAuthorInput { id: ID! } type Mutation { createAuthor(input: CreateAuthorInput!): Author deleteAuthor(input: DeleteAuthorInput!): Author } type Query { authorById(id: ID!): Author authors(limit: Int): AuthorConnection bookByAuthorId(authorId: ID!): Book }
Subgraph 2: Books Subgraph
schema { query: Query, mutation: Mutation } type Author { id: ID! books: BookConnection } type Book { id: ID! title: String! authorId: ID! genre: String publicationYear: Int } type BookConnection { items: [Book] nextToken: String } input CreateBookInput { title: String! authorId: ID! genre: String publicationYear: Int } input DeleteBookInput { id: ID! } type Mutation { createBook(input: CreateBookInput!): Book deleteBook(input: DeleteBookInput!): Book } type Query { bookById(id: ID!): Book books(limit: Int): BookConnection authorById(id: ID!): Author }
Subgraph 3: Reviews Subgraph
schema { query: Query mutation: Mutation } type Author { id: ID! reviews: ReviewConnection } type Book { id: ID! reviews: ReviewConnection } type Review { id: ID! authorId: String! bookId: String! comment: String! rating: Int! } type ReviewConnection { items: [Review] nextToken: String } input CreateReviewInput { bookId: ID! reviewerId: ID! authorId: ID! comment: String! rating: Int! } input DeleteReviewInput { id: ID! } type Query { reviewById(id: ID!): Review reviews(limit: Int): ReviewConnection authorById(id: ID!): Author bookById(id: ID!): Book } type Mutation { createReview(input: CreateReviewInput!): Review deleteReview(input: DeleteReviewInput!): Review }
The first thing to notice when taking a look at our subgraphs schemas is that we did not need to add any special directives or annotations to make them compatible with our GraphQL Gateway which is one major benefit of the GraphQL Fusion spec. One goal for the GraphQL-Fusion spec is to simplify how the gateway is configured and not require the subgraphs to implement any additional protocol.
Schema composition is a major component of the GraphQL Fusion spec. When a Fusion schema is “composed” from multiple subgraphs, it is able to infer the semantic meaning of a GraphQL schema, which means there is little to no need for additional annotations. Schema composition can understand naming patterns and GraphQL best practices such as the Relay pattern. The schema composition logic is executed at build time and produces a single document which provides all the information necessary for the gateway to perform query planning at runtime in order to join data across the subgraphs.
Consider the following snippet from the Authors subgraph schema:
type Book { authorId: ID! author: Author } type Query { authorById(id: ID!): Author authors(limit: Int, nextToken: String): AuthorConnection bookByAuthorId(authorId: ID!): Book }
In this example, we use the “{type}By{key}
” naming convention to define what query operations are available. For example, in order to retrieve a Book type from this subgraph you must provide an authorId
input which acts as a key for retrieving the data. The Book type contains an author field which is used to join the data of a Book with its corresponding Author. In other approaches, we might need to annotate the schema with directives in order to define this relationship and how to retrieve the Book.author
field using this subgraph. With GraphQL Fusion, this is automatically handled by the schema composition routine so there are no changes necessary to integrate the subgraph with the gateway.
Below is a condensed snippet of the entire composed schema document types in this example. Note that for the Book type the schema composition routine includes the field author. This field has the @source directive that indicates it is defined in the Authors subgraph. The Book type also includes an @resolver directive which indicates that fields which have the Authors subgraph as their source can be retrieved using the bookByAuthorId query passing the $Book_authorId variable :
schema @fusion(version: 1) @httpClient(subgraph: "Books", baseAddress: "<server endpoint url>") @httpClient(subgraph: "Authors", baseAddress: "<server endpoint url>") @httpClient(subgraph: "Reviews", baseAddress: "<server endpoint url>") query: Query } type Book @variable(subgraph: "Books", name: "Book_id", select: "id") @variable(subgraph: "Reviews", name: "Book_id", select: "id") @variable(subgraph: "Books", name: "Book_authorId", select: "authorId") @variable(subgraph: "Authors", name: "Book_authorId", select: "authorId") @resolver(subgraph: "Books", select: "{ bookById(id: $Book_id) }", arguments: [ { name: "Book_id", type: "ID!" } ]) @resolver(subgraph: "Authors", select: "{ bookByAuthorId(authorId: $Book_authorId) }", arguments: [ { name: "Book_authorId", type: "ID!" } ]) @resolver(subgraph: "Reviews", select: "{ bookById(id: $Book_id) }", arguments: [ { name: "Book_id", type: "ID!" } ]) { author: Author @source(subgraph: "Authors") authorId: String! @source(subgraph: "Books") @source(subgraph: "Authors") id: ID! @source(subgraph: "Books") @source(subgraph: "Reviews") reviews: ReviewConnection! @source(subgraph: "Reviews") title: String @source(subgraph: "Books") } type Query { authorById(id: ID!): Author @variable(subgraph: "Books", name: "id", argument: "id") @resolver(subgraph: "Books", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) @variable(subgraph: "Authors", name: "id", argument: "id") @resolver(subgraph: "Authors", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) @variable(subgraph: "Reviews", name: "id", argument: "id") @resolver(subgraph: "Reviews", select: "{ authorById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) bookByAuthorId(authorId: ID!): Book @variable(subgraph: "Authors", name: "authorId", argument: "authorId") @resolver(subgraph: "Authors", select: "{ bookByAuthorId(authorId: $authorId) }", arguments: [ { name: "authorId", type: "ID!" } ]) bookById(id: ID!): Book @variable(subgraph: "Books", name: "id", argument: "id") @resolver(subgraph: "Books", select: "{ bookById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) @variable(subgraph: "Reviews", name: "id", argument: "id") @resolver(subgraph: "Reviews", select: "{ bookById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) }
Composing the Subgraphs
In our GraphQL-Fusion implementation, the compose document metadata is packaged within a .fgp
file using the Open Package Convention. In order to integrate the Gateway with our subgraphs, we package the subgraph metadata using the following script:
./compose-gateway-schema.sh
Deploying the GraphQL Gateway
Now that the gateway schema is composed, we can deploy the gateway endpoint with the following script:
./deploy-fusion-gateway.sh
Take note of the GraphQLGateway.GraphQLGatewayEndpoint which is outputted by the above script as we will need it in the next section:
Outputs: GraphQLGateway.GraphQLGatewayEndpoint = _http://GraphQ-Graph-<AAA>.<region>.elb.amazonaws.com/graphql _GraphQLGateway.GraphQLGatewayServiceLoadBalancerDNS691F4497 = _[GraphQ-Graph-<AAA>.us-west-2.elb.amazonaws.com](http://graphq-graph-ugfqnjtfi6lc-301122370.us-west-2.elb.amazonaws.com/)_GraphQLGateway.GraphQLGatewayServiceServiceURL267E9CE0 = _[http://GraphQ-Graph-UgfqNJTFI6Lc-301122370.us-west-2.elb.amazonaws.com](http://graphq-graph-ugfqnjtfi6lc-301122370.us-west-2.elb.amazonaws.com/)_
Testing the Sample
Once the gateway has been deployed, you can access it at the GraphQLGateway.GraphQLGatewayEndpoint
provided as output from the previous step. Navigating to the endpoint in a browser will bring up the GraphQL explorer which the Gateway implementation provides. Select “Create Document” to being inserting sample data.
1. Add a sample author
mutation createAuthor { createAuthor(input: { name: "Mark Twain", bio: "Mark Twain was an American humorist, journalist, lecturer, and novelist", contactEmail: "[email protected]", nationality: "USA" }) { id, bio, contactEmail, nationality } }
Sample Response:
{ "data": { "createAuthor": { "id": "bab29018-c276-4636-840e-099e227e634f", "bio": "Mark Twain was an American humorist, journalist, lecturer, and novelist", "contactEmail": "[email protected]", "nationality": "USA" } } }
2. Add a sample book from this author using the id for the author from above step.
mutation createBook { createBook(input: { title: "The Adventures of Tom Sawyer", authorId: "bab29018-c276-4636-840e-099e227e634f", genre: "Adventure Fiction", publicationYear: 1876, }) { id, title, authorId, genre, publicationYear } }
Sample Response:
{ "data": { "createBook": { "id": "6490e420-a375-49a4-bb5b-1c9540e70add", "title": "The Adventures of Tom Sawyer", "authorId": "bab29018-c276-4636-840e-099e227e634f", "genre": "Adventure Fiction", "publicationYear": 1876 } } }
3. Add a sample review for this book using the generated book id and author id.
mutation createReview { createReview(input: { authorId: "bab29018-c276-4636-840e-099e227e634f", bookId: "6490e420-a375-49a4-bb5b-1c9540e70add", comment: "This is a great American novel about the mischievous adventures of a boy named Tom Sawyer", rating: 8 }) { id, authorId, bookId, comment, rating } }
Sample Response:
{ "data": { "createReview": { "id": "2d07d856-522f-4259-9848-0a67a14929fd", "authorId": "bab29018-c276-4636-840e-099e227e634f", "bookId": "6490e420-a375-49a4-bb5b-1c9540e70add", "comment": "This is a great American novel about the mischievous adventures of a boy named Tom Sawyer", "rating": 8 } }
4. Run a Test Query
query GetBookData { bookById(id: "6490e420-a375-49a4-bb5b-1c9540e70add") { id, title, publicationYear, genre, author { id, name, bio, nationality }, reviews { items { id, rating, comment } } } }
Sample response:
{ "data": { "bookById": { "id": "6490e420-a375-49a4-bb5b-1c9540e70add", "title": "The Adventures of Tom Sawyer", "publicationYear": 1876, "genre": "Adventure Fiction", "author": { "id": "bab29018-c276-4636-840e-099e227e634f", "name": "Mark Twain", "bio": "Mark Twain was an American humorist, journalist, lecturer, and novelist", "nationality": "USA" }, "reviews": { "items": [ { "id": "2d07d856-522f-4259-9848-0a67a14929fd", "rating": 8, "comment": "This is a great American novel about the mischievous adventures of a boy named Tom Sawyer." } ] } } } }
Inspecting the Query Plan
The Gateway IDE allows you to inspect the query plan of the request to identify the sub-queries which are executed against the backend subgraphs by enabling the fusion query plan.
The query plan for this request indicates that the Gateway executed 3 requests to 3 different backend subgraphs. First, in parallel, the Gateway retrieves data about a book from the Reviews subgraph and the Books subgraph using the input id. Then, once the book data is resolved including the author id for the book, the Gateway sends a subsequent query to the Authors subgraph to retrieve the author data for the author that has the corresponding author id returned in the book data. You can view the requests and metrics associated with each request in Amazon Cloudwatch for both the AppSync subgraphs and the GraphQL Yoga subgraph running on AWS Lambda.
Cleanup
The sample provides cleanup scripts for cleaning up all resources:
./cleanup-infrastructure.sh
Going Further
The development of the GraphQL-Fusion spec is ongoing as the community works together to refine the draft. For more information on the GraphQL-Fusion implementation, visit the launch blog.
About the author
| Nicholas is a Senior Software Engineer who has been working on AWS AppSync for the past 3 years. He spends his work days focused on improving GraphQL query execution performance and weekends roaming around San Francisco with his dog Pippa. |