GraphQL
Resolvers in Depth
Understanding Resolvers
Resolvers are the heart of every GraphQL server. They define how to fetch the data for each field in your schema. Understanding resolvers deeply is crucial for building efficient and maintainable GraphQL APIs.
Resolver Function Signature
Every resolver function receives four arguments:
const resolvers = {
Query: {
user: (parent, args, context, info) => {
// parent: The result from the parent resolver
// args: Arguments passed to the field
// context: Shared data across all resolvers
// info: Information about the query execution
return getUserById(args.id);
}
}
};
The Four Arguments:
- parent: The return value of the parent resolver (or root value)
- args: An object containing the arguments passed to the field
- context: A shared object available to all resolvers (authentication, database connections, etc.)
- info: Contains information about the operation (field name, path, schema details)
Resolver Chains
Resolvers are executed in a chain, where the result of a parent resolver is passed to its child resolvers:
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return { id: args.id, name: 'John Doe', email: 'john@example.com' };
}
},
User: {
// Parent contains the user object from the Query.user resolver
posts: (parent, args, context, info) => {
return getPostsByUserId(parent.id);
},
followers: (parent, args, context, info) => {
return getFollowersByUserId(parent.id);
}
}
};
The
parent argument allows you to access data from the parent resolver, enabling nested data fetching without redundant queries.
Default Resolvers
GraphQL provides default resolvers for fields that match property names on the parent object:
const resolvers = {
Query: {
user: () => ({ id: 1, name: 'Alice', email: 'alice@example.com' })
},
User: {
// No need to define resolvers for id, name, email
// GraphQL automatically resolves them from the parent object
// Only define custom resolvers for computed fields
fullProfile: (parent) => {
return `${parent.name} (${parent.email})`;
}
}
};
If a field exists on the parent object and you don't define a resolver, GraphQL will automatically use the property value. This is called the default resolver.
Async Resolvers
Most resolvers need to fetch data asynchronously. GraphQL fully supports async/await:
const resolvers = {
Query: {
users: async (parent, args, context, info) => {
const users = await context.db.user.findMany();
return users;
},
user: async (parent, { id }, context) => {
const user = await context.db.user.findUnique({ where: { id } });
if (!user) throw new Error('User not found');
return user;
}
},
User: {
posts: async (parent, args, context) => {
return await context.db.post.findMany({
where: { authorId: parent.id }
});
}
}
};
GraphQL automatically waits for all async resolvers to complete before returning the response. You can safely use async/await in any resolver.
Resolver Patterns
Here are common patterns for organizing resolvers:
1. Service Layer Pattern
// services/userService.js
class UserService {
constructor(db) {
this.db = db;
}
async getUsers() {
return await this.db.user.findMany();
}
async getUserById(id) {
return await this.db.user.findUnique({ where: { id } });
}
}
// resolvers/userResolvers.js
const resolvers = {
Query: {
users: (parent, args, { services }) => {
return services.user.getUsers();
},
user: (parent, { id }, { services }) => {
return services.user.getUserById(id);
}
}
};
2. DataLoader Pattern (for N+1 problem)
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (ids) => {
const users = await db.user.findMany({
where: { id: { in: ids } }
});
return ids.map(id => users.find(user => user.id === id));
});
const resolvers = {
Post: {
author: (parent, args, { loaders }) => {
return loaders.user.load(parent.authorId);
}
}
};
3. Field Resolver Pattern
const resolvers = {
User: {
// Computed field
fullName: (parent) => {
return `${parent.firstName} ${parent.lastName}`;
},
// Virtual field requiring database query
postCount: async (parent, args, { db }) => {
return await db.post.count({
where: { authorId: parent.id }
});
},
// Conditional field
email: (parent, args, { currentUser }) => {
if (currentUser && currentUser.id === parent.id) {
return parent.email;
}
return null; // Hide email from other users
}
}
};
Performance Consideration: Be careful with field resolvers that make database queries. They execute for every instance of that type. Use DataLoader or batch queries to avoid N+1 problems.
Error Handling in Resolvers
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
try {
const user = await context.db.user.findUnique({ where: { id } });
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
// Custom error handling
throw new Error(`Failed to fetch user: ${error.message}`);
}
}
}
};
Practice Exercise:
- Create a resolver that fetches a blog post and logs each of the four arguments (parent, args, context, info)
- Create a nested resolver for
Post.commentsthat uses the parent post ID - Implement a computed field
Post.isPublishedthat checks ifpublishedAtis in the past - Create an async resolver that fetches data from a database and handles errors properly