Resolvers
A resolver is a function that fetches the value for a field. The shape is (parent, args, context, info). Compose resolvers per field; let the executor handle traversal + batching.
Field resolvers, context, DataLoader, errors
EXAMPLE
// 1) Schema
const typeDefs = /* GraphQL */ `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts(authorId: ID): [Post!]!
}
type Mutation {
createUser(input: NewUserInput!): User!
}
input NewUserInput {
name: String!
email: String!
}
`;
// 2) Top-level resolvers
const resolvers = {
Query: {
users: (_, __, ctx) => ctx.db.users.findAll(),
user: (_, { id }, ctx) => ctx.db.users.findById(id),
posts: (_, { authorId }, ctx) =>
ctx.db.posts.find({ authorId: authorId ?? undefined }),
},
Mutation: {
createUser: async (_, { input }, ctx) => {
if (!ctx.user || ctx.user.role !== 'admin') {
throw new GraphQLError('forbidden', { extensions: { code: 'FORBIDDEN' } });
}
return ctx.db.users.create(input);
},
},
};
// 3) Field-level resolvers — for relations / computed fields
resolvers.User = {
// Each User's `posts` field — runs per User
posts: (user, _, ctx) => ctx.db.posts.findByAuthor(user.id),
// Computed field
initials: (user) => user.name.split(' ').map(p => p[0]).join(''),
// Lazy-load expensive fields only when requested
avatarUrl: (user, _, ctx) => ctx.s3.getAvatarUrl(user.id),
};
resolvers.Post = {
author: (post, _, ctx) => ctx.db.users.findById(post.authorId),
};
// 4) Resolver arguments
// (parent, args, context, info)
// parent — value from the PARENT field's resolver (the User object for User.posts)
// args — arguments from the query (e.g. { id }, { input })
// context — per-request shared object (db, current user, dataloader, etc.)
// info — schema/AST info (rarely needed)
// 5) Context — build per request
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return {
user,
db: createDbClient(),
loaders: {
user: new DataLoader(ids => db.users.findByIds(ids)),
postsByAuthor: new DataLoader(ids => db.posts.findByAuthors(ids)),
},
};
},
});
// 6) The N+1 problem + DataLoader
// query: { users { posts { author { name } } } }
// Without batching: 1 query for users → N for posts → N×M for authors → ✗
//
// DataLoader batches + caches per request:
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findByIds(ids);
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id)); // MUST return in same order as ids
});
// Then in resolver:
resolvers.Post.author = (post, _, ctx) => ctx.loaders.user.load(post.authorId);
// ALL authors for ALL posts in ONE query.
// 7) Errors
import { GraphQLError } from 'graphql';
resolvers.Query.user = async (_, { id }, ctx) => {
if (!ctx.user) {
throw new GraphQLError('Unauthenticated', { extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } } });
}
const user = await ctx.db.users.findById(id);
if (!user) {
throw new GraphQLError('User not found', { extensions: { code: 'NOT_FOUND' } });
}
return user;
};
// 8) Authorisation per resolver
function requireRole(role) {
return (resolver) => async (parent, args, ctx, info) => {
if (ctx.user?.role !== role) throw new GraphQLError('Forbidden');
return resolver(parent, args, ctx, info);
};
}
resolvers.Mutation.deleteUser = requireRole('admin')(async (_, { id }, ctx) => {
await ctx.db.users.delete(id);
return true;
});
// 9) Subscriptions resolver
resolvers.Subscription = {
postCreated: {
subscribe: (_, __, ctx) => ctx.pubsub.asyncIterableIterator(['POST_CREATED']),
},
};
// 10) Default scalars + custom
import { GraphQLScalarType, Kind } from 'graphql';
resolvers.DateTime = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 date-time',
serialize(value) { return value instanceof Date ? value.toISOString() : value; },
parseValue(value) { return new Date(value); },
parseLiteral(ast) { return ast.kind === Kind.STRING ? new Date(ast.value) : null; },
});
// 11) Union / interface — resolveType
resolvers.SearchResult = {
__resolveType(obj) {
if (obj.email) return 'User';
if (obj.title) return 'Post';
return null;
},
};
// 12) Default resolver behavior
// If a field on a type has NO explicit resolver, GraphQL returns parent[fieldName] if it exists.
// So you only need resolvers for: relations, computed fields, fields with auth, lazy loading.
// 13) Best practices
// • Keep resolvers thin — call into services / repos
// • Use DataLoader for every belongs-to / has-many relationship
// • Centralise auth via a wrapping HOF (requireRole, requireAuth)
// • Throw GraphQLError with extensions.code — clients branch on code, not message
// • Don't return DB rows directly if they contain secrets — pluck what's safe
// 14) Modern alternatives
// • Code-first (Pothos, Nexus, TypeGraphQL) — types flow from code
// • Apollo Federation — split schema across services; resolvers on each
// • Hasura / PostGraphile — auto-generate resolvers from a DB schema
// 15) Testing
import { execute } from 'graphql';
import { buildSchema } from 'graphql';
const result = await execute({
schema,
document: parse(`{ user(id: "1") { name posts { title } } }`),
contextValue: { user: testUser, db: mockDb, loaders: createLoaders(mockDb) },
});
expect(result.data).toMatchObject({ user: { name: 'Ada', posts: [...] } });
Why it matters
DataLoader is non-negotiable for any GraphQL backend with relations. One request, one batched query per relation type — the N+1 problem disappears without changing your schema.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
const resolvers = {
Query: { user: (_, { id }, ctx) => ctx.db.users.find(id) },
User: { posts: (u, _, ctx) => ctx.db.posts.byUser(u.id) },
};
Try it Yourself »
Discussion
Loading…