Resolver Chaining
Resolver chaining is when one resolver's output becomes the input for another resolver. This is how GraphQL resolves nested data automatically.
When you query nested fields, each level's resolver receives the parent level's result as its first argument.
How Chaining Works
Let's look at a concrete example. When you query a user's posts, GraphQL chains the resolvers together.
const resolvers = {
Query: {
// Step 1: Fetch the user
user: (parent, args, context) => {
console.log('Step 1: Fetching user');
return context.db.users.find(u => u.id === args.id);
},
},
User: {
// Step 2: Fetch posts for the user
posts: (parent, args, context) => {
console.log('Step 2: Fetching posts for user', parent.id);
return context.db.posts.filter(p => p.authorId === parent.id);
},
},
};
// Query
query {
user(id: "1") {
name
posts {
title
}
}
}
// Console output:
// Step 1: Fetching user
// Step 2: Fetching posts for user 1
The User.posts resolver receives the user object from Query.user as its parent argument.
Deep Chaining
Chaining can go multiple levels deep. Each resolver in the chain receives the result from the previous level.
const resolvers = {
Query: {
user: (parent, args, context) => context.db.users.find(u => u.id === args.id),
},
User: {
posts: (parent, args, context) => context.db.posts.filter(p => p.authorId === parent.id),
},
Post: {
comments: (parent, args, context) => context.db.comments.filter(c => c.postId === parent.id),
author: (parent, args, context) => context.db.users.find(u => u.id === parent.authorId),
},
Comment: {
author: (parent, args, context) => context.db.users.find(u => u.id === parent.authorId),
},
};
// Query
query {
user(id: "1") {
posts {
title
comments {
text
author {
name
}
}
}
}
}
// Execution chain:
// 1. Query.user → returns User object
// 2. User.posts → receives User, returns [Post]
// 3. Post.title → returns title string
// 4. Post.comments → receives Post, returns [Comment]
// 5. Comment.text → returns text string
// 6. Comment.author → receives Comment, returns User
// 7. User.name → returns name string
Each resolver in the chain only needs to know about its immediate parent and children.
The N+1 Problem
Resolver chaining can lead to the N+1 problem. If you query 10 users and each has posts, you might make 11 database queries (1 for users + 10 for posts).
// This could make N+1 queries:
query {
users { // 1 query to fetch all users
name
posts { // N queries to fetch posts for each user
title
}
}
}
The solution is DataLoaders, which batch and cache database queries. We'll cover that in a later section.
Resolver Chaining Best Practices
Keep resolvers focused: Each resolver should do one thing well.
Use context for shared resources: Don't create database connections in individual resolvers.
Handle null cases: Always check if the parent or database result exists.
User: {
posts: (parent, args, context) => {
// Handle case where parent might be null
if (!parent) return [];
return context.db.posts.filter(p => p.authorId === parent.id);
},
},