GraphQL Schema Stiching: Modularizing GraphQL Schemas
Many introductory GraphQL tutorials put all the graphql
type definitions in a single GraphQL file. This works fine for small applications but quickly imposes maintenance costs as the size of that file balloons to hundreds or thousands of lines. Most projects will want to split their GraphQL type definitions across multiple files. This process is the topic of this blog post. The complete code can be found on GitHub.
Single File Strategy
First, let's review how to define your GraphQL type definitions in a single file. You can find this code in the monolithic folder on GitHub.
Suppose we want to model a GraphQL schema that describes students and the classes they're taking. We might have three files.
typeDefinitions.graphql
type Student {
id: ID!
name: String
classes: [Class]
}
type Class {
id: ID!
name: String
description: String
}
type Query {
student(id: ID!): Student
class(id: ID!): Class
}
resolvers.js
const { getStudentById, getClassById, getStudentClasses } = require("./data");
// The implementations of these resolver functions are linked above
module.exports = {
Query: {
student: (_, { id }) => getStudentById(id),
class: (_, { id }) => getClassById(id)
},
Student: {
classes: student => getStudentClasses(student)
}
};
server.js
const { GraphQLServer } = require("graphql-yoga");
const resolvers = require("./resolvers");
// Notice that we pass a single type definitions file and
// a single resolvers object straight into our server.
const server = new GraphQLServer({
typeDefs: "./typeDefinitions.graphql",
resolvers
});
server.start(4000, () =>
console.log(`Server is running on http://localhost:${4000}`)
);
This approach initially works just fine, but what happens when you want to incorporate a department
type and a tuition
type and a type to track extracurricular
activities. The easy answer is to keep using the same two typeDefinitions.graphql
and resolvers.js
files, but as these file grow, this strategy becomes unmanageable. Let's look at an alternative approach.
But we need to explore some tooling to help us along the way. We will make use of two libraries: graphql-import
and graphql-tools
GraphQL Schema Stiching: Modularizing GraphQL Schemas
I used graphql-import's importSchema
to read in the GraphQL schemas from disk.
You might wonder why I can't use fs.readFile
or something similar to read in my schemas as strings. The fact of the matter is that importSchema
resolves a very important problem: it parses my GraphQL type definitions to ensure that they are valid. After modularizing my type definitions, I will have two files:
student.graphql
type Student {
id: ID!
name: String
classes: [Class]
}
type Query {
student(id: ID!): Student
}
class.graphql
type Class {
id: ID!
name: String
description: String
}
type Query {
class(id: ID!): Class // Note that Class is not defined in this file
}
Unfortunately, student.graphql
alone is not a valid .graphql
file. Why? Because it references Class
, which is not defined anywhere in the student.graphql
file. graphql-import
gives me a special syntax to inform the parser of this dependency.
// Notice how I reference the other type definition file
# import Class from "../class/class.graphql"
type Student {
id: ID!
name: String
classes: [Class]
}
type Query {
student(id: ID!): Student
}
Once we have imported our type definitions, we can begin to manipulate them with graphql-tools.
graphql-tools
graphql-tools
is a set of utility functions released by the Apollo team that allow you to manage and manipulate your GraphQL schema. They provide two functions I found particularly useful: makeExecutableSchema and mergeSchemas.
makeExecutableSchema
accepts a type definition and its associated resolvers object, and it will output an object of type graphQLSchema
. You can pass this object into any spec-compliant GraphQL server.
mergeSchemas
accepts an array of graphQLSchema
objects and merge them into a single graphQLSchema
object. This process is known as schema stitching.
Stiching the Schemas
Here's how we can modularize our application. For each module,
- Create a
.graphql
file containing that module's definitions. We showed these above. - Create a second file specifying that module's resolvers.
- Create a third file that uses
makeExecutableSchema
to create agraphQLSchema
.
You can find the code demonstrating this process in the modular folder on GitHub
Your filetree should look like this:
+-- schema.js
+-- server.js
+-- student
| +-- index.js
| +-- resolver.js
| +-- student.graphql
+-- class
| +-- index.js
| +-- resolver.js
| +-- student.graphql
Below are the two files demonstrating step 3.
class/index.js
const path = require("path");
const { makeExecutableSchema } = require("graphql-tools");
const classResolvers = require("./resolvers");
const { importSchema } = require("graphql-import");
const typeDefs = importSchema(path.join(__dirname, "class.graphql"));
module.exports = makeExecutableSchema({
typeDefs,
resolvers: classResolvers
});
student/index.js
const path = require("path");
const { makeExecutableSchema } = require("graphql-tools");
const studentResolvers = require("./resolvers");
const { importSchema } = require("graphql-import");
const typeDefs = importSchema(path.join(__dirname, "student.graphql"));
module.exports = makeExecutableSchema({
typeDefs,
resolvers: studentResolvers
});
You can continue this process for every module you have. Once your have all of your graphQLSchema
objects (the result of makeExecutableSchema
), the process of merging them together is straight-forward:
schema.js
const { mergeSchemas } = require("graphql-tools");
const studentSchema = require("./student");
const classSchema = require("./class");
module.exports = mergeSchemas({ schemas: [studentSchema, classSchema] });
You can pass this resulting schema into the GraphQL server of your choice. I recommend GraphQL Yoga:
server.js
const { GraphQLServer } = require("graphql-yoga");
const schema = require("./schema");
const server = new GraphQLServer({
schema
});
server.start(4000, () =>
console.log(`Server is running on http://localhost:${4000}`)
);
Conlusion
Schema stiching allows us to take advantage of the amazing benefits of GraphQL without sacrificing modularity. At Spantree, we're frequently experimenting with innovative technologies and coding patterns to maximize our productivity and the quality of the software we provide to our clients. You can learn more about us on our site. We're always looking for new clients.
In my next blog post, we will talk about how to accomplish schema stiching with TypeScript. It's surprisingly quite different!