You can merge multiple GraphQL schemas into one, and the trickiest part isn’t the merging itself, but understanding how GraphQL’s type system handles conflicts and how to resolve them gracefully.
Let’s see this in action. Imagine we have two simple GraphQL schemas:
Schema A (Users):
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
}
Schema B (Products):
type Query {
product(sku: String!): Product
}
type Product {
sku: String!
description: String!
price: Float!
}
We want to combine these into a single schema that exposes both user and product queries. Many GraphQL libraries offer tools for this. For example, in Apollo Server (Node.js), you’d typically use mergeSchemas from @graphql-tools/schema:
import { ApolloServer } from '@apollo/server';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { mergeSchemas } from '@graphql-tools/schema';
// Define resolvers for Schema A
const userResolvers = {
Query: {
user: (parent, { id }) => ({ id, name: `User ${id}` }),
},
};
const userSchema = `
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
}
`;
// Define resolvers for Schema B
const productResolvers = {
Query: {
product: (parent, { sku }) => ({ sku, description: `Product ${sku}`, price: 19.99 }),
},
};
const productSchema = `
type Query {
product(sku: String!): Product
}
type Product {
sku: String!
description: String!
price: Float!
}
`;
// Create executable schemas for each part
const executableUserSchema = makeExecutableSchema({
typeDefs: userSchema,
resolvers: userResolvers,
});
const executableProductSchema = makeExecutableSchema({
typeDefs: productSchema,
resolvers: productResolvers,
});
// Merge the schemas
const mergedSchema = mergeSchemas({
schemas: [
executableUserSchema,
executableProductSchema,
],
});
// Create Apollo Server with the merged schema
const server = new ApolloServer({ schema: mergedSchema });
// ...start server...
When you run this, a single GraphQL endpoint will serve queries like:
query {
user(id: "123") {
id
name
}
product(sku: "ABC-456") {
sku
description
price
}
}
The mental model here is that mergeSchemas acts like a sophisticated union operation for your GraphQL types and resolvers. It takes multiple GraphQL schema definitions (type definitions and their associated resolvers) and stitches them together into a single, unified schema. The Query, Mutation, and Subscription root types are merged by adding fields from each input schema. Other types are also merged, and importantly, if the same type is defined in multiple schemas, their fields are combined.
The real power comes when you have multiple teams, microservices, or even different parts of a monolith that each own their own GraphQL schema. Instead of a monolithic schema that everyone has to touch, you can have independently developed and deployed schemas that are then composed into a single API gateway schema. This promotes modularity and team autonomy.
The core problem this solves is schema sprawl and the difficulty of managing a single, massive GraphQL schema as an application grows. By breaking it down, you can isolate concerns, enable independent development, and reduce the cognitive load on developers. The mergeSchemas function (or similar tools in other frameworks) is the mechanism that bridges these separate schemas, presenting a unified API to clients.
When types have the same name but different fields, or when fields on the same type have different types, mergeSchemas will typically throw an error. You then need to use techniques like delegation or schema stitching with specific conflict resolution strategies. For instance, if two schemas define a User type, but one has an email field and the other has a phoneNumber field, mergeSchemas can be configured to combine these fields into a single User type in the merged schema. This often involves specifying how resolvers should be composed or delegated to the underlying services.
The most surprising thing is how seamlessly GraphQL handles type conflicts when fields are additive. If SchemaA defines type Foo { bar: String } and SchemaB defines type Foo { baz: Int }, the merged schema will have type Foo { bar: String, baz: Int } without any explicit configuration for Foo itself, as long as the Query types don’t have conflicting fields. The complexity arises only when there’s a direct clash on the same field name with incompatible types or when resolvers need to be manually orchestrated.
The next challenge you’ll encounter is handling shared types across multiple schemas and ensuring their consistency.