Skip to content
beginner Phase 4 · Resolvers and Data Source Integration

Resolver Functions Fundamentals

How resolvers map each schema field to the function that produces its value.

45m
1 problems
Topic Progress 0%

Every Field Is a Function Call You Have Not Written Yet

Every Field Is a Function Call You Have Not Written Yet

A schema describes a tree, a document selects a slice of it, and the server still has to produce actual values. Resolvers do that: one function per field, turning inputs into the value that field contributes to the response.

type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  name: String!
  email: String!
}
const resolvers = {
  Query: {
    user: (_root, { id }) => db.users.find(id),
    // returns { id: 1, name: "Ada", email: "ada@example.com" }
  },
  // User.id, User.name, User.email need no code: the default resolver
  // reads the matching property from the parent value.
};

For the document { user(id: "1") { name } } the executor performs exactly two steps:

Query.user(args)  ──►  { id: 1, name: "Ada", email: "ada@example.com" }
                                        │
                             default resolver reads .name
                                        ▼
                        response: { "user": { "name": "Ada" } }

The model to keep: the response is assembled field by field; it is not a serialization of whatever the top resolver returned. That is why a client asking for two fields receives exactly two, and why adding a field to the schema disturbs nothing — it either falls back to the default resolver or gets a function of its own.

It also explains the shape of resolver maps. Keys are type names, then field names:

{ Query: { ... }, Mutation: { ... }, User: { ... }, Post: { ... } }

A resolver declared under the wrong type silently does nothing, and the field falls back to the default — one of the most common "why is this always null" debugging sessions in GraphQL.

The Four Inputs Every Resolver Receives

The Four Inputs Every Resolver Receives

Every resolver is invoked with the same four arguments, in the same order:

Position Name Carries
1 parent (aka root) the value the parent field resolved to
2 args coerced arguments for this field
3 context request-shared state: user, data sources, loaders
4 info AST, schema nodes, path, and return type for this field

parent is what makes nesting work. Query.user returns a user object, so User.orders receives that object and can query with its id:

const resolvers = {
  Query: {
    user: (_p, { id }, ctx) => ctx.dataSources.users.byId(id),
  },
  User: {
    orders: (user, { first }, ctx) =>
      ctx.dataSources.orders.forUser(user.id, first),
    // user === whatever Query.user returned; nothing more, nothing less
  },
};

If Query.user returned a plain string instead of an object, User.orders would receive that string — resolvers do not know about the schema's intent, only about the value handed to them. The full downward journey of that value is parent argument flow.

context is constructed once per request and shared by every resolver in that execution; resolver context covers what belongs there and the cross-request leak you get by building it too early. args arrives already coerced: types, defaults, and non-null rules were applied during validation.

info is the escape hatch. info.path says where you are (["user", "orders", 3]), info.parentType and info.returnType describe the schema node being filled, and info.fieldNodes holds the raw selection including aliases and directives. Reach for it when building dataloader keys, accounting query cost, or tracing — never as a substitute for arguments the schema already declared.

A resolver does not have to receive all four. (_p, args) => ... is fine when context is unused; positional destructuring keeps signatures honest about what a field actually needs.

Missing Resolvers, Undefined, and Resolver Independence

Missing Resolvers, Undefined, and Resolver Independence

Three behaviors are worth predicting before you meet them in production.

Field order in the schema and document does not sequence anything. Resolvers run as the executor reaches them, and siblings may execute concurrently. Code that assumes "the field listed first ran first" breaks intermittently under load — what actually overlaps is explained in async resolver patterns.

A missing resolver falls back to the default resolver, which reads parent[fieldName]:

// schema: type User { nickname: String }
resolveDefault({ nickname: "ada" }, "nickname")  // -> "ada"
resolveDefault({ nick: "ada" }, "nickname")       // -> undefined (names differ)

Renamed properties, computed values, or data keyed differently all need an explicit resolver. The default is why plain JSON backends appear to need no resolvers at all — and why renaming a property in an upstream JSON payload silently turns a field null.

undefined becomes null, or an error:

resolver returns undefined  +  field is nullable  ->  "field": null
resolver returns undefined  +  field is String!   ->  field error, null bubbles

The second line is where "this used to work" bugs live: a data source stopped returning a property, the field was non-null, and the whole parent object went null. The bubbling rule itself is resolver error propagation.

Because resolvers are independent functions, they are cheap to unit test — call the function with a hand-made parent, literal args, and a stub context, then assert the return value. No server, no HTTP, and the failure you are chasing reproducers in a file you can step through. The move from stubs to real integrations is database data source wiring.

Practice Problems

0 / 1 solved
Implement Resolvers for a Nested Author/Book Schema
Resolver Implementation

Given a schema where Query.book returns a book, Book.author resolves its author, and Author.books lists that author's books with a limit argument, write the resolver map. Scalars on User/Book must come from the default resolver, and every database access must go through the context data sources.

Example:

Input: { book(id: "b1") { title author { name books(first: 2) { title } } } }

Output: { "data": { "book": { "title": "Editing Emacs", "author": { "name": "Ada", "books": [ { "title": "Notes" }, { "title": "Signals" } ] } } } }

Book.author receives the book value returned by Query.book, so it can read book.authorId; Author.books then receives the author value and uses its own id.

Optimal Solution — O(1) time, O(1) space

One resolver per field that needs work, default resolution for plain properties, and parent values threaded downward for nested ids.

const resolvers = {
  Query: {
    book: (_p, { id }, ctx) => ctx.dataSources.books.byId(id),
  },
  Book: {
    author: (book, _args, ctx) => ctx.dataSources.authors.byId(book.authorId),
    // title, id, year: no resolver — default resolver reads them off the book
  },
  Author: {
    books: (author, { first = 20 }, ctx) =>
      ctx.dataSources.books.byAuthor(author.id, { limit: first }),
  },
};

Edge Cases:

  • A book with a null authorId must resolve author to null only if the field is nullable
  • Omitting first must apply the declared default rather than returning an unbounded list
  • The default resolver returning undefined on a non-null scalar turns the whole book null

Quiz

1. In User.orders, what does the first resolver argument contain?

Question 1 options

2. The schema declares nickname: String, no resolver exists for it, and the parent value is { nick: "ada" }. What does the client receive?

Question 2 options

3. Which of these is not one of the four standard resolver inputs?

Question 3 options

4. Why can you not use the order of fields in a document to sequence work?

Question 4 options

Flashcards

Question

What a resolver is

Answer

The function that produces the value for exactly one field in the selection set. The response is assembled field by field from resolver returns.

Question

The four resolver inputs

Answer

parent (value the parent field resolved to), args (coerced arguments), context (per-request shared state), info (AST, schema, path, return type).

Question

Default resolver behavior

Answer

Reads parent[fieldName]. Perfect for plain JSON, silent failure when the property name differs — that mismatch is a common cause of unexpected nulls.

Question

undefined from a resolver

Answer

Nullable field -> null in the response. Non-null field -> field error, and null bubbles to the nearest nullable ancestor.

Question

Why resolver maps key by type name

Answer

{ Query: {...}, User: {...} } — a resolver registered under the wrong type never runs and the field silently falls back to the default resolver.

Revision Notes

Key Takeaways

  • 1. Every selected field maps to one resolver call; responses are built field by field, not serialized wholesale
  • 2. parent carries the value the enclosing field produced — that is the entire mechanism behind nesting
  • 3. The default resolver explains both the zero-boilerplate case and the silently-null case
  • 4. Resolvers are independent functions, so they run concurrently where allowed and unit-test without a server

Interview Tips

  • • Trace a nested document out loud: which resolver runs, what it returns, and what the child resolver receives as parent — interviewers listen for that causal chain
  • • Be ready to explain why a field is null: no resolver, property-name mismatch, or a non-null child bubbling — three different causes, three different fixes

Cheat Sheet

Resolver Fundamentals Cheat Sheet

const resolvers = {
  Query: {
    user: (parent, args, context, info) => context.dataSources.users.byId(args.id),
  },
  User: {
    orders: (user, { first }, context) => context.dataSources.orders.forUser(user.id, first),
  },
};
  • Signature: parent, args, context, info — in that order, every time
  • Missing resolver → default resolver reads parent[fieldName] → undefined → null (or error if non-null)
  • Wrong type key in the map = resolver never runs, field silently null
  • args is already coerced (defaults, non-null, enums applied)
  • info for path/schema/selection metadata, not for arguments
  • Sibling fields may run concurrently — never sequence work by document order

Continue Learning