Claude
Skills
Sign in
Back

graphql-resolvers

Included with Lifetime
$97 forever

Use when implementing GraphQL resolvers with resolver functions, context management, DataLoader batching, error handling, authentication, and testing strategies.

Backend & APIs

What this skill does


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

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

Field resolvers define how to resolve individual fields on a type. The
parent argument contains the resolved parent object.

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

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

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

## Resolver Chains and Execution

Resolvers execute in a chain where parent resolvers complete before
child resolvers begin. Understanding execution order is crucial for
optimization.

```typescript
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 Pattern for Batching

DataLoader solves the N+1 problem by batching multiple individual
loads into a single batch request and caching results.

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

## Advanced DataLoader Patterns

```typescript
// 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();
```

## Async Error Handling

Proper error handling in resolvers ensures meaningful errors reach the
client while protecting sensitive information

Related in Backend & APIs