Resolvers Explained

intermediate
12 min

Resolvers Explained

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.

One Function Per Field

Conceptually, a GraphQL server holds a resolver map mirroring the schema. In JavaScript it looks like this:

js
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:

  • parent - the value returned by the resolver one level up. For Post.author, that is the post object itself.
  • args - the arguments for this field, already parsed and type-checked ({ id: "7" }).
  • context - a per-request object you construct: database handles, the authenticated user, loaders. It is the standard channel for dependency injection and auth.
  • info - metadata about the execution: the AST of the query, the path, the return type. Rarely needed, occasionally invaluable (for example, to look ahead at requested subfields).

The Resolver Chain

Execution walks the query tree top-down. For:

graphql
query { post(id: "7") { title author { name } } }
  1. Query.post runs, returning a post record.
  2. Post.title runs with that record as parent.
  3. 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.

Default Resolvers Do the Boring Work

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: Auth and Per-Request State

Context is created once per request, making it the right home for authorization:

js
const server = new ApolloServer({ typeDefs, resolvers }); await startStandaloneServer(server, { context: async ({ req }) => ({ db, currentUser: await getUserFromToken(req.headers.authorization), }), });
js
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.

A Performance Preview

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.

Key Takeaways

  • Every field is backed by a resolver with the signature (parent, args, context, info).
  • Execution is a top-down chain: parent results feed child resolvers; siblings may run concurrently.
  • Default resolvers read same-named properties off parent, so you only write the interesting ones.
  • context is per-request state: databases, loaders, and the authenticated user.
  • The per-field model creates the N+1 problem, solved later with DataLoader.

Next lesson: we put schemas and resolvers together and build a real GraphQL server with Node.js.

Resolvers Explained - GraphQL | CodeYourCraft | CodeYourCraft