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,

  1. Create a .graphql file containing that module's definitions. We showed these above.
  2. Create a second file specifying that module's resolvers.
  3. Create a third file that uses makeExecutableSchema to create a graphQLSchema.

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!