Queries & Resolvers

GraphQL — Queries & Resolvers
GraphQL
Chapter 3 · Queries & Resolvers

⚙️ Queries & Resolvers

Chapter 2 defined the schema's shape. This chapter is about how a query actually gets filled with real data — every field in the schema is backed by a function called a resolver, and understanding how resolvers connect to each other is the key to understanding GraphQL execution.

What a Resolver Is

A resolver is a function attached to one specific field, responsible for returning that field's value. Most scalar fields don't need custom code at all — if a field's name matches a property already present on its parent object, the runtime's default resolver just reads it directly.

The Resolver Signature

function resolver(parent, args, context, info) { /* ... */ }
ParameterWhat It Is
parentWhatever the parent field's resolver returned — not the original query's top-level arguments
argsThe arguments passed to this specific field in the query (e.g. id: 42)
contextShared data available to every resolver in one request — the authenticated user, a database connection (Chapter 8 builds on this)
infoMetadata about the query itself (field name, path, the full AST) — rarely touched directly

🪆 Resolving Nested Fields, Step by Step

Take this query against Chapter 2's schema:

query { user(id: 42) { name posts { title } } }
  1. Query.user's resolver runs first, with args.id === 42. It looks up and returns a User object.
  2. For name: no custom resolver exists, so the default resolver just reads parent.name off the User object from step 1.
  3. For posts: User.posts's resolver runs, with parent now being that same User object — it typically reads parent.id to look up that user's posts.
  4. For each returned post's title: again, a default resolver reads parent.titleparent here is now one individual Post object from step 3.

Notice that parent changes meaning at every level — it's always "whatever the field one level up just returned," never the original top-level id: 42 argument.

The Resolver Map

const resolvers = { Query: { user: (parent, args, context) => db.users.findById(args.id), posts: () => db.posts.findAll(), }, User: { posts: (parent) => db.posts.findByAuthorId(parent.id), // parent = the User }, Post: { author: (parent) => db.users.findById(parent.authorId), // parent = the Post }, };

When You Need a Custom Resolver

Whenever a field's value isn't already sitting on the parent object with a matching name — a relationship to look up, a computed value, or data pulled from a different source entirely.

When the Default Suffices

Most scalar fields — User.name, Post.title — need zero resolver code, since the parent object already carries that exact property.

One Handler vs Many Small Functions

REST

One route handler builds and returns the entire response body for an endpoint — all the logic for that shape lives in one function.

GraphQL

Many small, focused resolvers — one per field — compose together automatically into whatever shape the client's query actually asked for.

💻 Coding Challenges

Challenge 1: Write the Post.comments Resolver

Using Chapter 2's Comment type, write the resolver for Post.comments that looks up all comments belonging to a given post, assuming a db.comments.findByPostId(postId) function exists.

Goal: Practice writing a resolver that correctly uses parent to find related data.

→ Solution

Challenge 2: Trace a Three-Level Query

For the query { user(id: 5) { posts { comments { text } } } }, list each resolver that runs, in order, and state what parent is at each step.

Goal: Practice tracing execution through more nesting than the chapter's own worked example.

→ Solution

Challenge 3: Fix a Resolver Using the Wrong Argument

A buggy User.posts resolver is written as (parent, args) => db.posts.findByAuthorId(args.id), and it fails whenever posts is queried without an id argument directly on it. Explain the bug and provide the fix.

Goal: Practice recognizing the exact mistake this chapter's tip box warns about.

→ Solution

⚠️ Gotcha: Confusing parent With the Original Query's Arguments

Coming from REST, where one handler has access to the entire request at once, it's natural to expect a nested resolver to somehow still have access to the original top-level arguments (like id: 42 from the user field). It doesn't — a resolver only ever sees its own field's args, plus whatever parent the field one level up returned. A User.posts resolver has no id argument of its own; it needs to read parent.id — the id property already present on the User object passed down to it — not reach for some argument that was never passed to this specific field at all.

🎯 What's Next

With reading data covered, the next chapter turns to writing it: Mutations & Input Types — the Mutation root type in practice, input types, and returning updated data after a write.