Overview

Resolvers are the functions that populate the data for each field in a GraphQL schema. They connect the GraphQL schema to your data sources and business logic.

Resolver Function Signature

Basic Resolver

const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      // Resolver implementation
      return getUserById(args.id);
    }
  }
};

Resolver Parameters

parent (root)

  • Description: Result of the parent field’s resolver
  • Type: Any (result of parent resolver)
  • Use Case: Accessing parent data in nested resolvers
const resolvers = {
  User: {
    posts: (parent, args, context, info) => {
      // parent is the User object
      return getPostsByUserId(parent.id);
    }
  }
};

args

  • Description: Arguments passed to the field
  • Type: Object with argument values
  • Use Case: Filtering, pagination, search parameters
const resolvers = {
  Query: {
    users: (parent, args, context, info) => {
      const { limit = 10, offset = 0, status } = args;
      return getUsers({ limit, offset, status });
    }
  }
};

context

  • Description: Shared context across all resolvers in a query
  • Type: Object (mutable across resolvers)
  • Use Case: Database connections, authentication, caching
const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      // Check authentication
      if (!context.user) {
        throw new AuthenticationError('Not authenticated');
      }
      
      // Use database from context
      return context.db.users.findById(args.id);
    }
  }
};

info

  • Description: AST representation of the query
  • Type: GraphQLResolveInfo
  • Use Case: Advanced introspection, query analysis
const resolvers = {
  Query: {
    users: (parent, args, context, info) => {
      // Get selected fields
      const selectedFields = info.fieldNodes[0].selectionSet.selections
        .map(selection => selection.name.value);
      
      return getUsersWithFields(args, selectedFields);
    }
  }
};

Resolver Types

Scalar Resolvers

const resolvers = {
  DateTime: {
    // Custom scalar resolver
    parseValue(value) {
      return new Date(value);
    },
    serialize(value) {
      return value.toISOString();
    },
    parseLiteral(ast) {
      if (ast.kind === Kind.STRING) {
        return new Date(ast.value);
      }
      return null;
    }
  }
};

Object Type Resolvers

const resolvers = {
  User: {
    fullName: (parent, args, context, info) => {
      return `${parent.firstName} ${parent.lastName}`;
    },
    
    posts: async (parent, args, context, info) => {
      return context.dataLoaders.posts.load(parent.id);
    },
    
    friends: (parent, args, context, info) => {
      return getFriends(parent.id, args.limit);
    }
  }
};

Interface Resolvers

const resolvers = {
  Node: {
    __resolveType(obj, context, info) {
      if (obj.__typename) {
        return obj.__typename;
      }
      
      // Type resolution logic
      if (obj.email) {
        return 'User';
      }
      if (obj.title) {
        return 'Post';
      }
      
      return null; // GraphQL error
    }
  }
};

Union Resolvers

const resolvers = {
  SearchResult: {
    __resolveType(obj, context, info) {
      if (obj.__typename) {
        return obj.__typename;
      }
      
      // Union type resolution
      if (obj.userId) {
        return 'User';
      }
      if (obj.postId) {
        return 'Post';
      }
      
      return null;
    }
  }
};

Data Loading Patterns

Direct Database Access

const resolvers = {
  Query: {
    user: async (parent, args, context, info) => {
      const user = await context.db.users.findById(args.id);
      return user;
    }
  },
  
  User: {
    posts: async (parent, args, context, info) => {
      const posts = await context.db.posts.findByAuthor(parent.id);
      return posts;
    }
  }
};

DataLoader (Batch Loading)

import DataLoader from 'dataloader';
 
const createDataLoaders = (db) => ({
  users: new DataLoader(async (ids) => {
    const users = await db.users.findByIds(ids);
    return ids.map(id => users.find(user => user.id === id));
  }),
  
  posts: new DataLoader(async (userIds) => {
    const posts = await db.posts.findByAuthorIds(userIds);
    return userIds.map(userId => 
      posts.filter(post => post.authorId === userId)
    );
  })
});
 
const resolvers = {
  User: {
    posts: (parent, args, context, info) => {
      return context.dataLoaders.posts.load(parent.id);
    }
  }
};

Cached Resolvers

const resolvers = {
  Query: {
    user: async (parent, args, context, info) => {
      const cacheKey = `user:${args.id}`;
      let user = await context.cache.get(cacheKey);
      
      if (!user) {
        user = await context.db.users.findById(args.id);
        await context.cache.set(cacheKey, user, { ttl: 300 });
      }
      
      return user;
    }
  }
};

Error Handling

Resolver Errors

const resolvers = {
  Query: {
    user: async (parent, args, context, info) => {
      try {
        const user = await context.db.users.findById(args.id);
        
        if (!user) {
          throw new UserNotFoundError(`User ${args.id} not found`);
        }
        
        return user;
      } catch (error) {
        if (error instanceof UserNotFoundError) {
          throw new GraphQLError(error.message, {
            extensions: {
              code: 'USER_NOT_FOUND',
              argumentName: 'id'
            }
          });
        }
        
        throw error; // Re-throw other errors
      }
    }
  }
};

Validation Errors

import { UserInputError } from 'apollo-server';
 
const resolvers = {
  Mutation: {
    createUser: async (parent, args, context, info) => {
      const { input } = args;
      
      // Validation
      if (input.password.length < 8) {
        throw new UserInputError('Password too short', {
          argumentName: 'input.password',
          validationErrors: ['Password must be at least 8 characters']
        });
      }
      
      return context.db.users.create(input);
    }
  }
};

Authentication Errors

import { AuthenticationError, ForbiddenError } from 'apollo-server';
 
const resolvers = {
  Query: {
    adminUsers: (parent, args, context, info) => {
      if (!context.user) {
        throw new AuthenticationError('Not authenticated');
      }
      
      if (context.user.role !== 'ADMIN') {
        throw new ForbiddenError('Insufficient permissions');
      }
      
      return context.db.users.findAdmins();
    }
  }
};

Performance Optimization

Query Complexity Analysis

const resolvers = {
  Query: {
    users: (parent, args, context, info) => {
      // Calculate complexity
      const complexity = calculateQueryComplexity(info);
      
      if (complexity > context.maxComplexity) {
        throw new GraphQLError('Query too complex');
      }
      
      return getUsers(args);
    }
  }
};

Field-Level Caching

const resolvers = {
  User: {
    avatarUrl: async (parent, args, context, info) => {
      const cacheKey = `user:${parent.id}:avatar`;
      let avatarUrl = await context.cache.get(cacheKey);
      
      if (!avatarUrl) {
        avatarUrl = await generateAvatarUrl(parent);
        await context.cache.set(cacheKey, avatarUrl, { ttl: 3600 });
      }
      
      return avatarUrl;
    }
  }
};

Selective Field Resolution

const resolvers = {
  User: {
    profile: (parent, args, context, info) => {
      // Only fetch profile if requested
      const requestedFields = info.fieldNodes[0].selectionSet.selections
        .map(selection => selection.name.value);
      
      if (requestedFields.includes('bio') || requestedFields.includes('website')) {
        return context.db.profiles.findByUserId(parent.id);
      }
      
      return null;
    }
  }
};

Testing Resolvers

Unit Testing

const { makeExecutableSchema } = require('@graphql-tools/schema');
const { graphql } = require('graphql');
 
describe('User Resolvers', () => {
  const mockContext = {
    db: {
      users: {
        findById: jest.fn()
      }
    }
  };
  
  it('should resolve user', async () => {
    mockContext.db.users.findById.mockResolvedValue({
      id: '1',
      name: 'John Doe'
    });
    
    const result = await graphql({
      schema,
      source: '{ user(id: "1") { id name } }',
      contextValue: mockContext
    });
    
    expect(result.data.user.name).toBe('John Doe');
  });
});

Integration Testing

const { createTestClient } = require('apollo-server-testing');
 
describe('User API', () => {
  const { query, mutate } = createTestClient(server);
  
  it('should get user', async () => {
    const GET_USER = `
      query GetUser($id: ID!) {
        user(id: $id) {
          id
          name
        }
      }
    `;
    
    const res = await query({
      query: GET_USER,
      variables: { id: '1' }
    });
    
    expect(res.data.user.name).toBe('John Doe');
  });
});

Resolver Composition

Resolver Middleware

const authMiddleware = (resolver) => {
  return (parent, args, context, info) => {
    if (!context.user) {
      throw new AuthenticationError('Not authenticated');
    }
    
    return resolver(parent, args, context, info);
  };
};
 
const resolvers = {
  Query: {
    users: authMiddleware((parent, args, context, info) => {
      return context.db.users.findAll();
    })
  }
};

Resolver Factories

const createCRUDResolvers = (model) => ({
  Query: {
    [`${model.name.toLowerCase()}`]: (parent, args, context, info) => {
      return context.db[model.name].findById(args.id);
    },
    
    [`${model.name.toLowerCase()}s`]: (parent, args, context, info) => {
      return context.db[model.name].findAll(args);
    }
  },
  
  Mutation: {
    [`create${model.name}`]: (parent, args, context, info) => {
      return context.db[model.name].create(args.input);
    },
    
    [`update${model.name}`]: (parent, args, context, info) => {
      return context.db[model.name].update(args.id, args.input);
    },
    
    [`delete${model.name}`]: (parent, args, context, info) => {
      return context.db[model.name].delete(args.id);
    }
  }
});

Best Practices

Error Handling

  • Consistent Errors: Use standard error formats
  • Appropriate HTTP Codes: Map GraphQL errors to HTTP codes
  • Detailed Messages: Provide helpful error information

Performance

  • Batch Loading: Use DataLoader for N+1 problems
  • Caching: Implement appropriate caching strategies
  • Limits: Set query complexity and depth limits

Security

  • Authentication: Check user permissions
  • Authorization: Validate access to resources
  • Input Validation: Sanitize and validate inputs

Maintainability

  • Separation of Concerns: Keep resolvers focused
  • Testing: Write comprehensive tests
  • Documentation: Document resolver behavior

0 items under this folder.