Sarah Chima Atuonwu I am a Fullstack software developer that is passionate about building products that make lives better. I also love sharing what I know with others in simple and easy-to-understand articles.

Understanding schema stitching in GraphQL

8 min read 2275

GraphQL Logo Over Colorful Yarn

One of the major benefits of GraphQL is that you can query for all the data you need through one schema. This may assume that most systems are one big monolithic service. However, as the schema grows, it may need to be split into smaller schemas to be maintainable. Also, with the popularity of the microservice architecture, many systems are made up of smaller services that are responsible for the data they provide.

In such cases, there is a need to provide a unified GraphQL schema that clients can query. Otherwise, we will go back to a disadvantage of the traditional API requests, where you need to query several APIs to get the data you need.

This is where schema stitching comes in. Schema stitching enables us to combine multiple graphs into a single graph that data can be fetched from.

In this article, we will use a practical example to discuss schema stitching, how it works under the hood, and how to merge types across several schemas.

What is schema stitching?

Schema stitching combines multiple subschemas and creates a combined proxy layer called gateway that a client can use to make requests. This means that you can combine smaller GraphQL schemas from different modules or even different remote services into one schema called the gateway.

When a request comes from a client to the gateway, the gateway delegates the request to the subschema(s) that is responsible for providing the fields requested.

Phone, Gateway, and Subschema Diagram

How does the gateway know which subschema to delegate a request to? When a request comes in to the gateway, the gateway looks up the stitched schema configuration to know which subschema(s) is responsible for resolving the requested fields and delegates the request to the subschema.

It then combines the various resolved fields gotten from the schemas and sends them to the client. The client is generally not aware of what goes on when a request is made.

Most of the benefits of schema stitching are on the backend. Large graphs can be split into smaller graphs, and each team is responsible for maintaining their schema and the fields they resolve without impacting the developers working on other graphs.



Now, let us use an example for how schema stitching works in action. To follow along, you would need a basic knowledge of GraphQL and a knowledge of JavaScript.

Project overview and setup

Since our focus is on schema stitching, we’ll be using a starter project. In this project, we have two subgraphs with their schemas and one folder for the gateway service. We’ll see how we can stitch a schema to the gateway that is in a different module. Keeping in mind that most times microservices usually have their subschemas running in different servers, we will also see how we can stitch a remote schema to the gateway. We’ll also look at how to merge types that have their partial definitions existing in different subschemas.

Let’s get started:

First, clone the project here.

This is the overall project structure and the important files we will be making use of in this article:

schema-stitching
  - start // where we will be making most changes
    - gateway
      - index.js // where the schema stitching takes places
    - book
      - book.graphql
      - resolvers.js
      - index.js
      - datasources
        - books.json // mocked data for books
        - BooksAPI.js
    - review
      - review.graphql
      - resolvers.js
      - datasources
        - reviews.json // mocked reviews
        - ReviewsAPI.js
  - final // Find the complete project here which you can make reference to

Next, let’s install the dependencies for gateway. In the start directory, run the following commands:

cd start/gateway
npm install

Let’s build our gateway server and see schema stitching in action. We’ll be doing this in the index.js file found in the gateway folder. This is how the bare-bones file looks now. It’s just a basic GraphQL node server without any schema:


More great articles from LogRocket:


const { createServer } = require( '@graphql-yoga/node');

async function main()  {
    const server = createServer({
    })

    await server.start()
}

main().catch(error => console.error(error))

Next, let’s add our reviews subschema to the gateway server. First, we import the methods we need to create the reviews subschema:

const { loadSchema } = require('@graphql-tools/load');
const { GraphQLFileLoader } =  require('@graphql-tools/graphql-file-loader');
const { addResolversToSchema } = require('@graphql-tools/schema');
const { stitchSchemas } = require('@graphql-tools/stitch');

Since the reviews schema is from a different source, we need these loadSchema and GraphQLFileLoader to load it. The stitchSchemas method is what we’ll use to stitch our schemas together. We also need to import our reviews resolver file and its data source file:

const ReviewsAPI = require("./../review/datasources/ReviewsAPI");
const reviewsResolvers = require('./../review/resolvers.js');

Next, we create the subschema inside the main function:

async function main()  {
    const reviewsSchema = await loadSchema('./../review/reviews.graphql',       {
        loaders: [new GraphQLFileLoader()]
      }
    )

    const reviewsSchemaWithResolvers = addResolversToSchema({
        schema: reviewsSchema,
        resolvers: reviewsResolvers
    })
    const reviewsSubschema = { schema: reviewsSchemaWithResolvers };

We’ll now create our gateway schema with the reviewsSubschema and stitch it to our main gateway schema:

    // build the combined schema
    const gatewaySchema = stitchSchemas({
      subschemas: [
        reviewsSubschema
      ]
    });

    const server = createServer({
        context: () => { 
            return {
                dataSources: {
                    reviewsAPI: new ReviewsAPI()
                }
            }
        },
        schema: gatewaySchema,
        port: 4003
    })

So our file should look like this:

const { stitchSchemas } = require('@graphql-tools/stitch');
const { loadSchema } = require('@graphql-tools/load');
const { GraphQLFileLoader } =  require('@graphql-tools/graphql-file-loader');
const { addResolversToSchema } = require('@graphql-tools/schema');
const { createServer } = require( '@graphql-yoga/node');

const ReviewsAPI = require("./../review/datasources/ReviewsAPI");
const reviewsResolvers = require('./../review/resolvers.js');

async function main()  {
    const reviewsSchema = await loadSchema('./../review/reviews.graphql',       {
        loaders: [new GraphQLFileLoader()]
      }
    )

    const reviewsSchemaWithResolvers = addResolversToSchema({
        schema: reviewsSchema,
        resolvers: reviewsResolvers
    })
    const reviewsSubschema = { schema: reviewsSchemaWithResolvers };

    // build the combined schema
    const gatewaySchema = stitchSchemas({
      subschemas: [
        reviewsSubschema
      ]
    });

    const server = createServer({
        context: () => { 
            return {
                dataSources: {
                    reviewsAPI: new ReviewsAPI()
                }
            }
        },
        schema: gatewaySchema,
        port: 4003
    })
    await server.start()
}

main().catch(error => console.error(error))

Let’s start this server with npm start and try to query some review fields. Click on the link for the server and run the following query:

query {
  reviews {
    bookIsbn
    comment
    rating
  }
}

You should see a list of reviews with the requested fields.

Now we have seen how we can stitch a schema in a different module. But as mentioned, this is not always the case. Most times, we have to stitch remote GraphQL schemas. So we will use the book subschema to show how we can stitch remote GraphQL schemas.

Stitching remote subschemas

Let’s start the GraphQL server for the book subschema. The index.js file already contains code to start the server:

cd ../book
npm install
npm start

The server should be running at http://0.0.0.0:4002/graphql.

To stitch remote schemas, we need a nonexecutable schema and an executor.

The schema can be gotten through introspection or gotten as a flat SDL string from the server or repo. In our case, we’ll use introspection. Introspection enables you to query a GraphQL server for information about its schema. So with introspection, you can find out about the types, fields, queries, and mutations. But note that not all schemas enable introspection. In that case, you can fall back to using the flat SDL.

The executor is a simple generic method that performs requests to the remote schema.

Let’s do this for the running book GraphQL server. We’ll go back to the index.js file in the gateway directory.

Let’s import the necessary dependencies:

const { introspectSchema } = require('@graphql-tools/wrap');
const { fetch } =  require('cross-undici-fetch')
const { print } = require('graphql')

Next, we’ll add the executor for the books schema:

async function remoteExecutor({ document, variables }) {
  const query = print(document)
  const fetchResult = await fetch('http://0.0.0.0:4002/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables })
  })
  return fetchResult.json()
}

The GraphQL endpoint is hardcoded here. But if you are to use this for multiple remote schemas, you’ll want to make the executor more generic.

Finally, let’s build our schema and add it to the gateway schema:

  const booksSubschema = {
    schema: await introspectSchema(remoteExecutor),
    executor: remoteExecutor,
  };
  const gatewaySchema = stitchSchemas({
    subschemas: [booksSubschema, reviewsSubschema],
  });

In a different terminal, start your gateway server if it is stopped and query for the following using the server link provided:

query{
  reviews {
    bookIsbn
    comment
    rating
  }

  books {
    isbn
    title
  }
}

You should get both reviews and books.

Great! We have seen how schemas can be stitched to a gateway. Now, let us consider how to merge types that are defined in multiple schemas.

Type merging for stitched schemas

In stitched schemas, it is common for a type to have fields that are provided by multiple services. In such cases, the gateway uses the stitched schema configuration to know which service resolves a particular field. We will use an example to explain how it works.

Let us extend the Review type to include information about the Book. So in the reviews.graphql file found in the reviews service, we add the book field to the review and define the Book type:

type Review {
    id: Int!
    bookIsbn: String!
    rating: Int!
    comment: String
    book: Book! 
}

type Book {
    isbn: String
}

This is a unidirectional type relationship, which means the reviews service does not provide any unique field for the Book type. So in this case, we only need to add a merge configuration to the Book subschema since it provides all the unique fields for the Book type. All we need to do is to ensure that the keyfield isbn, which is needed to resolve the Book type, is correctly provided. We will do that by adding a resolver that returns the isbn of the book.

In the resolvers.js file in the reviews service, we add the resolver for the book field:

const resolvers = {
  Query: {
    reviews(_, __, { dataSources }) {
      return dataSources.reviewsAPI.getAllReviews();
    },
    reviewsForBook(_, { isbn }, { dataSources }) {
      const reviews = dataSources.reviewsAPI.getReviewsForBook(isbn);
      return { isbn, reviews };
    },
  },
  Review: { 
    book: (review) => {
      return { isbn: review.bookIsbn };
    },
  },
};
module.exports = resolvers;

Next, we need to add a merge configuration for the book subschema in the gateway. This is necessary since we have the Book type defined in multiple schemas. Update the booksSubschema to the following:

  const booksSubschema = {
    schema: await introspectSchema(remoteExecutor),
    executor: remoteExecutor,
    merge: {
      Book: {
        selectionSet: "{ isbn }",
        fieldName: "book",
        args: ({ isbn }) => ({ isbn }),
      },
    },
  };

This is all we need to do for this unidirectional type merging; we don’t need to add the merge configuration to the Reviews subschema since it does not provide any unique field. If you are confused about the properties needed for the merge config, hold on — it will be explained shortly.

If you run the following query:

{
  reviews {
    id
    rating
    comment
    book {
      author
    }
  }
}

You should get:

{
  "data": {
    "reviews": [
      {
        "id": 1,
        "rating": 4,
        "comment": "A great introduction to Javascript",
        "book": {
          "author": "Marijn Haverbeke"
        }
      },

If you want to know more about how the gateway resolves these fields, you can refer to the merging flow documentation.

That was a simple example of type merging. However, in many cases, unique fields are provided by multiple services. For instance, we may want to be able to get all reviews for a particular book and the reviews service will be responsible for resolving that field. In such a case, we’ll need to add a merge configuration to the gateway service to enable it to resolve these fields. Let’s do that.

First, we add the reviews field to the Book type defined in reviews.graphql:

type Book {
  isbn: String
  reviews: [Review]
}

We’ll also add a query that accepts an ISBN and returns the Book type. We will use this in our merge configuration as the field name needed to resolve the added fields for the Book type:

type Query {
  reviews: [Review!]!
  reviewsForBook(isbn: String): Book
}

Next, let’s add its resolver:

const resolvers = {
  Query: {
    reviews(_, __, { dataSources }) {
      return dataSources.reviewsAPI.getAllReviews();
    },
    reviewsForBook(_, { isbn }, { dataSources }) {
      const reviews = dataSources.reviewsAPI.getReviewsForBook(isbn);
      return { isbn, reviews };
    },
  },

Next, we’ll add the merge configuration to the gateway service. So in the index.js file in the gateway directory, we add the merge configuration to the reviewsSubschema definition:

    const reviewsSubschema = { 
      schema: reviewsSchemaWithResolvers,        
      merge: {
        Book: {
          selectionSet: "{ isbn }",
          fieldName: "reviewsForBook",
          args: ({ isbn }) => ({ isbn }),
        }
      } 
    };

In the merge configuration, we specified that the fields of the Book type will be resolved by the reviewsForBook, which is the fieldName defined here. This is the top-level query to consult for resolving the Book fields defined in the Reviews subschema.

The selectionSet indicates the fields that need to be selected from the Book object to get the arguments needed for reviewsForBook.

args transforms the selectionSet into the arguments that is used by reviewsForBook. This is useful if we want to do any transformation to the selectionSet before it is passed to the fieldName.

Now, let’s get the reviews for a book with this query:

{
  book(isbn: "9781491943533") {
    isbn
    author
    reviews {
      comment
      rating
    }
  }
}

You should get this:

{
  "data": {
    "book": {
      "isbn": "9781491943533",
      "author": "Nicolás Bevacqua",
      "reviews": [
        {
          "comment": "Dive into ES6 and the Future of JavaScript",
          "rating": 5
        }
      ]
    }
  }
}

Summary

In this tutorial, we have discussed what schema stitching is, why schema stitching is necessary, and we used a practical example to see how we can stitch schemas from a different module and a remote schema running in a different server. We also saw how you can merge types defined in multiple subschemas. For a more detailed explanation of type merging, you can refer to the documentation for it.

We can see that schema stitching is very useful for large systems made up of smaller services. It also helps teams to maintain their schemas independent of other developers in other teams.

Another popular way of combining multiple subschemas is Apollo Federation. You can read this article to find out how it differs from schema stitching.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Sarah Chima Atuonwu I am a Fullstack software developer that is passionate about building products that make lives better. I also love sharing what I know with others in simple and easy-to-understand articles.

One Reply to “Understanding schema stitching in GraphQL”

  1. Nice one Sarah. This article reinforced my knowledge on GraphQL and thought me new stuff too. Thanks.

Leave a Reply