Skip to main content

Chapter 4: Resolvers & Context

Resolvers are the heart of your GraphQL API. They define how to fetch the data for each field in your schema. In this chapter, we'll explore how to write effective resolvers and use context to share data across your resolvers.

Resolver Basics

Resolver Signatures

A resolver is a function that resolves a value for a type or field in a schema. Here's the basic signature:

type ResolverFn = (
parent: any,
args: { [argName: string]: any },
context: any,
info: GraphQLResolveInfo
) => any

Let's break down each parameter:

  1. parent: The result of the previous resolver
  2. args: Arguments provided to the field
  3. context: Shared context object
  4. info: Information about the execution state

Example Resolver

const resolvers = {
Query: {
user: async (parent, { id }, context, info) => {
return await context.prisma.user.findUnique({
where: { id }
});
}
},
User: {
posts: async (parent, args, context) => {
return await context.prisma.post.findMany({
where: { authorId: parent.id }
});
}
}
};

Using Context

Context Setup

The context object is created for each request and can be used to share data between resolvers:

import { createYoga } from '@graphql-yoga/node'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

const yoga = createYoga({
schema,
context: async ({ request }) => {
// Get the user token from the Authorization header
const token = request.headers.get('authorization')?.replace('Bearer ', '')

// Verify the token and get the user
const user = token ? await verifyToken(token) : null

return {
prisma,
user,
// Add other context values
requestId: crypto.randomUUID(),
timestamp: new Date()
}
}
})

Dependency Injection

Use context for dependency injection:

interface Context {
prisma: PrismaClient
user: User | null
services: {
auth: AuthService
email: EmailService
storage: StorageService
}
}

const resolvers = {
Mutation: {
createPost: async (_, { input }, context: Context) => {
// Use injected services
const post = await context.prisma.post.create({
data: input
})

await context.services.email.sendNotification({
to: post.author.email,
subject: 'Post Created',
body: `Your post "${post.title}" was created successfully`
})

return post
}
}
}

Error Handling

GraphQL Errors

Use GraphQLError for proper error handling:

import { GraphQLError } from 'graphql'

const resolvers = {
Query: {
user: async (_, { id }, context) => {
const user = await context.prisma.user.findUnique({
where: { id }
})

if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 }
}
})
}

return user
}
}
}

Custom Error Types

Create custom error classes:

class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 }
}
})
}
}

class AuthorizationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 }
}
})
}
}

// Usage in resolvers
const resolvers = {
Mutation: {
updatePost: async (_, { id, input }, context) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in')
}

const post = await context.prisma.post.findUnique({
where: { id }
})

if (post.authorId !== context.user.id) {
throw new AuthorizationError('You can only update your own posts')
}

return context.prisma.post.update({
where: { id },
data: input
})
}
}
}

Advanced Resolver Patterns

Field-level Authorization

const resolvers = {
User: {
email: (parent, _, context) => {
// Only show email to the user themselves or admins
if (context.user?.id !== parent.id && context.user?.role !== 'ADMIN') {
return null
}
return parent.email
}
}
}

Data Loaders

Use DataLoader for batching and caching:

import DataLoader from 'dataloader'

const createUserLoader = (prisma: PrismaClient) => {
return new DataLoader(async (ids: string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: ids } }
})

const userMap = new Map(users.map(user => [user.id, user]))
return ids.map(id => userMap.get(id) || null)
})
}

// In context
context: async ({ request }) => {
return {
prisma,
userLoader: createUserLoader(prisma)
}
}

// In resolvers
const resolvers = {
Post: {
author: async (parent, _, context) => {
return context.userLoader.load(parent.authorId)
}
}
}

Resolver Middleware

Create middleware for common resolver tasks:

const withAuth = (resolver: ResolverFn) => {
return async (parent, args, context, info) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in')
}
return resolver(parent, args, context, info)
}
}

const resolvers = {
Mutation: {
createPost: withAuth(async (_, { input }, context) => {
return context.prisma.post.create({
data: {
...input,
authorId: context.user.id
}
})
})
}
}

Testing Resolvers

Unit Testing

import { createTestClient } from 'apollo-server-testing'

describe('User Resolvers', () => {
it('fetches a user by id', async () => {
const mockContext = {
prisma: {
user: {
findUnique: jest.fn().mockResolvedValue({
id: '1',
name: 'John Doe'
})
}
}
}

const { query } = createTestClient({
schema,
context: mockContext
})

const res = await query({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id: '1' }
})

expect(res.data.user).toEqual({
id: '1',
name: 'John Doe'
})
})
})

Summary

In this chapter, we've covered:

  • Resolver basics and signatures
  • Context setup and dependency injection
  • Error handling with GraphQLError
  • Advanced patterns like DataLoader and middleware
  • Testing resolvers

These concepts are fundamental to building robust GraphQL APIs. In the next chapter, we'll explore authentication and authorization in detail.