Front-End Web & Mobile

Introducing Merged APIs on AWS AppSync

AWS AppSync is a serverless GraphQL service that makes it easy to create, manage, monitor and secure your GraphQL APIs. Within an AppSync API, developers can access data across multiple different data sources including Amazon DynamoDB, AWS Lambda, and HTTP APIs. As the service continues to grow in adoption, our customers have faced challenges related to team collaboration across multiple teams and AWS accounts within an organization. Each AppSync API has a single GraphQL schema and configured data sources, resolvers, and functions.

When exposing multiple microservices through a single GraphQL API endpoint, multiple teams owning a portion of the API have to work simultaneously in the same API. Multiple developer teams working on the same API without proper isolation and guardrails can lead to accidental breaking changes, with no effective way for such teams to avoid stepping on each other’s toes when, for example, a developer in team A pushes an API change that could break functionality team B previously implemented and vice-versa. Another challenge is support and maintenance; when you have a single GraphQL API, it is difficult to troubleshoot whether there is an issue in one part of the Graph. For Enterprises who have multiple business domains in the same Graph, no single human can understand the entire Graph and maintenance becomes a nightmare.

Today, we are pleased to announce the general availability of Merged APIs in AWS AppSync. Merged APIs enable teams to merge resources, including types, data sources, functions, and resolvers, from multiple source AppSync APIs into a single, unified AppSync endpoint. At launch, you can merge up to 10 source AppSync APIs into a Merged API. In the frontend, clients only need to interact with a single endpoint to retrieve data across multiple source APIs. In the backend, development teams can create, update, test, and deploy their independent source APIs as part of a CI/CD pipeline. Once they have approved their changes, they can merge their changes to the Merged API endpoint in order to make them available to clients without blocking on other changes from other source APIs. Execution of queries, mutations, and subscriptions on a Merged API is handled by the AppSync service, providing the same monitoring and performance experience as a source AppSync API today.

The diagram below shows an example how a Merged API is configured with multiple source APIs across different teams:

Example of Merged API configured across multiple Source APIs

Figure 1: Example of Merged API configured across multiple Source APIs

Schema Directives

When there are conflicts across Source Schema definitions, new GraphQL directives can be used to provide the flexibility to resolve conflicts.

  • @canonical: if two or more source APIs have the same GraphQL type or field, one of the APIs can annotate their type or field as canonical, which takes precedence when merging the schemas. Conflicting types without this directive in other source APIs are ignored when merged.
  • @hidden: teams may want to remove or hide specific types or operations in the target API so only internal clients can access specific typed data. With this directive attached, types or fields are not merged to the Merged API target.
  • @renamed: There are use cases where different APIs have the same type or field name. However they all need to be available in the merged schema. A simple way to have these types in the Merged API target is by renaming one of them for handling any naming conflicts.

Auto Merge

AppSync provides multiple ways of merging the changes from the source API(s) to the Merged API. By default, these merge operations are manual. However, you can enable the auto merge mode which will submit a merge operation to the Merged API whenever a source API has an update. The way this works is that the Merged API subscribes for changes to the source API which has enabled auto-merge. When there is a change, the AppSync service assumes the configured merged API execution role on the Merged API and submits a StartSchemaMergeoperation on behalf of the Merged API owner to merge in the changes. Whenever you change the types, data sources, resolvers, or functions of a source API, auto merge will trigger an update to the corresponding Merged API.

Comparison to other approaches

There are many solutions and patterns in the GraphQL community for combining GraphQL schemas and enabling team collaboration through a shared graph. AppSync Merged APIs adopts a “build time” approach to schema composition, where source APIs are combined into a separate, Merged API. An alternative approach is to layer a “run time” router across multiple source APIs or sub-graphs. In this approach the router receives a request, references a combined schema that it maintains as metadata, constructs a request plan, and then distributes request elements across its underlying sub-graphs/servers.

AppSync Merged APIs can only be associated with AppSync source APIs. If you need support for schema composition across AppSync and non-AppSync sub-graphs you can connect one or more AppSync GraphQL API and/or Merged APIs into a router-based solution.

Example Scenario : Creating a Merged API for Book Reviews and Recommendations

In the following example, we will create a Merged API that will power the backend of a book review and recommendations website, similar to Goodreads. In this example, we will have multiple source APIs which are owned and developed by separate teams including:

  1. Books Source API → This team is responsible for storing the data related to each book in the sites catalog. The team manages a source AppSync API which provides the ability to query the book metadata and add, update, and delete books from the catalog.
  2. Authors Source API → This team is responsible for storing the data related to authors for the books in the site catalog. The Authors source AppSync API will provide 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.
  3. Reviews Source API → This team 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 source AppSync API will provide the ability to query reviews by book or by author as well as add, update and delete reviews.
  4. Users Source API → This team is responsible for handling the data related to users in the site. Users of the site can review books as well as create reading lists. The Users source AppSync API will provide the ability to query information related to a given user as well as the reading list of a given user on the site. User entries can be added, updated, and deleted within this source API.

Creating the Source AppSync APIs

In this section, we will create the source AppSync APIs that will make up our Merged API for the book review site. This example uses Amazon DynamoDB as the data source. However you can use other data sources as well supported by AWS AppSync based on your use case.

Creating the Books Source API

  • Navigate to the AppSync console and click on Create API . In Step 1 of the API creation wizard select GraphQL APIs option, Design from scratch and click Next.

Selection of API Type between GraphQL and MergedAPI in console

  • In step 2 of the wizard, we add the Books API metadata so that the Merged API owner can contact the books team when investigating any issues if necessary.

Note: You can also use private API features while creating the Source API if needed for your use case.

Specifying API metadata for Source API

  • In step 3 of the wizard, we generate a Book type backed by a DynamoDB table in order to store the Book metadata.
    • Note that in our model, we include an authorId field which is required for each book. This field is the key which will be used to retrieve metadata associated with an Author from the Authors source API. When building the source schema, it is recommended to identify your types by unique keys such as an ID field to make it easier to join data across different source APIs.
    • In the DynamoDB table, we add indexes which will allow us to query the books by a given author or a given publisher.

GraphQL Type Backed By DynamoDB

Source API Book Model

Source API Books Table

  • In Step 4 of the wizard, we review the API details and then click on Create API. It starts creation of the API and DynamoDB table.

Creating the Authors Source API

  • Again, navigate to the AppSync console and click on Create API . In Step 1 of the API creation wizard select GraphQL APIs option, Design from scratch and click Next.
  • In step 2 of the wizard, we add the Authors API metadata so that the Merged API owner can contact the authors team when investigating any issues if necessary.

Specifying Authors API metadata for Source API

  • In step 3 of the wizard, we generate an Author type backed by a DynamoDB table. The model for this table includes id, name, bio, contactEmail, and nationality of the author.

Source API Authors Model

Authors Table

  • In Step 4 of the wizard, we review the API details and then click on Create API. It starts creation of the API and DynamoDB table.

Creating the Reviews Source API

  • Again, navigate to the AppSync console and click on Create API . In Step 1 of the API creation wizard select GraphQL APIs option, Design from scratch and click Next.
  • In step 2 of the wizard, we add the Reviews API metadata so that the Merged API owner can contact the reviews team when investigating any issues if necessary.

Specifying API metadata for Reviews Source API

  • In step 3 of the wizard, we generate the Review type backed by a DynamoDB table. The Review type will include a reviewerId which references the user that wrote the review, authorId that references the author of the book being reviewed, and a bookId that references the book the review is about as well as an id, comment, rating, createdAt timestamp, and updatedAt timestamp. We will add indexes to the DynamoDB table to query reviews for a given book and reviews by a given reviewer.

Reviews Source API Model
Reviews Table

Creating the Users Source API

  • Again, navigate to the AppSync console and click on Create API . In Step 1 of the API creation wizard select GraphQL APIs option, Design from scratch and click Next.
  • In step 2 of the wizard, we add the Users API metadata so that the Merged API owner can contact the users team when investigating any issues if necessary.

Specifying Users Source API Metadata

  • In step 3 of the wizard, add a User type backed by a generated DynamoDB table with id, name, and email fields.

Users API Model

Users Table

  • In Step 4 of the wizard, we review the API details and then click on Create API. It starts creation of the API and DynamoDB table.

Creating the Merged AppSync API

Now that we have created all four source AppSync APIs, it is time to create the Merged API.

  • Navigate to the AppSync console and click on Create API . In Step 1 of the API creation wizard select Merged APIs option and click Next.

Selection of Merged API in the console

  • Similar to before we add the API name and contact info in step 2.
  • For this example, we will create a new service role for the Merged API.
    • The Merged API execution role is responsible for securely accessing the merged source API resources during queries and mutations using a new IAM action: appsync:SourceGraphQL.
    • AppSync requires this permission on the top level field ARNs within a request for each top level field that has a configured resolver merged from a source API. You can choose to allow or deny specific top-level fields similar to how the IAM authorization mode works in AppSync today for the appsync:GraphQL permission. For non-top level fields, AppSync requires permission on the source API ARN itself.
    • For example, here is the policy of the Merged API execution role.
{
            "Effect": "Allow",
            "Action": [
                "appsync:SourceGraphQL"
            ],
            "Resource": [
                "arn:aws:appsync:us-east-1:123456789012:apis/<Source API Id>",
                "arn:aws:appsync:us-east-1:123456789012:apis/<Source API Id/*>"
            ]
        }
    • If you want to restrict access to just certain GraphQL operations, you can do this for the root `Query`, `Mutation`, and `Subscription` fields.
  • {
                "Effect": "Allow",
                "Action": [
                    "appsync:SourceGraphQL"
                ],
                "Resource": [
                    "arn:aws:appsync:us-east-1:123456789012:apis/<Source API Id>/types/Query/fields/<Field-1>",
                    "arn:aws:appsync:us-east-1:123456789012:apis/<Source API Id/types/Mutation/fields/<Field-1>"
                    "arn:aws:appsync:us-east-1:123456789012:apis/<Source API Id/types/Subscription/fields/<Field-1>"
                ]
            }
    • For more on IAM authorization for Merged APIs, refer to the documentation.

Merged APIs Metadata

  • Next, we will add source API associations to the Merged API in step 3. AppSync supports adding source APIs from your account and source APIs from other accounts which have been shared via AWS Resource Access Manager (AWS RAM).

Note: You can also use private API features while creating the Merged API if needed for your use case.

  • For now, we will add the source APIs from our account that we have just created. In a follow up blog, we will go through how to share a source API from another account in AWS RAM.
  • Click the Add Source APIs button and check the UsersAPI, AuthorsAPI, ReviewsAPI, and BooksAPI sources. Finally, click Add Source APIs to confirm. Finally, click next to finish step 3.
  • Note : Associating a source API requires permissions on both resources in the association. The operation requires the appsync:AssociateSourceGraphqlApi action on the Merged API and the appsync:AssociateMergedGraphqlApi on the Source API. In contrast, disassociation operations only require permission on one of the resources in the association. For example, the DisassociateSourceGraphqlApi requires permission on the Merged API to remove a source API from it. DisassociateMergedGraphqlApi can be used to disassociate a source API from its Merged API and this action does authorization on the source API.

Add Source APIs from your account

Source APIs confirm

  • In step 4 of the wizard, we will select the authorization mode. In this example, we will use the default API key primary authentication mode to match the primary authentication mode of all source APIs.
    • In order for associations to be compatible, a Merged API must include the primary authentication mode of each source API either as a primary or additional authentication mode in its configuration.

API KEY Auth mode

  • In step 5 of the wizard, review the configuration and click Create API.
    • The creation of the Merged API will associate all the source APIs that were configured and merge the source resources to the unified endpoint. The flash bar at the top of the page is used to indicate whether any of these associations failed due to a conflict or not.
    • The status of each source API association is visible in the main page for your Merged API. Here you can find information regarding the current merge status, last successful merge, owner contact, and links to the source APIs that have been associated.

Status of Merged API Creation
Note: You can hover and click over the merge status column to get a detailed message related to the success or failure of each individual source API merge:

Status of Merged API Creation with Hover

  • Now that your Merged API is created, we can view the schema to see that all types from all source APIs are available in the Merged API endpoint.

Joining Data Across Source APIs

Now that we have successfully merged the schemas from four different Source APIs, we can access the data from each source API independently. However, the true power of the Merged API architecture is the ability to join data across different teams. In this example, we have many opportunities to join the data in order to provide a more useful experience for clients. For example, when querying the Merged API endpoint we may want the ability to get the basic information about an author while also returning a paginated list of books in the catalog that author wrote. Going even a step further, we might want to also retrieve the list of reviews for each book in the list.

In order to join data across different source APIs, we must ensure that each type we are joining has a well known primary key field or fields that can be used to reference this relation in another source API. It is common to use an ID field to identify each type as we have done in this example. For each Book type in the Books source API, we store an authorId field which can be used to join data about the Author from the Authors source API. The authorId acts as the “key” for retrieving data about an author. Next, we will walk through how we can use these “key” fields, to join data.

Joining Books Data with Authors Data Using a Field Resolver

  • The first step in joining the data between the Books and Authors types is to define a Book type within the Authors source API. This Book type will act as a sub-set of the Book definition and only include the fields which are relevant in the Authors source API.
  • Navigate to the Authors source API from the link in the Merged API table and add the following definition to the schema. Click Save Schema in the top right hand corner.
type Book {
    authorId: ID!
    author: Author
}
  • Next, we add a resolver on the Book.author field which will return the author of a given book. In the Book service, it stores the id of the author as the authorId field. Our resolver will access this authorId field from the context.source object as it will be retrieved by the parent resolver owned by the Book source API.
  • Create a function called GetAuthorByParentAuthorId in the Authors source API by navigating to functions and clicking Create function in the top right hand corner. This function will use the DynamoDB AuthorsTable as a data source.

Author By AuthorId function

  • The function will simply call a GetItemon the DynamoDB table using the authorId from the context.source object. We write the function code in JavaScript like this:
export function request(ctx) {
    return {
        operation: 'GetItem',
        key: {
            id: {
                'S': ctx.source.authorId
            }
        }
    }
}

export function response(ctx) {
    if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type, ctx.result)
    }
    
    return ctx.result
}
  • Click Create in the top right hand corner to create the function.
  • Now that the function is created, we will use it in a Pipeline resolver attached to the Book.author field in the Authors source API. Navigate to Book.author field in Resolvers section and click Attach.

Resolver Attach

  • We use the default request and response code for the Pipeline resolver and add the function we just created. Click Create in the top right hand corner to create the resolver.

Resolver code Authors

  • Now that we have created this resolver, we can properly link the data between these source APIs. In order to make this resolver available in the Merged API, we need to merge this update. We can do this by navigating to the settings page of the Authors Source API and clicking the Merge Now button.

Merge Now

  • Now that the resolver is successfully merged, verify that the Book type in the Merged API includes a field for author and that there is a pipeline resolver defined on that author field.
type Book {
    id: ID!
    title: String!
    authorId: ID!
    publisherId: ID
    author: Author
    genre: String
    publicationYear: Int
}

Joining Reviews Data with Authors Data Using a Field Resolver

  • Next, we will add a resolver to join the reviews data with the authors data.
  • Staying in the Authors source API, we will add another type to the Source API schema:
type Review {
   authorId: ID!
   author: Author
}
  • This type will be used to add the ability to retrieve the author for a given Review in a single query.
  • We now create a pipeline resolver on the Review.author field in the Authors source API. The pipeline resolver should use the default request and response resolver code. We will add the same function named GetAuthorByParentAuthorIdas we used above to this resolver as well because the Review type also references authors using an authorId field.
  • Once the pipeline resolver is added, we again merge the update to the Merged API via the Merge Now button in the settings page.

Joining Books Data with Reviews Data Using a Field Resolver

  • Next, we will add a resolver to join the reviews with the books from the sites catalog.
  • Navigate to the Reviews source API.
    • Since each Review type has a bookIdindicating which Book the review is about, we can add a reference Book type in the Reviews source API which adds a paginated list of Reviews for a given book like so:
type Book {
    id: ID!
    reviews: ReviewConnection
}
  • We now create a function with the ReviewsTable as a data source that will query the reviews table by book id in order to return a paginated list of reviews for each book. The book id is accessible via the id field of the Book type. We access this id field through the ctx.source object. We will use the following code to JavaScript code for the function and name it GetReviewsByBook.
export function request(ctx) {
    return {
        operation: 'Query',
        query: {
            expression: '#bookId = :bookId',
            expressionNames: {
                '#bookId': 'bookId'
            },
            expressionValues: {
                ':bookId': util.dynamodb.toDynamoDB(ctx.source.id)
            }
        },
        index: 'reviews-by-book-index',
        scanIndexForward: true,
        select: 'ALL_ATTRIBUTES'
    }
}

export function response(ctx) {
    if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type, ctx.result)
    }
    
    return ctx.result
}
  • After creating the new function in the Reviews source API, we repeat the same process as above, adding this function to a pipeline resolver on the Book.reviews field and merging this update to the Merged API.

Joining Authors Data with Reviews Data Using a Field Resolver

  • Staying in the Reviews source API, we also want to join the data between authors and reviews in order to be able to retrieve reviews for a given author. We add the following author type in the Reviews source API to enhance the type and provide the ability to join this data.
type Author {
    id: ID!
    reviews: ReviewConnection
}
  • Again, we create a function with the Reviews table as a data source. It will query the reviews table by author id in order to return a paginated list of authors for each book. The id for the author type of interest is found in the ctx.source object. We will use the following code to JavaScript code for the function and name it GetReviewsByAuthor.
export function request(ctx) {
    return {
        operation: 'Query',
        query: {
            expression: '#authorId = :authorId',
            expressionNames: {
                '#authorId': 'authorId'
            },
            expressionValues: {
                ':authorId': util.dynamodb.toDynamoDB(ctx.source.id)
            }
        },
        index: 'reviews-by-author-index',
        scanIndexForward: true,
        select: 'ALL_ATTRIBUTES'
    }
}

export function response(ctx) {
    if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type, ctx.result)
    }
    
    return ctx.result
}
  • Again, we add this function to a pipeline resolver with default request and response resolver code. This time we attach it to the Author.reviews field we added. We merge the update to the Merged API by clicking the Merge Now button in the settings page for the Reviews source API.

Joining Authors Data With Books Data Using a Field Resolver

  • Next, we will add the ability to get the books of a given author by joining the books data.
  • First, we navigate to the Books source API and create a new reference Author type. The type will include the primary key id field as well as the new field that returns a paginated list of books written by a given author.
type Author {
   id: ID!
   books: BookConnection
}
  • The id for the Author will be accessible in the ctx.source object for this resolver. We will use this id to lookup all entries for books with an authorId that matches.
  • Create a new function in the Books source API called GetBooksForAuthorwhich uses the Books table as a data source. Add the following function code in JavaScript:
export function request(ctx) {
    return {
        operation: 'Query',
        query: {
            expression: '#authorId = :authorId',
            expressionNames: {
                '#authorId': 'authorId'
            },
            expressionValues: {
                ':authorId': util.dynamodb.toDynamoDB(ctx.source.id)
            }
        },
        index: 'author-index',
        scanIndexForward: true,
        select: 'ALL_ATTRIBUTES'
    }
}

export function response(ctx) {
    if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type, ctx.result)
    }
    
    return ctx.result
}
  • Add the function to a default pipeline resolver on the Author.books field and save the pipeline resolver. Merge the update to the Merged API using the Merge Now button in the settings page.

Joining Reviews Data With Books Data Using a Field Resolver

  • Finally, we will add a resolver to get the book information for a given review to join the book data with the review data. Each Review type in the Reviews source API has a bookIdfield which can be used in performing this join.
  • Staying in the Books source API, create a new reference type to add a book field to the Review type like so:
type Review {
   bookId: ID!
   book: Book
}
  • Create a new function called GetBookForReviewthat will use the bookIdfound in the ctx.source object and call a GetItem on the Books table to retrieve the data related to this book. We will use the following code for this function:
export function request(ctx) {
    return {
       operation: 'GetItem',
       key: {
          id: {
               'S': ctx.source.bookId
           }
       }
    }
}

export function response(ctx) {
    if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type, ctx.result)
    }
    
    return ctx.result
}
  • Add the function to a default pipeline resolver on the Review.book field and create the pipeline resolver. Merge the update to the Merged API using the Merge Now button.

Challenge: Add your own resolver in the Users source API to retrieve the User data for the User that reviewed a given book by adding a user field to the Review type. You can use the reviewerId as the key for joining the User data. Merge the resolver to the Merged API to ensure that this data can be added.

Enabling Auto Merge

To Enable Auto Merge on a Source API, Navigate to Source API Settings, and click Edit . Then select Automatic merging and click Save.

Auto Merge

Testing the Merged API

First, lets add some test data to populate the data sources. Navigate to Merged API (Books Reviews Merged Api) and click on Query Editor. Once you run a mutation mentioned below, note down the Ids to be used in subsequent queries/mutations.

mutation CreateAuthor {
    createAuthor(input: {
        name: "Jane Austen",
        bio: "English novelist known for her witty and insightful works set in the English countryside during the Regency era.",
        nationality: "English",
        contactEmail: "janeausten@example.com"
    }) {
        id,
        name,
        bio,
        nationality,
        contactEmail
    }
}

mutation CreateAuthor2 {
   createAuthor(input: {
    name: "Ernest Hemingway",
    bio: "American writer known for his concise and minimalist writing style. His works often explore themes of war, masculinity, and existentialism.",
    nationality: "American",
    contactEmail: "hemingway@example.com"
  }) {
        id,
        name,
        bio,
        nationality,
        contactEmail
  }
}
  
mutation CreateAuthor3 {
    createAuthor(input: {
        name: "J.K. Rowling",
        bio: "British author best known for creating the immensely popular 'Harry Potter' series.",
        nationality: "British",
        contactEmail: "jkrowling@example.com"
    }) {
        id,
        name,
        bio,
        nationality,
        contactEmail
    }
}

mutation CreateAuthor4 {
    createAuthor(input: {
        name: "William Shakespeare",
        bio: "English playwright and poet.",
        nationality: "English",
        contactEmail: "shakespeare@example.com"
    }) {
        id,
        name,
        bio,
        nationality,
        contactEmail
    }
}

mutation CreateBookMutation {
  createBook(input: {
    authorId: "<author id of Jane austen from above>",
    publisherId: "2b345cde-6789-01fg-hijk-lmnopqrstuv",
    title: "Emma",
    publicationYear: 1815,
    genre: "Classic"}) {
    id
    author {
      bio
      contactEmail
      name
      nationality
    }
    genre
    publicationYear
    title
  }
}

mutation CreateBookMutation1 {
  createBook(input: {
    authorId: "<author id of J.K. Rowling from above>",
    publisherId: "2b345cde-1111-01fg-hijk-lmnopqrabcd",
    title: "Harry Potter and the Chamber of Secrets",
    publicationYear: 1998,
    genre: "Fantasy"}) {
    id
    author {
      bio
      contactEmail
      name
      nationality
    }
    genre
    publicationYear
    title
  }
}

mutation CreateBookMutation2 {
  createBook(input: {
    authorId: "<author id of Jane austen from above>",
    publisherId: "2b345cde-6789-01fg-hijk-lmnopqrstuv",
    title: "Sense and Sensibility",
    publicationYear: 1814,
    genre: "Fantasy"}) {
    id
    author {
      bio
      contactEmail
      name
      nationality
    }
    genre
    publicationYear
    title
  }
}

mutation CreateReview1 {
    createReview(input: {
        authorId: "<author id of JK Rowling>",
        bookId: "<book id of Harry Potter and the Chamber of Secrets>",
        comment: "This book was captivating!",
        createdAt: "2023-05-08T12:00:00Z",
        rating: 9,
        reviewerId: "c52309fa-4a5b-42fc-b942-94c77e96ac3d",
        updatedAt: "2023-05-08T12:00:00Z"
        }) {
        id,
        comment,
        rating
    }
}
  

mutation CreateReview2 {
    createReview(input: {
        authorId: "<author id of Jane Austen>",
        bookId: "<book id of Sense and Sensibility>",
        comment: "A thought-provoking read!",
        createdAt: "2023-05-08T12:00:00Z",
        rating: 4,
        reviewerId: "74afce67-6b41-4fe6-aab0-fde839356231",
        updatedAt: "2023-05-08T12:00:00Z"}) {
            id,
            comment,
            rating
    }
}

mutation CreateReview3 {
    createReview(input: {
        authorId: "<Author id of Jane Austen>",
        bookId: "<book id of Emma>",
        comment: "Couldn't put this book down!",
        createdAt: "2023-05-08T12:00:00Z",
        rating: 5,
        reviewerId: "71cd9ec4-3b56-407d-8144-095e0ba1a534",
        updatedAt: "2023-05-08T12:00:00Z"}) {
            id,
            comment,
            rating
    }
}

Now we can test the response for queries that span multiple source APIs.

Here is an example of Listing Books along with Author and Review Information :

query ListBooksAuthorsAndReviews {
  listBooks {
    items {
      id,
      title
      authorId
      genre
      publicationYear
      publisherId
      author {
        id,
        name,
        bio,
        nationality
      },
      reviews {
        items {
          id,
          createdAt,
          updatedAt,
          rating,
          comment
        }
      }
    },
  }
}

Response:

{
  "data": {
    "listBooks": {
      "items": [
        {
          "id": "85783c75-94c1-4eb4-bd73-aad50cd1481d",
          "title": "Emma",
          "authorId": "9af707d9-30ff-4841-84bf-7881cfce802c",
          "genre": "Classic",
          "publicationYear": 1815,
          "publisherId": "2b345cde-6789-01fg-hijk-lmnopqrstuv",
          "author": {
            "id": "9af707d9-30ff-4841-84bf-7881cfce802c",
            "name": "Jane Austen",
            "bio": "English novelist known for her witty and insightful works set in the English countryside during the Regency era.",
            "nationality": "English"
          },
          "reviews": {
            "items": [
              {
                "id": "3531083c-0535-4e19-9d14-71cba00fad76",
                "createdAt": "2023-05-08T12:00:00Z",
                "updatedAt": "2023-05-08T12:00:00Z",
                "rating": 5,
                "comment": "Couldn't put this book down!"
              }
            ]
          }
        },
        {
          "id": "b7d7ae3e-8d13-4fe4-8a87-970c79ebf884",
          "title": "Sense and Sensibility",
          "authorId": "9af707d9-30ff-4841-84bf-7881cfce802c",
          "genre": "Fantasy",
          "publicationYear": 1814,
          "publisherId": "2b345cde-6789-01fg-hijk-lmnopqrstuv",
          "author": {
            "id": "9af707d9-30ff-4841-84bf-7881cfce802c",
            "name": "Jane Austen",
            "bio": "English novelist known for her witty and insightful works set in the English countryside during the Regency era.",
            "nationality": "English"
          },
          "reviews": {
            "items": [
              {
                "id": "73535cca-b3ae-4193-ade7-ea5e77f13748",
                "createdAt": "2023-05-08T12:00:00Z",
                "updatedAt": "2023-05-08T12:00:00Z",
                "rating": 4,
                "comment": "A thought-provoking read!"
              }
            ]
          }
        },
        {
          "id": "3a43cc15-1510-4a24-b68b-317cf01a0040",
          "title": "Harry Potter and the Chamber of Secrets",
          "authorId": "2a1a80a7-773d-48c4-a685-a9619700febe",
          "genre": "Fantasy",
          "publicationYear": 1998,
          "publisherId": "2b345cde-1111-01fg-hijk-lmnopqrabcd",
          "author": {
            "id": "2a1a80a7-773d-48c4-a685-a9619700febe",
            "name": "J.K. Rowling",
            "bio": "British author best known for creating the immensely popular 'Harry Potter' series.",
            "nationality": "British"
          },
          "reviews": {
            "items": [
              {
                "id": "fd8071d4-7460-4b73-a28d-31b3a5675b58",
                "createdAt": "2023-05-08T12:00:00Z",
                "updatedAt": "2023-05-08T12:00:00Z",
                "rating": 9,
                "comment": "This book was captivating!"
              }
            ]
          }
        }
      ]
    }
  }
}

Other queries to try :

query ListAuthorsBooksAndReviews {
  listAuthors {
    items {
      id
      name
      bio
      nationality
      books {
        items {
            id
            title
        }
      }
      reviews {
        items {
          id
          createdAt
          updatedAt
          rating
        }
      }
    }
  }
}

query ListAuthorsBooksAndReviews {
  listReviews {
    items {
      id
      createdAt
      updatedAt
      rating
      comment
      book {
        id,
        title
      }
      author {
        id,
        name,
        bio,
        nationality
      }
    }
  }
}

Testing the Source APIs Independently

In the above step, we used id field keys to join the data across different source APIs. These keys form the “interface” of the source API, enabling us to test the Source APIs independently without needing to setup a Merged API itself. Suppose we are members of the Books source API development team. We have created two resolvers to link the Books data to other source APIs on the Author.books and Review.book fields.

  • In order to test the Author.books resolver in the Books source API, we can use the AppSync test endpoint to ensure that our JavaScript code for the GetBooksForAuthor function is generating the correct request and response function evaluations. You can learn more about testing AppSync resolvers here .
  • Testing in the console or via the EvaluateMappingTemplate or EvaluateCode operations does not actually call the data source that is configured for a given function or resolver.
  • In order to test the source API end to end, we can make use of the @hiddendirective and create mock resolvers to mock the author id key. In order to do this, we can add a field getAuthorto the Query type in the Books source API that is hidden. Save the schema after adding this field.
type Query {
   ... all existing fields
   getAuthor(id: ID!): Author @hidden 
}
  • Navigate to the data sources menu in the Books source API and click Create data source. Create a new None data source called MockAuthorDatasource which will be used for this mock resolver.
  • Create a new function which uses the None data source created called MockGetAuthor. We will use the following code for the function:
export function request(ctx) {
    return {
        payload: {
            id: ctx.args.id
        }
    };
}

export function response(ctx) {
    return ctx.result;
}
  • Add the MockGetAuthorfunction to a default pipeline resolver attached to Query.getAuthor. Save the pipeline resolver in order to allow the ability to mock the input to the Author.books resolver.
  • Now, we will test the source API resolver itself. First we add some mock data for books using mutations:
mutation CreateBookMutation {
  createBook(input: {
    authorId: "1873aeff-312c-4f39-87f3-6e7715d2a7c6",
    publisherId: "2b345cde-6789-01fg-hijk-lmnopqrstuv",
    title: "Harry Potter and the Chamber of Secrets",
    publicationYear: 1998,
    genre: "Fantasy"}) {
    id
    authorId
    genre
    publicationYear
    title
  }
}

mutation CreateBookMutation2 {
  createBook(input: {
    authorId: "1873aeff-312c-4f39-87f3-6e7715d2a7c6",
    publisherId: "2b345cde-6789-01fg-hijk-lmnopqrstuv",
    title: "Harry Potter and the Prisoner of Azkaban",
    publicationYear: 1999,
    genre: "Fantasy"}) {
    id
    authorId
    genre
    publicationYear
    title
  }
}



mutation CreateBookMutation3 {
  createBook(input: {
    authorId: "c954e68e-2651-4dd7-a63d-30d7fffa05d2",
    publisherId: "1a234bcd-5678-90ef-ghij-klmnopqrstuv",
    title: "Pride and Prejudice",
    publicationYear: 1813,
    genre: "Romance"}) {
    id
    authorId
    genre
    publicationYear
    title
  }
}
  • Next, we query the books of a given mock author id.
query testAuthorBooksResolver {
    getAuthor(id: "1873aeff-312c-4f39-87f3-6e7715d2a7c6") {
         id,
         books {
            items {
                id,
                title,
                genre,
                authorId,
                publisherId,
                publicationYear
            }
        }
    }
}

Response: 
{
  "data": {
    "getAuthor": {
      "id": "1873aeff-312c-4f39-87f3-6e7715d2a7c6",
      "books": {
        "items": [
          {
            "id": "948c9a0e-f646-46ea-87c6-e718de4dccbe",
            "title": "Harry Potter and the Prisoner of Azkaban",
            "genre": "Fantasy",
            "authorId": "1873aeff-312c-4f39-87f3-6e7715d2a7c6",
            "publisherId": "3c456def-7890-12gh-ijkl-mnopqrstuv",
            "publicationYear": 1999
          },
          {
            "id": "48ae5262-c94f-42da-a1af-17b9610c38da",
            "title": "Harry Potter and the Chamber of Secrets",
            "genre": "Fantasy",
            "authorId": "1873aeff-312c-4f39-87f3-6e7715d2a7c6",
            "publisherId": "2b345cde-6789-01fg-hijk-lmnopqrstuv",
            "publicationYear": 1998
          }
        ]
      }
    }
  }
}
  • Using the mocked None resolver, we have now successfully validated the source API resolver is working properly.

Cleanup

  1. Navigate to AppSync console, Select Books Review Merged API, click Delete , confirm the API Name and click Delete again.
  2. Repeat step 1 for all the Source APIs as well.
  3. Navigate to DynamoDB console, Select tables associated with Source APIs (BooksTable, AuthorsTable, ReviewsTableand UsersTable) , click Delete , select confirm and click Delete again.

Important things to know

  1. At Launch, A single Source API can be associated only with one Merged API.
  2. At Launch, A Merged API cannot be associated as a Source API to another Merged API.
  3. The number of source APIs per Merged API has a limit of 10 at launch.
  4. Merged API specific resolvers and functions are not supported at this time. To add custom logic specific to Merged APIs, you can add functions and resolvers to the Source APIs and merge the changes to your Merged APIs.

Get started today!

AWS AppSync Merged APIs are generally available in all AWS regions where AWS AppSync is available today. You can refer to AWS Regional Services List to find out the regions where AppSync is available. To learn more about Merged APIs, refer to the AppSync documentation or visit the AppSync product page for more general information on AWS AppSync. We can’t wait to see what you will build!

About the authors

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.
Venugopalan Vasudevan is a Senior Specialist Solutions Architect focusing on AWS Front-end Web & Mobile services. Venu helps customers build their front-end and mobile strategies on AWS, including maturing and enhancing their DevOps practices.