The schema declares what your API can do; resolvers are the functions that actually do it. Every field in a GraphQL response was produced by a resolver. Understanding the resolver chain is the single most important step in going from writing queries to building servers, and it also explains GraphQL performance characteristics we will revisit in lesson 11.
Conceptually, a GraphQL server holds a resolver map mirroring the schema. In JavaScript it looks like this:
const resolvers = {
Query: {
post: (parent, args, context, info) =>
context.db.posts.findById(args.id),
},
Post: {
author: (parent, args, context) =>
context.db.users.findById(parent.authorId),
},
};Each resolver receives four arguments:
Post.author, that is the post object itself.{ id: "7" }).Execution walks the query tree top-down. For:
query { post(id: "7") { title author { name } } }Query.post runs, returning a post record.Post.title runs with that record as parent.Post.author runs, fetching the user; then User.name runs with the user as parent.Sibling fields can resolve concurrently; a child never starts before its parent finishes. Resolvers can return promises, and virtually all real resolvers do.
Did you notice we never wrote Post.title? If you omit a resolver, the server uses a default resolver: it reads the property of the same name off parent (calling it if it is a function). Since Query.post returned an object that already has a title property, the default handles it. You only write resolvers for fields that need computation, renaming, or fetching, which keeps resolver maps small.
Context is created once per request, making it the right home for authorization:
const server = new ApolloServer({ typeDefs, resolvers });
await startStandaloneServer(server, {
context: async ({ req }) => ({
db,
currentUser: await getUserFromToken(req.headers.authorization),
}),
});Mutation: {
deletePost: async (_, { id }, { db, currentUser }) => {
const post = await db.posts.findById(id);
if (post.authorId !== currentUser.id) throw new GraphQLError("FORBIDDEN");
await db.posts.delete(id);
return { deletedPostId: id, errors: [] };
},
}Because authorization runs inside resolvers, it applies no matter which query path reached the field, unlike REST middleware that guards only one route.
One resolver per field means a list of 50 posts triggers Post.author 50 times, potentially 50 database queries. This is the famous N+1 problem, and the standard fix, DataLoader batching, gets a full treatment in lesson 11. For now, just internalize the execution model: it is elegant, and it is naive by default.
(parent, args, context, info).parent, so you only write the interesting ones.context is per-request state: databases, loaders, and the authenticated user.Next lesson: we put schemas and resolvers together and build a real GraphQL server with Node.js.