awesome-everything RU
↑ Back to the climb

APIs

GraphQL N+1: code reading

Crux Read real GraphQL resolver and DataLoader code, predict the N+1 or correctness behaviour, and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

N+1 problems are diagnosed in resolver code, batch functions, and the validation layer — not in prose. Read each snippet, predict what it does under 50 posts or a hostile client, then choose the fix a senior engineer makes first.

Goal

Practise the loop you run on every GraphQL review: spot the per-field fetch, audit the batch contract, and confirm the query-shape gate — before the database log lights up in production.

Snippet 1 — the resolver that fires N+1

const resolvers = {
  Query: {
    posts: () => db.query('SELECT * FROM posts LIMIT 50'),
  },
  Post: {
    // called once per post, in isolation
    author: (post) => db.query(
      'SELECT * FROM users WHERE id = $1', [post.authorId]
    ),
  },
};
Quiz

For posts { title author { name } } over 50 posts, how many DB queries fire, and what is the structural fix?

Snippet 2 — the batch function

async function batchAuthors(ids) {
  const rows = await db.query(
    'SELECT id, name FROM users WHERE id = ANY($1)', [ids]
  );
  // returns rows in whatever order Postgres chose
  return rows;
}
Quiz

This batch function returns the raw rows. What is the latent bug, and how do you fix it?

Snippet 3 — the one-to-many loader

async function batchTags(postIds) {
  const rows = await db.query(
    'SELECT post_id, tag FROM tags WHERE post_id = ANY($1)', [postIds]
  );
  const map = new Map();
  rows.forEach(r => {
    if (!map.has(r.post_id)) map.set(r.post_id, []);
    map.get(r.post_id).push(r.tag);
  });
  return postIds.map(id => map.get(id));   // posts with no tags?
}
Quiz

A post with zero tags has no rows, so map.get(id) is undefined. What goes wrong, and what is the fix?

Snippet 4 — the validation gate

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(10)],   // only depth is capped
});
Quiz

This server caps depth at 10 but nothing else. A client sends a 5-level query with first: 100 at every level. What gets through, and what is the missing layer?

Recap

Every GraphQL N+1 incident is read in code: a per-field resolver that fetches one row per parent (51 queries → DataLoader), a batch function that trusts database row order (corruption → Map by key), a one-to-many loader that drops empty arrays (validation error → default to []), and a validation layer that caps depth but not row expansion (100^5 leak → multiplicative complexity plus alias caps). Read the resolver, audit the batch contract, confirm the gate — then re-trace resolver counts to verify the fix held.

Continue the climb ↑GraphQL N+1: batch and harden an API
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.