graphql-tools lets you build GraphQL APIs by composing smaller, independent schema pieces.

Let’s see this in action. Imagine we’re building a simple API for a book catalog.

First, we define a type for a Book and a query to fetch books.

import { makeExecutableSchema } from 'graphql-tools';

const typeDefs = `
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

const resolvers = {
  Query: {
    books: () => [
      { title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams" },
      { title: "Pride and Prejudice", author: "Jane Austen" },
    ],
  },
};

const schema = makeExecutableSchema({ typeDefs, resolvers });

When a client queries query { books { title } }, graphql-tools takes the typeDefs and resolvers, combines them into a single executable schema, and then processes the query. It finds the books field in the Query type, executes the corresponding resolver function, and then maps the returned data to the requested title field.

This might seem straightforward, but the real power emerges when you realize you don’t have to write one giant typeDefs and resolvers object. You can break them down.

Consider adding a Book type that can be searched by author.

import { mergeSchemas } from 'graphql-tools';

const bookTypeDefs = `
  type Book {
    title: String
    author: String
  }

  type Query {
    booksByAuthor(author: String!): [Book]
  }
`;

const bookResolvers = {
  Query: {
    booksByAuthor: (parent, { author }) => {
      const allBooks = [
        { title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams" },
        { title: "Pride and Prejudice", author: "Jane Austen" },
        { title: "Sense and Sensibility", author: "Jane Austen" },
      ];
      return allBooks.filter(book => book.author === author);
    },
  },
};

const userTypeDefs = `
  type User {
    id: ID!
    name: String
  }

  type Query {
    users: [User]
  }
`;

const userResolvers = {
  Query: {
    users: () => [
      { id: '1', name: 'Alice' },
      { id: '2', name: 'Bob' },
    ],
  },
};

const schema1 = makeExecutableSchema({ typeDefs: bookTypeDefs, resolvers: bookResolvers });
const schema2 = makeExecutableSchema({ typeDefs: userTypeDefs, resolvers: userResolvers });

const mergedSchema = mergeSchemas({
  schemas: [schema1, schema2],
});

Here, mergeSchemas takes multiple executable schemas (each created with makeExecutableSchema) and combines them into a single, unified schema. The Query type from bookTypeDefs and userTypeDefs are merged. Now, a client can query query { booksByAuthor(author: "Jane Austen") { title } users { name } } and get results from both parts of the API. This modularity is key for larger applications, allowing different teams to work on separate schema modules without conflict.

The graphql-tools library handles schema stitching, which is the process of merging multiple GraphQL schemas into one. It does this by respecting the GraphQL specification’s rules for schema composition. When you merge schemas, graphql-tools checks for type name collisions and can automatically resolve conflicts if schemas define types with the same name but different fields. It also allows for "remote schemas," where parts of your API might be served by entirely different GraphQL servers.

When you define a resolver for a field that exists in multiple merged schemas, the resolver from the schema that was listed earlier in the schemas array passed to mergeSchemas will take precedence. This is a crucial detail for understanding how conflicts are resolved in practice.

This approach allows you to build complex GraphQL APIs by composing smaller, independently testable schema modules, each potentially managed by a different team or service.

The next step is to explore how to add mutations to your schema.

Want structured learning?

Take the full Graphql-tools course →