NodeJSResolvers

Resolvers

A schema declares what data exists; resolvers are the functions that actually fetch it. GraphQL executes a query by walking its tree top-down, calling one resolver per requested field and feeding each parent's result into its children. Resolvers are where your API meets databases, REST services, and other backends — and where the infamous N+1 query problem lives, the single most important performance pitfall in GraphQL. This page covers the resolver signature and its four arguments, the execution model, default resolvers, the N+1 problem, and how DataLoader batches it away.

The resolver signature — four arguments

JS
const resolvers = {
  Query: {
    // Every resolver receives the same four arguments:
    user: (parent, args, context, info) => {
      //   parent  — the result of the PARENT field (undefined for root fields)
      //   args    — the field's arguments: { id: '42' }
      //   context — per-request shared object (auth, db, dataloaders)
      //   info    — AST details about the query (rarely needed)
      return context.db.users.findById(args.id)
    },
  },
  User: {
    // 'parent' here is the User returned above; resolve its posts:
    posts: (parent, args, context) => context.db.posts.findByAuthor(parent.id),
  },
}

Argument

Is

Most common use

parent

The value the parent field resolved to

Read parent.id to fetch related data

args

The arguments passed to this field

Lookup keys, filters, pagination

context

Per-request object shared by all resolvers

Auth (context.user), db, DataLoaders

info

AST / execution metadata

Advanced: look-ahead, field selection

Every resolver gets `(parent, args, context, info)` — `parent` chains results down, `context` carries per-request state
Each resolver is a function receiving four positional arguments. **`parent`** (sometimes called `root`/`source`) is the value returned by the *parent* field's resolver — this is how data chains down the tree: `User.posts` reads `parent.id` to find that specific user's posts. **`args`** holds the field's arguments. **`context`** is the per-request object (created by your [Apollo `context` function](/nodejs/apollo-server)) shared across every resolver in the request — the right home for the authenticated user, the database handle, and DataLoaders. **`info`** carries low-level AST details and is rarely needed. A resolver can return a value or a Promise; GraphQL awaits Promises automatically, so async resolvers just work.
The execution model — walking the tree

Text
Query:  { user(id: "42") { name  posts { title } } }

Execution walks the tree, calling a resolver per field:

   Query.user(id:"42")                 → returns user #42
      └─ User.name        (parent=user)  → "Ada"          (default resolver: parent.name)
      └─ User.posts       (parent=user)  → [post, post]   (your resolver: find by author)
            └─ Post.title (parent=post)  → "..."          (default: parent.title)

   • Children run only AFTER the parent resolves (parent feeds child).
   • Sibling fields (name, posts) resolve in PARALLEL.
   • A field's resolver runs ONLY if the client asked for that field.
GraphQL calls resolvers top-down: parents before children, siblings in parallel, skipped fields never run
Execution is a **tree walk**. GraphQL starts at the root field, resolves it, then passes that result as `parent` to each selected child field's resolver, recursing down to the scalar leaves. Two properties matter for performance. First, **sibling fields resolve in parallel** — `name` and `posts` on the same user run concurrently, so independent async work overlaps. Second, **a resolver runs only if its field was requested** — unasked fields cost nothing, which is the whole point of selective fetching. The flip side: a field deep in the tree that's expensive to resolve will be called *once per parent*, and when those parents are a list, you get the N+1 problem below.
Default resolvers — what you don't write
If you omit a resolver, GraphQL uses a default that reads the property of the same name off `parent`
You don't write a resolver for *every* field. When a field has no explicit resolver, GraphQL uses a **default resolver**: it looks for a property (or method) of the same name on the `parent` object and returns it. So if `Query.user` returns `{ id, name, email }`, the `User.name`, `User.email`, and `User.id` fields resolve automatically by reading those properties — no code needed. You only write explicit resolvers for fields that require *work*: fetching related data (`User.posts`), computing a derived value (`User.fullName` from first + last), reshaping, or calling another service. This is why a resolver map is usually small relative to the schema: most fields are plain property reads handled by the default.
The N+1 problem — the classic GraphQL trap

JS
// Query: { posts { title  author { name } } }
const resolvers = {
  Query: {
    posts: () => db.posts.findAll(),          // 1 query → 100 posts
  },
  Post: {
    // This runs ONCE PER POST — 100 posts ⇒ 100 separate author queries:
    author: (post) => db.users.findById(post.authorId),   // ❌ N+1
  },
}
-- The database sees:
SELECT * FROM posts;                          -- 1 query
SELECT * FROM users WHERE id = 1;             -- + N queries
SELECT * FROM users WHERE id = 2;             --   one per post...
SELECT * FROM users WHERE id = 3;             --   ...100 round-trips
...                                           -- = 1 + N = catastrophic
Resolving a field on a list runs its resolver once per item — the N+1 query problem that murders GraphQL performance
This is *the* GraphQL performance pitfall. Because a child resolver runs **once per parent**, asking for `author` on a list of 100 posts fires the `Post.author` resolver 100 times — each doing its own `findById` — for **1 + N = 101** database round-trips where one or two would do. It's invisible in development with 3 rows and devastating in production with thousands. It arises naturally from the tree-walking execution model and *will* appear in any non-trivial schema. The fix is **batching**: instead of N individual lookups, collect all the requested ids during a tick and issue *one* query (`WHERE id IN (...)`). You don't hand-roll this — you use **DataLoader**.
DataLoader — batch and cache per request

JS
import DataLoader from 'dataloader'

// The batch function receives ALL ids collected during one tick, returns
// results in the SAME ORDER as the keys:
function createLoaders(db) {
  return {
    userById: new DataLoader(async (ids) => {
      const users = await db.users.findByIds(ids)           // ONE query: WHERE id IN (...)
      const map = new Map(users.map((u) => [String(u.id), u]))
      return ids.map((id) => map.get(String(id)) ?? null)   // align to key order
    }),
  }
}

// Create loaders PER REQUEST in the context function (never share across requests):
const context = async ({ req }) => ({ user: await getUser(req), loaders: createLoaders(db) })

const resolvers = {
  Post: {
    // .load() collects ids this tick; DataLoader fires ONE batched query:
    author: (post, _args, { loaders }) => loaders.userById.load(post.authorId),  // ✅
  },
}
-- Now the database sees just:
SELECT * FROM posts;
SELECT * FROM users WHERE id IN (1, 2, 3, ...);   -- ONE batched query → 2 total
DataLoader coalesces per-tick `.load()` calls into one batched query and caches by key — create it fresh per request
**DataLoader** (from Facebook) solves N+1 with two mechanisms. **Batching**: instead of querying immediately, `.load(id)` records the id and waits until the end of the current event-loop tick, then calls your batch function *once* with the full array of ids — you turn that into a single `WHERE id IN (...)` query and return results *in key order*. All 100 `Post.author` calls collapse into one query. **Per-request caching**: within a request, `.load(5)` called twice returns the same cached result, so the same entity is never fetched twice. The critical rule: **create DataLoaders fresh in the per-request [context](/nodejs/apollo-server)**, never as module-level singletons — a shared loader would leak one user's cached data into another user's request and never see updates. New loaders per request = correct isolation plus the batching win.
Resolver best practices
  • Keep resolvers thin — delegate to a service/model layer (context.db, userService); a resolver should orchestrate, not contain business logic.

  • Authorize inside resolvers using context.user — check permissions on the fields and mutations that need them; there is no per-route middleware to lean on.

  • Batch every list→related-entity edge with DataLoader — assume N+1 exists until you have proven it batched.

  • Return Promises freely — GraphQL awaits them, and sibling fields resolve in parallel, so independent async work overlaps for free.

  • Throw GraphQLError with an extensions.code for expected failures (not-found, unauthorized) so clients can branch on the error type.

  • Use info sparingly — look-ahead (detecting which sub-fields were requested) is powerful for optimizing queries but adds complexity; reach for it only when measured.

Next
Step back and weigh the paradigms: [GraphQL vs REST](/nodejs/graphql-vs-rest).