Write efficient resolvers with DataLoader, batching, and N+1 prevention
Implement efficient GraphQL resolvers using DataLoader to prevent N+1 queries. Use this when building relationship fields or optimizing slow data fetching patterns.
/plugin marketplace add pluginagentmarketplace/custom-plugin-graphql/plugin install developer-roadmap@pluginagentmarketplace-graphqlThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/config.yamlassets/schema.jsonreferences/GUIDE.mdreferences/PATTERNS.mdscripts/validate.pyBuild performant data fetching with proper patterns
Master resolver implementation including the critical DataLoader pattern for preventing N+1 queries, context design, and error handling strategies.
| Pattern | Purpose | When to Use |
|---|---|---|
| DataLoader | Batch + cache | Any relationship field |
| Context | Request-scoped data | Auth, loaders, datasources |
| Field resolver | Computed fields | Derived data |
| Root resolver | Entry points | Query/Mutation fields |
// (parent, args, context, info) => result
const resolvers = {
Query: {
// Root resolver - parent is undefined
user: async (_, { id }, { dataSources }) => {
return dataSources.users.findById(id);
},
},
User: {
// Field resolver - parent is User object
posts: async (user, { first = 10 }, { loaders }) => {
return loaders.postsByAuthor.load(user.id);
},
// Computed field - sync is fine
fullName: (user) => `${user.firstName} ${user.lastName}`,
// Default resolver (implicit)
// email: (user) => user.email,
},
};
const DataLoader = require('dataloader');
// N+1 Problem:
// Query: { users { posts { title } } }
// Without DataLoader: 1 + N queries
// Solution: Batch loading
const createLoaders = () => ({
// Batch by foreign key
postsByAuthor: new DataLoader(async (authorIds) => {
// 1. Single query for all authors
const posts = await db.posts.findAll({
where: { authorId: { [Op.in]: authorIds } }
});
// 2. Group by author
const postsByAuthor = {};
posts.forEach(post => {
if (!postsByAuthor[post.authorId]) {
postsByAuthor[post.authorId] = [];
}
postsByAuthor[post.authorId].push(post);
});
// 3. Return in same order as input
return authorIds.map(id => postsByAuthor[id] || []);
}),
// Batch by primary key
userById: new DataLoader(async (ids) => {
const users = await db.users.findAll({
where: { id: { [Op.in]: ids } }
});
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) || null);
}),
});
// Usage in resolvers
const resolvers = {
Post: {
author: (post, _, { loaders }) => {
return loaders.userById.load(post.authorId);
},
},
User: {
posts: (user, _, { loaders }) => {
return loaders.postsByAuthor.load(user.id);
},
},
};
const createContext = async ({ req, res }) => {
// 1. Parse auth token
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
// 2. Create request-scoped loaders (IMPORTANT!)
const loaders = createLoaders();
// 3. Initialize data sources
const dataSources = {
users: new UserDataSource(db),
posts: new PostDataSource(db),
};
// 4. Request metadata
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
return {
user,
loaders,
dataSources,
requestId,
};
};
// Apollo Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
context: createContext,
});
import { GraphQLError } from 'graphql';
const resolvers = {
Mutation: {
createUser: async (_, { input }, { dataSources, user }) => {
// 1. Auth check
if (!user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
// 2. Validation (return errors, don't throw)
const validationErrors = validateInput(input);
if (validationErrors.length > 0) {
return { user: null, errors: validationErrors };
}
// 3. Business logic
try {
const newUser = await dataSources.users.create(input);
return { user: newUser, errors: [] };
} catch (error) {
// Known error
if (error.code === 'DUPLICATE_EMAIL') {
return {
user: null,
errors: [{ field: 'email', message: 'Already exists' }]
};
}
// Unknown error - throw
throw new GraphQLError('Internal error', {
extensions: { code: 'INTERNAL_ERROR' }
});
}
},
},
};
import { PubSub, withFilter } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Mutation: {
sendMessage: async (_, { input }, { dataSources }) => {
const message = await dataSources.messages.create(input);
// Publish event
pubsub.publish('MESSAGE_SENT', {
messageSent: message,
channelId: input.channelId,
});
return message;
},
},
Subscription: {
// Simple subscription
userCreated: {
subscribe: () => pubsub.asyncIterator(['USER_CREATED']),
},
// Filtered subscription
messageSent: {
subscribe: withFilter(
() => pubsub.asyncIterator(['MESSAGE_SENT']),
(payload, variables) => {
return payload.channelId === variables.channelId;
}
),
},
},
};
| Resolver Type | Target | Action if Exceeded |
|---|---|---|
| Simple field | < 10ms | Check DB indexes |
| DataLoader batch | < 50ms | Optimize query |
| Complex computation | < 200ms | Consider caching |
| Total request | < 500ms | Profile and optimize |
| Issue | Symptom | Solution |
|---|---|---|
| N+1 queries | Slow, many DB calls | Use DataLoader |
| Memory leak | Growing memory | Create loaders per request |
| Stale data | Wrong results | Clear DataLoader cache |
| Race condition | Intermittent errors | Don't mutate context |
// 1. Log DataLoader batches
const loader = new DataLoader(async (keys) => {
console.log(`Batching ${keys.length} keys`);
// ...
});
// 2. Time resolvers
const withTiming = (resolver) => async (...args) => {
const start = Date.now();
const result = await resolver(...args);
console.log(`Took ${Date.now() - start}ms`);
return result;
};
// 3. Request logging plugin
const loggingPlugin = {
requestDidStart() {
const start = Date.now();
return {
willSendResponse() {
console.log(`Request took ${Date.now() - start}ms`);
},
};
},
};
Skill("graphql-resolvers")
graphql-schema-design - Schema that resolvers implementgraphql-apollo-server - Server configurationgraphql-security - Auth in resolvers03-graphql-resolvers - For detailed guidanceThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.