# GraphQL Resolvers ## Basic Resolver Pattern ```typescript import { GraphQLResolveInfo } from 'graphql'; // Resolver signature type Resolver = ( parent: TSource, args: TArgs, context: TContext, info: GraphQLResolveInfo ) => Promise | TReturn; // User resolvers const resolvers = { Query: { user: async ( parent, args: { id: string }, context: Context ): Promise => { return context.dataSources.users.findById(args.id); }, users: async ( parent, args: { first?: number; after?: string }, context: Context ): Promise => { return context.dataSources.users.findAll(args); }, }, Mutation: { createUser: async ( parent, args: { input: CreateUserInput }, context: Context ): Promise => { if (!context.user) { throw new Error('Unauthorized'); } return context.dataSources.users.create(args.input); }, }, }; ``` ## Context Setup ```typescript import { Request } from 'express'; import { User } from './models'; import { DataSources } from './datasources'; export interface Context { user: User | null; dataSources: DataSources; loaders: Loaders; req: Request; authToken: string | null; } // Apollo Server context const server = new ApolloServer({ typeDefs, resolvers, context: async ({ req }): Promise => { // Extract auth token const authToken = req.headers.authorization?.replace('Bearer ', '') || null; // Verify user let user: User | null = null; if (authToken) { user = await verifyToken(authToken); } // Create data sources const dataSources = new DataSources({ db: prisma, redis: redisClient, }); // Create DataLoaders const loaders = createLoaders(dataSources); return { user, dataSources, loaders, req, authToken, }; }, }); ``` ## DataLoader for N+1 Prevention ```typescript import DataLoader from 'dataloader'; // Create loaders export function createLoaders(dataSources: DataSources): Loaders { return { userLoader: new DataLoader( async (ids: readonly string[]) => { const users = await dataSources.users.findByIds([...ids]); // Return in same order as input ids return ids.map(id => users.find(u => u.id === id) || null); }, { cache: true, batchScheduleFn: (callback) => setTimeout(callback, 10), } ), postsByAuthorLoader: new DataLoader( async (authorIds: readonly string[]) => { const posts = await dataSources.posts.findByAuthorIds([...authorIds]); // Group by author return authorIds.map(authorId => posts.filter(p => p.authorId === authorId) ); } ), }; } // Field resolver using DataLoader const resolvers = { Post: { author: async ( post: Post, args, context: Context ): Promise => { // Batches multiple requests into single DB query return context.loaders.userLoader.load(post.authorId); }, }, User: { posts: async ( user: User, args, context: Context ): Promise => { return context.loaders.postsByAuthorLoader.load(user.id); }, }, }; ``` ## Field Resolvers ```typescript const resolvers = { User: { // Simple field resolver fullName: (user: User): string => { return `${user.firstName} ${user.lastName}`; }, // Async field resolver with DB query postCount: async ( user: User, args, context: Context ): Promise => { return context.dataSources.posts.countByAuthor(user.id); }, // Field resolver with arguments posts: async ( user: User, args: { first?: number; status?: PostStatus }, context: Context ): Promise => { return context.dataSources.posts.findByAuthor(user.id, { limit: args.first, status: args.status, }); }, // Nullable field with conditional logic profile: async ( user: User, args, context: Context ): Promise => { if (!user.hasProfile) return null; return context.loaders.profileLoader.load(user.id); }, }, }; ``` ## Interface Resolvers ```typescript const resolvers = { // Interface type resolver Searchable: { __resolveType(obj: Article | Video | Podcast): string { if ('content' in obj) return 'Article'; if ('duration' in obj) return 'Video'; if ('audioUrl' in obj) return 'Podcast'; throw new Error('Unknown Searchable type'); }, }, // Common interface fields (shared resolvers) Article: { id: (article: Article) => article.id, title: (article: Article) => article.title, description: (article: Article) => article.description, }, Video: { id: (video: Video) => video.id, title: (video: Video) => video.title, description: (video: Video) => video.description, }, }; ``` ## Union Resolvers ```typescript const resolvers = { // Union type resolver SearchResult: { __resolveType( obj: Article | Video | Podcast, context: Context, info: GraphQLResolveInfo ): string { if ('content' in obj) return 'Article'; if ('duration' in obj && 'url' in obj) return 'Video'; if ('audioUrl' in obj) return 'Podcast'; throw new Error('Unknown SearchResult type'); }, }, Query: { searchContent: async ( parent, args: { query: string }, context: Context ): Promise<(Article | Video | Podcast)[]> => { // Return mixed array of different types const [articles, videos, podcasts] = await Promise.all([ context.dataSources.articles.search(args.query), context.dataSources.videos.search(args.query), context.dataSources.podcasts.search(args.query), ]); return [...articles, ...videos, ...podcasts]; }, }, }; ``` ## Error Handling ```typescript import { GraphQLError } from 'graphql'; import { ApolloServerErrorCode } from '@apollo/server/errors'; const resolvers = { Query: { user: async ( parent, args: { id: string }, context: Context ): Promise => { const user = await context.dataSources.users.findById(args.id); if (!user) { throw new GraphQLError('User not found', { extensions: { code: 'USER_NOT_FOUND', http: { status: 404 }, userId: args.id, }, }); } return user; }, }, Mutation: { updateUser: async ( parent, args: { id: string; input: UpdateUserInput }, context: Context ): Promise => { // Check authentication if (!context.user) { throw new GraphQLError('Unauthorized', { extensions: { code: ApolloServerErrorCode.UNAUTHENTICATED, http: { status: 401 }, }, }); } // Check authorization if (context.user.id !== args.id && !context.user.isAdmin) { throw new GraphQLError('Forbidden', { extensions: { code: ApolloServerErrorCode.FORBIDDEN, http: { status: 403 }, }, }); } try { return await context.dataSources.users.update(args.id, args.input); } catch (error) { throw new GraphQLError('Failed to update user', { extensions: { code: 'UPDATE_FAILED', originalError: error, }, }); } }, }, }; ``` ## Pagination Resolvers ```typescript import { encodeCursor, decodeCursor } from './utils/cursor'; const resolvers = { Query: { posts: async ( parent, args: { first?: number; after?: string }, context: Context ): Promise => { const limit = Math.min(args.first || 10, 100); const cursor = args.after ? decodeCursor(args.after) : null; // Fetch one extra to determine hasNextPage const posts = await context.dataSources.posts.findAll({ limit: limit + 1, cursor, }); const hasNextPage = posts.length > limit; const edges = posts.slice(0, limit).map(post => ({ node: post, cursor: encodeCursor(post.id), })); return { edges, pageInfo: { hasNextPage, hasPreviousPage: !!cursor, startCursor: edges[0]?.cursor || null, endCursor: edges[edges.length - 1]?.cursor || null, }, totalCount: await context.dataSources.posts.count(), }; }, }, }; ``` ## Batching Patterns ```typescript // Batch multiple queries class UserDataSource { private db: PrismaClient; async findByIds(ids: string[]): Promise { // Single query instead of N queries return this.db.user.findMany({ where: { id: { in: ids } }, }); } async findByEmails(emails: string[]): Promise { return this.db.user.findMany({ where: { email: { in: emails } }, }); } } // DataLoader with caching const userLoader = new DataLoader( async (ids) => { console.log('Batching user queries:', ids.length); const users = await dataSources.users.findByIds([...ids]); return ids.map(id => users.find(u => u.id === id) || null); }, { cache: true, maxBatchSize: 100, batchScheduleFn: (callback) => setTimeout(callback, 10), } ); ``` ## Resolver Best Practices 1. **Use DataLoader**: Always batch and cache database queries 2. **Avoid N+1**: Use DataLoader for all foreign key relationships 3. **Type Safety**: Use TypeScript for resolver type safety 4. **Error Handling**: Throw GraphQLError with proper codes and extensions 5. **Authorization**: Check permissions in resolvers, not data sources 6. **Pagination**: Implement cursor-based pagination for lists 7. **Context**: Keep context creation lightweight 8. **Caching**: Use DataLoader caching per request 9. **Batching**: Batch queries with DataLoader or in data source 10. **Testing**: Unit test resolvers with mocked context