Writing Resolvers & Data Fetching
A schema defines what data exists. Resolvers define how to get it.
Every field in your schema can have a resolver - a function that returns the data for that field.
Schema (What) + Resolvers (How) = Working GraphQL API
Resolver Basics
A resolver is a function with this signature:
fieldName(parent, args, context, info) {
// Return the data for this field
}
parent
Result from the parent resolver. For root fields (Query/Mutation), this is often undefined.
args
Arguments passed to the field in the query. e.g., user(id: "123")
context
Shared data across all resolvers - database connections, auth info, etc.
info
Query AST and schema info. Rarely used directly.
Your First Resolvers
Let's build resolvers for a blog API:
// Schema
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
`;
// Resolvers
const resolvers = {
Query: {
// Get a single user by ID
user: (parent, args, context) => {
return context.db.users.findById(args.id);
},
// Get all users
users: (parent, args, context) => {
return context.db.users.findAll();
},
// Get a single post
post: (parent, args, context) => {
return context.db.posts.findById(args.id);
}
}
};
Default Resolvers
GraphQL has a default resolver that returns parent[fieldName]. This means you don't need resolvers for every field:
// If your database returns:
{
id: "123",
name: "Sarah Chen",
email: "sarah@example.com"
}
// GraphQL's default resolver handles these fields automatically!
// You don't need to write:
// User: {
// id: (parent) => parent.id,
// name: (parent) => parent.name,
// email: (parent) => parent.email
// }
Only write custom resolvers when you need to transform data or fetch from a different source. Let default resolvers do the heavy lifting.
Nested Resolvers
The power of GraphQL comes from nested data. Here's how to resolve relationships:
const resolvers = {
Query: {
user: (_, args, context) => context.db.users.findById(args.id),
post: (_, args, context) => context.db.posts.findById(args.id)
},
// Resolvers for User type fields
User: {
// Fetch posts for this user
posts: (parent, args, context) => {
// parent is the User object
return context.db.posts.findByAuthorId(parent.id);
}
},
// Resolvers for Post type fields
Post: {
// Fetch author for this post
author: (parent, args, context) => {
// parent is the Post object
return context.db.users.findById(parent.authorId);
}
}
};
Execution Flow
When this query runs:
query {
user(id: "123") {
name
posts {
title
author {
name
}
}
}
}
Mutation Resolvers
Mutations modify data and return results:
const resolvers = {
Mutation: {
createPost: async (_, args, context) => {
// Verify user is authenticated
if (!context.user) {
throw new Error("Authentication required");
}
// Create the post
const post = await context.db.posts.create({
title: args.input.title,
content: args.input.content,
authorId: context.user.id,
published: args.input.published || false
});
return post;
},
updatePost: async (_, args, context) => {
const post = await context.db.posts.findById(args.id);
// Check ownership
if (post.authorId !== context.user.id) {
throw new Error("Not authorized");
}
return context.db.posts.update(args.id, args.input);
},
deletePost: async (_, args, context) => {
const post = await context.db.posts.findById(args.id);
if (post.authorId !== context.user.id) {
throw new Error("Not authorized");
}
await context.db.posts.delete(args.id);
return true;
}
}
};
Setting Up Context
Context is created per-request and passed to all resolvers:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// Get auth token from headers
const token = req.headers.authorization || "";
// Verify and decode the token
const user = verifyToken(token);
return {
// Database connections
db: {
users: new UserService(),
posts: new PostService()
},
// Current user (null if not authenticated)
user,
// Other shared data
loaders: createLoaders() // For DataLoader (next lesson!)
};
}
});
Error Handling
Throw errors in resolvers - GraphQL handles them gracefully:
const resolvers = {
Query: {
user: async (_, args, context) => {
const user = await context.db.users.findById(args.id);
if (!user) {
throw new Error(`User with ID ${args.id} not found`);
}
return user;
}
}
};
Response with error:
{
"data": {
"user": null
},
"errors": [
{
"message": "User with ID 999 not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"]
}
]
}
Custom Error Classes
import { GraphQLError } from 'graphql';
// Custom errors with codes
class NotFoundError extends GraphQLError {
constructor(message) {
super(message, {
extensions: { code: 'NOT_FOUND' }
});
}
}
class AuthenticationError extends GraphQLError {
constructor(message = 'Authentication required') {
super(message, {
extensions: { code: 'UNAUTHENTICATED' }
});
}
}
// Usage in resolver
const resolvers = {
Query: {
user: async (_, args, context) => {
if (!context.user) {
throw new AuthenticationError();
}
const user = await context.db.users.findById(args.id);
if (!user) {
throw new NotFoundError(`User ${args.id} not found`);
}
return user;
}
}
};
Complete Server Example
import { ApolloServer, gql } from 'apollo-server';
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
me: User
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
}
`;
// In-memory data for demo
const users = [
{ id: "1", name: "Sarah", email: "sarah@example.com" },
{ id: "2", name: "Alex", email: "alex@example.com" }
];
const posts = [
{ id: "p1", title: "GraphQL Intro", content: "...", authorId: "1" },
{ id: "p2", title: "REST vs GraphQL", content: "...", authorId: "1" }
];
const resolvers = {
Query: {
me: (_, __, context) => context.user,
user: (_, args) => users.find(u => u.id === args.id),
posts: () => posts
},
Mutation: {
createPost: (_, args, context) => {
const post = {
id: `p${posts.length + 1}`,
title: args.title,
content: args.content,
authorId: context.user?.id || "1"
};
posts.push(post);
return post;
}
},
User: {
posts: (parent) => posts.filter(p => p.authorId === parent.id)
},
Post: {
author: (parent) => users.find(u => u.id === parent.authorId)
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
user: users[0] // Simulated logged-in user
})
});
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
Resolver Patterns
| Pattern | When to Use |
|---|---|
| Thin resolvers | Call service layer, don't put business logic in resolvers |
| Default resolvers | Let GraphQL handle simple field mapping |
| Context for shared state | DB connections, auth, loaders |
| Async resolvers | Return promises for DB/API calls |
Key Takeaways
- Resolvers fetch data - They're the "how" to the schema's "what"
- Four arguments - parent, args, context, info
- Default resolvers work - Only write custom ones when needed
- Context is per-request - Use it for auth, DB, shared data
- Nested resolution - Each field resolver receives its parent's result
Next up: The N+1 Problem - Learn about the most common GraphQL performance pitfall and how to fix it with DataLoader.