# GRAPHQL Resolvers

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

- Skill: `pluginagentmarketplace/graphql-resolvers` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add pluginagentmarketplace/graphql-resolvers`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pluginagentmarketplace/graphql-resolvers/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: pluginagentmarketplace (https://skillmd.com/u/pluginagentmarketplace)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pluginagentmarketplace/graphql-resolvers

---


# 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

```javascript
// (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

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

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

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

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

```javascript
// 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

