Agent Skills: GraphQL Resolvers Skill

Write efficient resolvers with DataLoader, batching, and N+1 prevention

graphqldataloaderbatchingn+1-problemresolvers
backendID: pluginagentmarketplace/custom-plugin-graphql/graphql-resolvers

Skill Files

Browse the full folder contents for graphql-resolvers.

Download Skill

Loading file tree…

skills/graphql-resolvers/SKILL.md

Skill Metadata

Name
graphql-resolvers
Description
Write efficient resolvers with DataLoader, batching, and N+1 prevention

GraphQL Resolvers Skill

Build performant data fetching with proper patterns

Overview

Master resolver implementation including the critical DataLoader pattern for preventing N+1 queries, context design, and error handling strategies.


Quick Reference

| 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 |


Core Patterns

1. Resolver Signature

// (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,
  },
};

2. DataLoader Pattern

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);
    },
  },
};

3. Context Setup

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,
});

4. Error Handling

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' }
        });
      }
    },
  },
};

5. Subscription Resolvers

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;
        }
      ),
    },
  },
};

Performance Targets

| 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 |


Troubleshooting

| 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 |

Debug Techniques

// 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`);
      },
    };
  },
};

Usage

Skill("graphql-resolvers")

Related Skills

  • graphql-schema-design - Schema that resolvers implement
  • graphql-apollo-server - Server configuration
  • graphql-security - Auth in resolvers

Related Agent

  • 03-graphql-resolvers - For detailed guidance