What Are Resolvers?
Resolvers are the functions that actually fetch the data for each field in your GraphQL schema. Every field on every type has a resolver function. When the server receives a query, it calls the resolver for each field in the query, starting from the root and working down through nested fields.
A resolver function receives four arguments: parent (the result of the parent resolver), args (the arguments passed to the field), context (shared state like database connections and auth info), and info (metadata about the query execution).
// Resolver function signature
type ResolverFn = (
parent: any, // Result from the parent resolver
args: any, // Arguments passed to the field
context: Context, // Shared context (DB, auth, etc.)
info: GraphQLResolveInfo // Query execution metadata
) => any;
Building Resolvers Step by Step
Let us build a complete set of resolvers for a blog API. First, the schema:
type Query {
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
post(id: ID!): Post
posts(filter: PostFilterInput): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
postsCount: Int!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
comments: [Comment!]!
commentsCount: Int!
createdAt: String!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
The Context Object
The context is created fresh for every request and shared across all resolvers. It typically contains database connections, authenticated user information, and data loaders:
import { PrismaClient } from '@prisma/client';
import DataLoader from 'dataloader';
interface Context {
prisma: PrismaClient;
currentUser: User | null;
loaders: {
userLoader: DataLoader<string, User>;
postsByAuthorLoader: DataLoader<string, Post[]>;
};
}
// Create context for each request
function createContext({ req }: { req: Request }): Context {
const prisma = new PrismaClient();
const token = req.headers.get('authorization')?.replace('Bearer ', '');
const currentUser = token ? verifyToken(token) : null;
return {
prisma,
currentUser,
loaders: {
userLoader: new DataLoader(async (ids: readonly string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: [...ids] } },
});
return ids.map(id => users.find(u => u.id === id)!);
}),
postsByAuthorLoader: new DataLoader(async (authorIds: readonly string[]) => {
const posts = await prisma.post.findMany({
where: { authorId: { in: [...authorIds] } },
});
return authorIds.map(id => posts.filter(p => p.authorId === id));
}),
},
};
}
Query Resolvers
const resolvers = {
Query: {
// Fetch a single user by ID
user: async (_parent: unknown, args: { id: string }, ctx: Context) => {
const user = await ctx.prisma.user.findUnique({
where: { id: args.id },
});
return user; // Returns null if not found, matching nullable return type
},
// Fetch a list of users with pagination
users: async (
_parent: unknown,
args: { limit: number; offset: number },
ctx: Context
) => {
return ctx.prisma.user.findMany({
take: args.limit,
skip: args.offset,
orderBy: { createdAt: 'desc' },
});
},
// Fetch a single post by ID
post: async (_parent: unknown, args: { id: string }, ctx: Context) => {
return ctx.prisma.post.findUnique({
where: { id: args.id },
});
},
// Fetch posts with optional filtering
posts: async (
_parent: unknown,
args: { filter?: { status?: string; authorId?: string; search?: string } },
ctx: Context
) => {
const where: any = {};
if (args.filter?.status) where.status = args.filter.status;
if (args.filter?.authorId) where.authorId = args.filter.authorId;
if (args.filter?.search) {
where.OR = [
{ title: { contains: args.filter.search, mode: 'insensitive' } },
{ body: { contains: args.filter.search, mode: 'insensitive' } },
];
}
return ctx.prisma.post.findMany({ where, orderBy: { createdAt: 'desc' } });
},
},
};
Field Resolvers (The Resolver Chain)
When GraphQL resolves nested fields, it passes the parent object's result to child resolvers. This is how the resolver chain works — each level receives the resolved data from the level above:
const resolvers = {
// ... Query resolvers above
// Field resolvers for User type
User: {
// parent is the User object returned by the parent resolver
posts: async (parent: User, _args: unknown, ctx: Context) => {
// Use DataLoader to batch and cache user post lookups
return ctx.loaders.postsByAuthorLoader.load(parent.id);
},
postsCount: async (parent: User, _args: unknown, ctx: Context) => {
return ctx.prisma.post.count({
where: { authorId: parent.id },
});
},
},
// Field resolvers for Post type
Post: {
// Resolve the author field — parent is the Post object
author: async (parent: Post, _args: unknown, ctx: Context) => {
// Use DataLoader to avoid N+1 queries
return ctx.loaders.userLoader.load(parent.authorId);
},
comments: async (parent: Post, _args: unknown, ctx: Context) => {
return ctx.prisma.comment.findMany({
where: { postId: parent.id },
orderBy: { createdAt: 'asc' },
});
},
commentsCount: async (parent: Post, _args: unknown, ctx: Context) => {
return ctx.prisma.comment.count({
where: { postId: parent.id },
});
},
},
// Field resolvers for Comment type
Comment: {
author: async (parent: Comment, _args: unknown, ctx: Context) => {
return ctx.loaders.userLoader.load(parent.authorId);
},
post: async (parent: Comment, _args: unknown, ctx: Context) => {
return ctx.prisma.post.findUnique({
where: { id: parent.postId },
});
},
},
};
How the Resolver Chain Works
When this query executes:
Query.postruns first, returning a Post object from the database- The Post object becomes the
parentargument forPost.author,Post.comments, etc. - Each Comment becomes the
parentforComment.author - Scalar fields (title, text, etc.) resolve automatically from the parent object — no resolver needed
Mutation Resolvers
const resolvers = {
// ... Query and type resolvers
Mutation: {
createUser: async (
_parent: unknown,
args: { input: { name: string; email: string } },
ctx: Context
) => {
// Validate input
if (!args.input.email.includes('@')) {
throw new GraphQLError('Invalid email address', {
extensions: { code: 'BAD_USER_INPUT' },
});
}
return ctx.prisma.user.create({
data: {
name: args.input.name,
email: args.input.email,
},
});
},
createPost: async (
_parent: unknown,
args: { input: { title: string; body: string; isPublished?: boolean } },
ctx: Context
) => {
// Check authentication
if (!ctx.currentUser) {
throw new GraphQLError('You must be logged in', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return ctx.prisma.post.create({
data: {
title: args.input.title,
body: args.input.body,
isPublished: args.input.isPublished ?? false,
authorId: ctx.currentUser.id,
},
});
},
updatePost: async (
_parent: unknown,
args: { id: string; input: { title?: string; body?: string; isPublished?: boolean } },
ctx: Context
) => {
if (!ctx.currentUser) {
throw new GraphQLError('You must be logged in', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
// Check ownership
const post = await ctx.prisma.post.findUnique({ where: { id: args.id } });
if (!post || post.authorId !== ctx.currentUser.id) {
throw new GraphQLError('Post not found or unauthorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return ctx.prisma.post.update({
where: { id: args.id },
data: args.input,
});
},
deletePost: async (
_parent: unknown,
args: { id: string },
ctx: Context
) => {
if (!ctx.currentUser) {
throw new GraphQLError('You must be logged in', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const post = await ctx.prisma.post.findUnique({ where: { id: args.id } });
if (!post || post.authorId !== ctx.currentUser.id) {
throw new GraphQLError('Post not found or unauthorized', {
extensions: { code: 'FORBIDDEN' },
});
}
await ctx.prisma.post.delete({ where: { id: args.id } });
return true;
},
},
};
Resolver Best Practices
- Keep resolvers thin: Move business logic into service layers, not resolvers
- Use DataLoader: Prevent N+1 queries by batching and caching database calls
- Default resolvers work: If a field name matches a property on the parent object, GraphQL resolves it automatically
- Always validate in mutations: Check auth, permissions, and input before writing data
- Throw GraphQLError: Use structured errors with extension codes for proper client handling