GraphQL

Resolvers in Depth

18 min Lesson 6 of 35

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:
  1. Create a resolver that fetches a blog post and logs each of the four arguments (parent, args, context, info)
  2. Create a nested resolver for Post.comments that uses the parent post ID
  3. Implement a computed field Post.isPublished that checks if publishedAt is in the past
  4. Create an async resolver that fetches data from a database and handles errors properly