| name | graphql-resolvers |
| user-invocable | false |
| description | Use when implementing GraphQL resolvers with resolver functions, context management, DataLoader batching, error handling, authentication, and testing strategies. |
| allowed-tools | [] |
GraphQL Resolvers
Apply resolver implementation patterns to create efficient, maintainable
GraphQL servers. This skill covers resolver function signatures,
execution chains, context management, DataLoader patterns, async
handling, authentication, and testing strategies.
Resolver Function Signature
Every resolver function receives four arguments: parent, args, context,
and info. Understanding these arguments is fundamental to writing
effective resolvers.
type ResolverFn = (
parent: any,
args: any,
context: any,
info: GraphQLResolveInfo
) => any;
const resolvers = {
Query: {
user: async (parent, args, context, info) => {
const { id } = args;
const { dataSources, user } = context;
return dataSources.userAPI.getUserById(id);
},
posts: async (parent, args, context, info) => {
const { limit, offset } = args;
const fields = info.fieldNodes[0].selectionSet.selections
.map(s => s.name.value);
return context.dataSources.postAPI.getPosts({
limit,
offset,
fields
});
}
}
};
Field Resolvers
Field resolvers define how to resolve individual fields on a type. The
parent argument contains the resolved parent object.
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
}
},
User: {
fullName: (parent) => {
return `${parent.firstName} ${parent.lastName}`;
},
posts: async (parent, args, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthor(parent.id);
},
friends: async (parent, { limit }, { dataSources }) => {
return dataSources.userAPI.getFriends(parent.id, limit);
},
postCount: async (parent, _, { dataSources }) => {
return dataSources.postAPI.countByAuthor(parent.id);
}
},
Post: {
author: async (parent, _, { dataSources }) => {
return dataSources.userAPI.(parent.);
},
: (parent, _, { dataSources }) => {
dataSources..(parent.);
}
}
};
Context Object Patterns
The context object is shared across all resolvers in a single request.
Use it for authentication, data sources, and request-scoped data.
interface Context {
user: User | null;
dataSources: DataSources;
db: Database;
req: Request;
loaders: Loaders;
}
const createContext = async ({ req }): Promise<Context> => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
const dataSources = {
userAPI: new UserAPI(),
postAPI: new PostAPI(),
commentAPI: new CommentAPI()
};
const loaders = {
userLoader: new DataLoader(ids => batchGetUsers(ids)),
postLoader: new DataLoader( (ids))
};
{
user,
dataSources,
: database,
req,
loaders
};
};
resolvers = {
: {
: {
(!user) {
();
}
user;
},
: (_, { id }, { loaders }) => {
loaders..(id);
}
}
};
Resolver Chains and Execution
Resolvers execute in a chain where parent resolvers complete before
child resolvers begin. Understanding execution order is crucial for
optimization.
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
console.log('1. Fetching user');
return db.users.findById(id);
}
},
User: {
posts: async (parent, _, { db }) => {
console.log('2. Fetching posts for user', parent.id);
return db.posts.findByAuthor(parent.id);
},
profile: async (parent, _, { db }) => {
console.log('2. Fetching profile for user', parent.id);
return db.profiles.findByUserId(parent.id);
}
},
Post: {
comments: async (parent, _, { db }) => {
console.log('3. Fetching comments for post', parent.id);
return db.comments.findByPostId(parent.id);
}
}
};
DataLoader Pattern for Batching
DataLoader solves the N+1 problem by batching multiple individual
loads into a single batch request and caching results.
import DataLoader from 'dataloader';
const batchGetUsers = async (userIds: string[]) => {
console.log('Batch loading users:', userIds);
const users = await db.users.findByIds(userIds);
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) || null);
};
const userLoader = new DataLoader(batchGetUsers, {
cache: true,
maxBatchSize: 100,
batchScheduleFn: cb => (cb, )
});
resolvers = {
: {
: (parent, _, { loaders }) => {
loaders..(parent.);
}
},
: {
: (parent, _, { loaders }) => {
loaders..(parent.);
}
}
};
Advanced DataLoader Patterns
interface CompositeKey {
userId: string;
type: string;
}
const batchGetUserData = async (keys: CompositeKey[]) => {
const byType = keys.reduce((acc, key) => {
acc[key.type] = acc[key.type] || [];
acc[key.type].push(key.userId);
return acc;
}, {});
const results = await Promise.all(
Object.entries(byType).map(([type, userIds]) =>
fetchDataByType(type, userIds)
)
);
return keys.map(key =>
results.find(r => r.userId === key.userId && r.type === key.type)
);
};
const dataLoader = new (
batchGetUserData,
{
:
}
);
dataLoader.({ : , : }, userData);
dataLoader.({ : , : });
dataLoader.();
Async Error Handling
Proper error handling in resolvers ensures meaningful errors reach the
client while protecting sensitive information.
import { GraphQLError } from 'graphql';
import { ApolloServerErrorCode } from '@apollo/server/errors';
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
try {
const user = await dataSources.userAPI.getUserById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'USER_NOT_FOUND',
http: { status: 404 }
}
});
}
return user;
} catch (error) {
console.error('Error fetching user:', error);
if (error instanceof GraphQLError) {
throw error;
}
throw new GraphQLError('Failed to fetch user', {
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
});
}
}
},
: {
: (_, { input }, { user, dataSources }) => {
(!input. || input.. < ) {
(, {
: {
: ,
:
}
});
}
(!user) {
(, {
: {
: .
}
});
}
{
dataSources..(input);
} (error) {
(, {
: {
:
},
: error
});
}
}
}
};
Authentication and Authorization
Implement authentication and authorization patterns in resolvers and
context.
const requireAuth = (resolver) => {
return (parent, args, context, info) => {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
return resolver(parent, args, context, info);
};
};
const requireRole = (role: string) => (resolver) => {
return (parent, args, context, info) => {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
if (!context.user.roles.includes(role)) {
throw new GraphQLError('Insufficient permissions', {
extensions: { code: 'FORBIDDEN' }
});
}
return (parent, args, context, info);
};
};
resolvers = {
: {
: ( user),
: ()(
(_, __, { dataSources }) => {
dataSources..();
}
),
: (_, { id }, { user, dataSources }) => {
post = dataSources..(id);
(!post) {
();
}
(post. === && post. !== user?.) {
(, {
: { : }
});
}
post;
}
},
: {
: (
(_, { id, input }, { user, dataSources }) => {
post = dataSources..(id);
(post. !== user. && !user..()) {
(, {
: { : }
});
}
dataSources..(id, input);
}
)
}
};
Caching Strategies
Implement caching at the resolver level for improved performance.
import { createHash } from 'crypto';
const cache = new Map<string, { data: any; expiry: number }>();
const getCacheKey = (prefix: string, args: any): string => {
const hash = createHash('md5')
.update(JSON.stringify(args))
.digest('hex');
return `${prefix}:${hash}`;
};
const cacheResolver = (
resolver,
{ ttl = 300, prefix = 'cache' } = {}
) => {
return async (parent, args, context, info) => {
const key = getCacheKey(prefix, args);
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log('Cache hit:', key);
return cached.data;
}
result = (parent, args, context, info);
cache.(key, {
: result,
: .() + (ttl * )
});
result;
};
};
resolvers = {
: {
: (
(_, { limit }, { dataSources }) => {
dataSources..(limit);
},
{ : , : }
),
: (_, { id }, { redis, dataSources }) => {
cacheKey = ;
cached = redis.(cacheKey);
(cached) {
.(cached);
}
user = dataSources..(id);
redis.(cacheKey, , .(user));
user;
}
}
};
Resolver Middleware and Plugins
Create reusable middleware patterns for cross-cutting concerns.
const logResolver = (resolver) => {
return async (parent, args, context, info) => {
const start = Date.now();
const fieldName = info.fieldName;
try {
const result = await resolver(parent, args, context, info);
const duration = Date.now() - start;
console.log(`${fieldName} resolved in ${duration}ms`);
return result;
} catch (error) {
console.error(`${fieldName} failed:`, error);
throw error;
}
};
};
const timeResolver = (resolver) => {
return async (parent, args, context, info) => {
const start = performance.now();
const result = await resolver(parent, args, context, info);
const duration = performance.now() - start;
info.operation.extensions = info.operation. || {};
info... =
info... || {};
info...[info.] = duration;
result;
};
};
= () => {
middlewares.(
(acc),
resolver
);
};
resolvers = {
: {
: (
logResolver,
timeResolver,
requireAuth
)( (_, { id }, { dataSources }) => {
dataSources..(id);
})
}
};
Testing Resolvers
Write comprehensive tests for resolvers using mocked context and data
sources.
import { describe, it, expect, vi } from 'vitest';
describe('User Resolvers', () => {
it('should fetch user by id', async () => {
const mockUser = { id: '1', username: 'test' };
const mockContext = {
dataSources: {
userAPI: {
getUserById: vi.fn().mockResolvedValue(mockUser)
}
}
};
const result = await resolvers.Query.user(
null,
{ id: '1' },
mockContext,
{} as any
);
expect(result).toEqual(mockUser);
expect(mockContext.dataSources.userAPI.getUserById)
.toHaveBeenCalledWith('1');
});
it('should throw error when user not found', async () => {
const mockContext = {
dataSources: {
userAPI: {
getUserById: vi.fn().mockResolvedValue(null)
}
}
};
(
resolvers..(, { : }, mockContext, {} )
)..();
});
(, () => {
mockContext = {
: ,
: {}
};
(
resolvers..(, {}, mockContext, {} )
)..();
});
(, () => {
mockUsers = [
{ : , : },
{ : , : }
];
batchFn = vi.().(mockUsers);
loader = (batchFn);
mockContext = {
: { : loader }
};
[user1, user2] = .([
resolvers..(
{ : },
{},
mockContext,
{}
),
resolvers..(
{ : },
{},
mockContext,
{}
)
]);
(user1).(mockUsers[]);
(user2).(mockUsers[]);
(batchFn).();
(batchFn).([, ]);
});
});
Best Practices
- Keep resolvers thin: Delegate business logic to service layer,
use resolvers only for data fetching and transformation
- Use DataLoader: Implement DataLoader for any resolver that
fetches related data to avoid N+1 queries
- Leverage context: Store shared resources (database, auth, data
sources) in context for all resolvers
- Handle errors gracefully: Catch errors and throw meaningful
GraphQLError instances with appropriate codes
- Implement proper auth: Check authentication and authorization in
resolvers or middleware consistently
- Cache strategically: Cache expensive operations at resolver
level using in-memory or distributed cache
- Use typed resolvers: Define TypeScript types for resolver
functions to catch errors at compile time
- Test thoroughly: Write unit tests for resolvers with mocked
dependencies and edge cases
- Avoid blocking operations: Use async/await and parallel
execution where possible to prevent blocking
- Monitor performance: Log resolver execution time and identify
slow resolvers for optimization
Common Pitfalls
- N+1 queries: Fetching related data in loops without batching,
causing excessive database queries
- Blocking operations: Using synchronous operations in resolvers
that block the event loop
- Memory leaks: Storing data in closures or module scope that
grows unbounded
- Inconsistent error handling: Throwing raw errors without proper
GraphQLError wrapping and codes
- Over-fetching in resolvers: Fetching entire objects when only
specific fields are needed
- Context mutation: Modifying context object during resolver
execution, causing side effects
- Missing authentication checks: Forgetting to verify auth in
sensitive resolvers
- Improper DataLoader usage: Creating new DataLoader instances per
resolver instead of per request
- Circular resolver chains: Creating resolver dependencies that
cause infinite loops
- Not using info parameter: Ignoring the info parameter that
contains requested fields for optimization
When to Use This Skill
Use GraphQL resolver skills when:
- Implementing a new GraphQL server
- Optimizing existing resolver performance
- Debugging N+1 query problems
- Adding authentication and authorization
- Implementing data batching and caching
- Writing resolver unit tests
- Refactoring resolvers for better maintainability
- Adding logging and monitoring to resolvers
- Implementing custom middleware or plugins
- Migrating from REST API to GraphQL
Resources