typescript-utility-types
Use when typeScript utility types, mapped types, and advanced type manipulation. Use when creating flexible, type-safe TypeScript code.
graphql-resolvers
Use when implementing GraphQL resolvers with resolver functions, context management, DataLoader batching, error handling, authentication, and testing strategies.
Full skill instructions
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.
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: {
// parent: root value (usually undefined for Query)
// args: arguments passed to the query
// context: shared context object
// info: execution information
user: async (parent, args, context, info) => {
const { id } = args;
const { dataSources, user } = context;
// Use context to access data sources and auth info
return dataSources.userAPI.getUserById(id);
},
posts: async (parent, args, context, info) => {
const { limit, offset } = args;
// Access requested fields from info
const fields = info.fieldNodes[0].selectionSet.selections
.map(s => s.name.value);
return context.dataSources.postAPI.getPosts({
limit,
offset,
fields
});
}
}
};
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: {
// Field resolver for computed field
fullName: (parent) => {
return `${parent.firstName} ${parent.lastName}`;
},
// Field resolver for related data
posts: async (parent, args, { dataSources }) => {
// parent.id available from parent User object
return dataSources.postAPI.getPostsByAuthor(parent.id);
},
// Field resolver with arguments
friends: async (parent, { limit }, { dataSources }) => {
return dataSources.userAPI.getFriends(parent.id, limit);
},
// Async computed field
postCount: async (parent, _, { dataSources }) => {
return dataSources.postAPI.countByAuthor(parent.id);
}
},
Post: {
author: async (parent, _, { dataSources }) => {
// parent.authorId from parent Post object
return dataSources.userAPI.getUserById(parent.authorId);
},
comments: async (parent, _, { dataSources }) => {
return dataSources.commentAPI.getByPostId(parent.id);
}
}
};
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;
}
// Context creation function
const createContext = async ({ req }): Promise<Context> => {
// Extract and verify authentication token
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
// Initialize data sources
const dataSources = {
userAPI: new UserAPI(),
postAPI: new PostAPI(),
commentAPI: new CommentAPI()
};
// Initialize DataLoaders
const loaders = {
userLoader: new DataLoader(ids => batchGetUsers(ids)),
postLoader: new DataLoader(ids => batchGetPosts(ids))
};
return {
user,
dataSources,
db: database,
req,
loaders
};
};
// Using context in resolvers
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new Error('Not authenticated');
}
return user;
},
post: async (_, { id }, { loaders }) => {
return loaders.postLoader.load(id);
}
}
};
Resolvers execute in a chain where parent resolvers complete before child resolvers begin. Understanding execution order is crucial for optimization.
const resolvers = {
Query: {
// Step 1: Root resolver executes
user: async (_, { id }, { db }) => {
console.log('1. Fetching user');
return db.users.findById(id);
}
},
User: {
// Step 2: Field resolvers execute with parent data
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: {
// Step 3: Nested field resolvers execute
comments: async (parent, _, { db }) => {
console.log('3. Fetching comments for post', parent.id);
return db.comments.findByPostId(parent.id);
}
}
};
// Query execution order:
// query {
// user(id: "1") { # 1. User resolver
// posts { # 2. Posts resolver
// comments { # 3. Comments resolver
// text
// }
// }
// profile { # 2. Profile resolver (parallel)
// bio
// }
// }
// }
DataLoader solves the N+1 problem by batching multiple individual loads into a single batch request and caching results.
import DataLoader from 'dataloader';
// Batch function receives array of keys
// Must return array of results in same order
const batchGetUsers = async (userIds: string[]) => {
console.log('Batch loading users:', userIds);
// Single database query for all IDs
const users = await db.users.findByIds(userIds);
// Create map for O(1) lookup
const userMap = new Map(users.map(u => [u.id, u]));
// Return users in same order as input IDs
return userIds.map(id => userMap.get(id) || null);
};
// Create loader in context
const userLoader = new DataLoader(batchGetUsers, {
// Optional configuration
cache: true, // Cache results (default: true)
maxBatchSize: 100, // Maximum batch size
batchScheduleFn: cb => setTimeout(cb, 10) // Custom scheduling
});
const resolvers = {
Post: {
author: async (parent, _, { loaders }) => {
// These calls are automatically batched
return loaders.userLoader.load(parent.authorId);
}
},
Comment: {
author: async (parent, _, { loaders }) => {
// Added to same batch as Post.author
return loaders.userLoader.load(parent.authorId);
}
}
};
// Example: Without DataLoader (N+1 problem)
// Query for 10 posts = 1 query for posts + 10 queries for authors
//
// With DataLoader:
// Query for 10 posts = 1 query for posts + 1 batched query for all
// authors
// Composite key loader
interface CompositeKey {
userId: string;
type: string;
}
const batchGetUserData = async (keys: CompositeKey[]) => {
// Group by type for efficient querying
const byType = keys.reduce((acc, key) => {
acc[key.type] = acc[key.type] || [];
acc[key.type].push(key.userId);
return acc;
}, {});
// Fetch data by type
const results = await Promise.all(
Object.entries(byType).map(([type, userIds]) =>
fetchDataByType(type, userIds)
)
);
// Map back to original key order
return keys.map(key =>
results.find(r => r.userId === key.userId && r.type === key.type)
);
};
const dataLoader = new DataLoader(
batchGetUserData,
{
cacheKeyFn: (key: CompositeKey) => `${key.userId}:${key.type}`
}
);
// Prime the cache
await dataLoader.prime({ userId: '1', type: 'profile' }, userData);
// Clear specific key
dataLoader.clear({ userId: '1', type: 'profile' });
// Clear all cache
dataLoader.clearAll();
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) {
// Log full error for debugging
console.error('Error fetching user:', error);
// Throw safe error to client
if (error instanceof GraphQLError) {
throw error;
}
throw new GraphQLError('Failed to fetch user', {
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
});
}
}
},
Mutation: {
createPost: async (_, { input }, { user, dataSources }) => {
// Validation errors
if (!input.title || input.title.length < 3) {
throw new GraphQLError('Title must be at least 3 characters', {
extensions: {
code: 'BAD_USER_INPUT',
argumentName: 'title'
}
});
}
// Authentication errors
if (!user) {
throw new GraphQLError('Must be authenticated', {
extensions: {
code: ApolloServerErrorCode.UNAUTHENTICATED
}
});
}
try {
return await dataSources.postAPI.create(input);
} catch (error) {
throw new GraphQLError('Failed to create post', {
extensions: {
code: 'INTERNAL_SERVER_ERROR'
},
originalError: error
});
}
}
}
};
Implement authentication and authorization patterns in resolvers and context.
// Authentication middleware
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);
};
};
// Authorization middleware
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 resolver(parent, args, context, info);
};
};
const resolvers = {
Query: {
me: requireAuth((_, __, { user }) => user),
adminPanel: requireRole('ADMIN')(
async (_, __, { dataSources }) => {
return dataSources.adminAPI.getDashboard();
}
),
// Resource-based authorization
post: async (_, { id }, { user, dataSources }) => {
const post = await dataSources.postAPI.getById(id);
if (!post) {
throw new GraphQLError('Post not found');
}
// Check if user can view this post
if (post.status === 'DRAFT' && post.authorId !== user?.id) {
throw new GraphQLError('Cannot view draft posts', {
extensions: { code: 'FORBIDDEN' }
});
}
return post;
}
},
Mutation: {
updatePost: requireAuth(
async (_, { id, input }, { user, dataSources }) => {
const post = await dataSources.postAPI.getById(id);
// Check ownership
if (post.authorId !== user.id && !user.roles.includes('ADMIN')) {
throw new GraphQLError('Not authorized to update this post', {
extensions: { code: 'FORBIDDEN' }
});
}
return dataSources.postAPI.update(id, input);
}
)
}
};
Implement caching at the resolver level for improved performance.
import { createHash } from 'crypto';
// In-memory cache
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;
}
const result = await resolver(parent, args, context, info);
cache.set(key, {
data: result,
expiry: Date.now() + (ttl * 1000)
});
return result;
};
};
const resolvers = {
Query: {
// Cache for 5 minutes
popularPosts: cacheResolver(
async (_, { limit }, { dataSources }) => {
return dataSources.postAPI.getPopular(limit);
},
{ ttl: 300, prefix: 'popular-posts' }
),
// Redis caching
user: async (_, { id }, { redis, dataSources }) => {
const cacheKey = `user:${id}`;
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch and cache
const user = await dataSources.userAPI.getUserById(id);
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
}
};
Create reusable middleware patterns for cross-cutting concerns.
// Logging middleware
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;
}
};
};
// Timing middleware
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;
// Add timing to extensions
info.operation.extensions = info.operation.extensions || {};
info.operation.extensions.timing =
info.operation.extensions.timing || {};
info.operation.extensions.timing[info.fieldName] = duration;
return result;
};
};
// Compose middleware
const compose = (...middlewares) => (resolver) => {
return middlewares.reduceRight(
(acc, middleware) => middleware(acc),
resolver
);
};
const resolvers = {
Query: {
user: compose(
logResolver,
timeResolver,
requireAuth
)(async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
})
}
};
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)
}
}
};
await expect(
resolvers.Query.user(null, { id: '999' }, mockContext, {} as any)
).rejects.toThrow('User not found');
});
it('should require authentication', async () => {
const mockContext = {
user: null,
dataSources: {}
};
await expect(
resolvers.Query.me(null, {}, mockContext, {} as any)
).rejects.toThrow('Not authenticated');
});
it('should use DataLoader for batching', async () => {
const mockUsers = [
{ id: '1', username: 'user1' },
{ id: '2', username: 'user2' }
];
const batchFn = vi.fn().mockResolvedValue(mockUsers);
const loader = new DataLoader(batchFn);
const mockContext = {
loaders: { userLoader: loader }
};
// Make multiple calls
const [user1, user2] = await Promise.all([
resolvers.Post.author(
{ authorId: '1' },
{},
mockContext,
{} as any
),
resolvers.Post.author(
{ authorId: '2' },
{},
mockContext,
{} as any
)
]);
expect(user1).toEqual(mockUsers[0]);
expect(user2).toEqual(mockUsers[1]);
expect(batchFn).toHaveBeenCalledTimes(1);
expect(batchFn).toHaveBeenCalledWith(['1', '2']);
});
});
Use GraphQL resolver skills when:
Use when typeScript utility types, mapped types, and advanced type manipulation. Use when creating flexible, type-safe TypeScript code.
Use when writing shell scripts that need to run across different systems, shells, or environments. Covers POSIX compatibility and platform differences.
Use when optimizing Tailwind CSS for production, reducing bundle size, and improving performance. Covers PurgeCSS, JIT mode, and build optimization.
Generate production-ready code from a Figma component or frame using the Figma MCP server
Use when integrating Dialyzer into development workflows and CI/CD pipelines for Erlang/Elixir projects.
Use when ruboCop configuration including YAML config, departments, inheritance, and customization.
Use when Elixir OTP patterns including GenServer, Supervisor, Agent, and Task. Use when building concurrent, fault-tolerant Elixir applications.
Use when creating custom Ameba rules for Crystal code analysis including rule development, AST traversal, issue reporting, and rule testing.
Use when swift's optional handling patterns including optional binding, chaining, nil coalescing, and modern approaches to safely working with optional values while avoiding common pitfalls and force unwrapping.
Use when annotating code with structured metadata, tags, and markers for AI-assisted development workflows. Covers annotation formats, semantic tags, and integration with development tools.
Use when scala collections including immutable/mutable variants, List, Vector, Set, Map operations, collection transformations, lazy evaluation with views, parallel collections, and custom collection builders for efficient data processing.
Use when creating custom React Flow nodes, edges, and handles. Covers custom node components, resizable nodes, toolbars, and advanced customization.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience