Front-End Web & Mobile

GraphQL Gateway Based Federation with AWS AppSync and GraphQL Fusion

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:
Fusion Gateway

In this demo, we will build an example book reviews application, similar to Goodreads. The application will require three different subgraphs:

  1. 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.
  2. 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.
  3. 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:

  1. An active AWS account
  2. AWS CDK
  3. .NET 8.0 version 8.0.100-preview.7
  4. Docker
  5. NPM
  6. Yarn
  7. A git client.

Deploying the Subgraphs

First, let’s clone the demo repository and deploy the subgraphs.

$ git clone https://github.com/aws-samples/aws-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.

Banana Cake Pop

1. Add a sample author


mutation createAuthor {
    createAuthor(input: {
        name: "Mark Twain",
        bio: "Mark Twain was an American humorist, journalist, lecturer, and novelist",
        contactEmail: "markTwain@example.com",
        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": "markTwain@example.com",
      "nationality": "USA"
    }
  }
}

create Author Mutation

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
    }
  }
}

create Book Mutation

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
    }
}

create Review Mutation
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."
			}
			]
		}
	}
  }
}

getBookDataQuery

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.

Query Plan

Query Plan Reponse
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.