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.
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.
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.
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:
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.
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.
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 } ] } } }
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.
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. Start monitoring for free.Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Understanding schema stitching in GraphQL"
Nice one Sarah. This article reinforced my knowledge on GraphQL and thought me new stuff too. Thanks.