GraphQL Interview Questions: Core Concepts to Advanced Patterns

Reviewed by Mark Dickie · Last updated

14 questions on this page you can answer and see marked on the spot. Go to the first one

GraphQL is a query language for APIs and a runtime for executing those queries, developed by Meta and open-sourced in 2015, that lets clients ask for exactly the data they need — no more, no less. For an interview, you should be comfortable with the type system and schema-definition language (SDL), how resolvers work, and the tradeoffs between GraphQL and REST. Interviewers across all seniority levels test schema design and query execution; senior roles add questions on the N+1 problem, caching, and subscriptions. A solid answer to almost any GraphQL question starts with the schema, because the schema is the contract between client and server.

What does a GraphQL interview actually test?

At its core, a GraphQL interview checks whether you understand the data graph model rather than endpoint-based thinking. Most questions fall into four areas:

AreaWhat you need to know
Schema & Type SystemScalar types, object types, enums, interfaces, unions, non-null (!)
Queries & MutationsQuery structure, variables, fragments, aliases, inline fragments
ResolversThe resolver chain, context, info argument, how default resolvers work
Performance & ProductionN+1 problem, DataLoader batching, persisted queries, pagination patterns

Difficulty 1–2 questions test vocabulary and basic syntax. Difficulty 3 questions ask you to design a schema or trace a query through its resolver chain. Difficulty 4–5 questions cover DataLoader internals, subscriptions over WebSockets, schema stitching vs. federation, and security concerns like query depth limiting.

Why is the N+1 problem the question GraphQL interviewers love most?

The N+1 problem is what happens when a list resolver fetches N parent records and then each child resolver fires a separate database call for every one of them, giving you N+1 round trips instead of one. It comes up in almost every mid-to-senior GraphQL interview because it is the most common production mistake and because the fix — DataLoader — is specific to GraphQL's resolver model.

Knowing how to spot it and batch around it tells an interviewer more about your practical experience than any schema-syntax question.

How to prepare for a GraphQL interview in order of priority

  1. Read and write SDL by hand. Being able to define types, queries, and mutations without an IDE autocomplete shows you understand the structure, not just the tooling.
  2. Trace a query end-to-end. Know what happens from the moment a client sends a query string to the moment the JSON response is sent back, including parsing, validation, and resolver execution.
  3. Implement a resolver with context. Practice wiring up authentication via the context argument — interviewers often ask how you'd protect a field.
  4. Explain DataLoader in plain terms. You don't need to have written a custom loader, but you need to describe batching and caching clearly, ideally with a concrete before/after example.
  5. Compare GraphQL to REST honestly. Interviewers respect candidates who name real tradeoffs (caching complexity, tooling overhead, file uploads) rather than just listing GraphQL's advantages.

Key facts

  • Tarmac has 98 GraphQL interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
  • Tarmac tracked 160 job postings asking for GraphQL in September 2026.
  • Roles asking for GraphQL advertise a median base salary of £81,000, across 14 job postings as of September 2026.
  • Tarmac last reviewed these GraphQL interview questions on 13 October 2026.

At a glance

Questions25 shown · 98 in the bank
Difficulty1–5 of 5
FormatsMultiple choice, Flashcard, Code output, Find the bug, Multiple answer, True / false, Ordering, Short answer, Fill in the blank

What you'll review

  1. types fields
  2. n plus one dataloader
  3. resolver chain
  4. caching persisted queries
  5. mutations subscriptions
  6. overfetching underfetching
  7. error handling
  8. transport
  9. fragments
  10. variables

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

In GraphQL's type system, what is a scalar type (String, Int, Boolean, ID, Float)?#

Options

Show answer

A GraphQL scalar type (String, Int, Boolean, ID, Float, or a custom one like DateTime) is a leaf type: it resolves to a concrete value with no sub-fields to select beneath it, unlike an object type where you must specify which fields you want. Scalars can be used both as return types and as input argument types.

Why:

Scalars are the leaves of a GraphQL query — once a field resolves to a scalar, there's nothing further to select underneath it (unlike an object type, where you must specify which of its sub-fields you want). GraphQL ships five built-in scalars (Int, Float, String, Boolean, ID) and schemas commonly define custom ones (DateTime, EmailAddress) with their own serialize/parse logic. Scalars are usable both as return types and as input argument types — they're not restricted to one side.

What is the N+1 query problem in the context of GraphQL field resolvers?#

Show answer

A single top-level query triggers one database call to fetch the parent list, then the resolver for the related field fires once per parent row—issuing N additional queries instead of one batched join.

Why:

GraphQL resolves each field independently. When a list of N items each resolve a related child field, the default per-resolver execution issues one query per item, producing 1 + N database round-trips.

The Query.product resolver returns null. The Product.price resolver tries to access parent.cost. What is the result of executing the query { product { price } }?#

const resolvers = {
  Query: {
    product: () => null,
  },
  Product: {
    price: (parent) => parent.cost,
  },
};

/*
  schema:
    type Query { product: Product }
    type Product { price: Float }

  query executed:
    { product { price } }
*/

Options

Show answer
null
Why:

When a parent resolver returns null, GraphQL short-circuits and returns null for the entire field subtree. The Product.price resolver is never invoked, so no error occurs — the response for product is simply null.

The function below is intended to produce an Apollo-style persisted query extension for a GraphQL operation. A persisted query's sha256Hash must uniquely identify the query document so the server can look it up without the client resending the full query text. The function receives operationName, query (the GraphQL document string), and variables. Which line contains the bug?#

const crypto = require('crypto');

function buildPersistedQuery(operationName, query, variables) {
  const hash = crypto.createHash('sha256')
    .update(JSON.stringify(variables))
    .digest('hex');

  return {
    extensions: {
      persistedQuery: {
        sha256Hash: hash,
        version: 1
      }
    }
  };
}
Show answer

The bug is on line 5.

Why:

Line 5 hashes JSON.stringify(variables) instead of the query document string. A persisted-query hash must be derived from the query text so that the same operation always yields the same hash regardless of the variables passed in. Hashing the variables means the hash changes on every call with different inputs, the server can never match it to a stored query, and the purpose of persisted queries is defeated. The correct line would be .update(query).

What is the conventional semantic difference between the Query and Mutation root types?#

Options

Show answer

Query fields are meant to be side-effect-free reads, which a GraphQL executor is free to resolve in parallel. Mutation fields perform writes, and the spec requires the executor to run a document's top-level mutation fields serially, one at a time in the order listed, precisely because unordered writes with side effects would be unpredictable. This ordering guarantee is real, not just a naming convention.

Why:

The spec treats the two root types differently at execution time: fields under Query (and the sibling fields of a nested selection) may be resolved in parallel since reads shouldn't interfere with each other, while top-level fields under Mutation are executed serially, one completing before the next starts, precisely because mutations have side effects and unordered writes would be unpredictable. This is a load-bearing distinction, not just naming — if you send a mutation document with three top-level mutation fields expecting them to apply in order, that ordering is actually guaranteed by the spec.

Which of these are among the three root operation types defined by the GraphQL spec? Select all that apply.#

Options

Pick every one that applies.

Show answer
  • Query
  • Mutation
  • Subscription
Why:

The GraphQL spec defines exactly three root operation types a schema can expose: Query for reads, Mutation for writes, and Subscription for a persistent connection streaming updates over time (typically over WebSockets or SSE). Fragment and Resolver are real, load-bearing GraphQL concepts, but neither is a root operation type — a fragment is a reusable named selection set you spread into a query, and a resolver is the server-side function that produces a field's value, not something the client selects.

One of GraphQL's core design goals was letting a client fetch exactly the fields it needs for a screen in a single round trip, rather than either overfetching a fixed REST shape or making multiple REST calls.#

Options

Show answer

True. GraphQL was designed so a client could request exactly the fields it needs for a screen in one round trip, instead of a REST API's fixed response shape either overfetching (returning unused fields) or underfetching (forcing multiple calls to assemble the needed data).

Why:

True. This is the founding motivation behind GraphQL's client-specified field selection: instead of a server dictating a fixed response shape per endpoint (forcing overfetching when it returns more than needed, or underfetching when it returns less, requiring extra round trips), the client describes exactly the shape of data it wants in one query and the server assembles precisely that in a single response.

What is a "resolver" in GraphQL?#

Show answer

The server-side function responsible for producing the value of one field. Every field in the schema has (or inherits, from a default resolver) a function that receives the parent object, the field's arguments, and shared context, and returns that field's value — synchronously or as a Promise. A query is executed by walking the selection set and calling each selected field's resolver in turn.

Why:

Resolvers are where a GraphQL schema meets real data — the schema is only the contract; resolvers are the implementation that fulfils it, one field at a time.

Consider a GraphQL query { users { posts { title } } } where N users are returned and each user's posts field is resolved through a DataLoader instance. Place the following steps in the order they occur during a single query execution that correctly avoids the N+1 problem.#

Put these in order

Show answer

The correct order is: the users resolver returns N users, GraphQL calls User.posts N times, each call queues a key via postLoader.load() and returns a Promise, DataLoader dispatches all keys in one batch, the batch function issues a single data-source request returning results in key order, and each Promise resolves with its mapped result. This batching turns N+1 queries into exactly 2.

Why:

The N+1 problem arises because a parent resolver returns N items and a child field resolver fires once per item. DataLoader fixes this by not hitting the data source on each load() call — it queues the key and returns a Promise. After the current execution tick, DataLoader dispatches all queued keys together in one batch, the batch function issues a single data-source request and returns results in key order, and each individual Promise resolves with its mapped result so GraphQL continues building the response.

A GraphQL resolver makes three calls to the same DataLoader instance during one execution tick: loader.load(1), loader.load(2), and loader.load(3). None of these keys are already cached. Place the following DataLoader internal events in the order they occur.#

Put these in order

Show answer

The correct order is: each load() call queues its key and returns an unresolved Promise, the scheduler triggers a dispatch after the tick, the batch function receives all keys [1, 2, 3] and makes a single fetch, results are cached in DataLoader's internal Map, and each Promise resolves with its value. This batching-and-caching cycle is what collapses N round-trips into one.

Why:

DataLoader's load() method never fetches immediately: it enqueues the key and returns a Promise. The actual fetch is deferred until the scheduler fires after the current tick. At dispatch the batch function receives all queued keys at once and makes one request. Results are then written to the in-memory cache so subsequent loads for the same key are served from cache, and finally each pending Promise resolves with its value.

According to the GraphQL response spec, which of the following are valid top-level keys that MAY appear in a standard GraphQL operation response?#

Options

Pick every one that applies.

Show answer

A standard GraphQL response may contain three top-level keys: data (the execution result), errors (a list of error objects), and extensions (implementation-defined metadata). Keys like meta or status are not defined by the GraphQL spec.

Why:

The GraphQL spec defines three valid top-level response keys: data (the result of execution, possibly null), errors (a list of field or request errors), and extensions (an optional map of implementation-defined metadata). meta and status are not part of the GraphQL specification.

A posts query returns 50 posts, and each post's author field is resolved with its own separate database lookup — 50 extra queries on top of the 1 that loaded the posts. What is this problem called, and what's the standard fix?#

Options

Show answer

Resolving each post's author with its own database lookup is the GraphQL N+1 problem: 1 query to load the list plus N more, one per item, for a related field resolved independently. The fix is batching — collect every id requested during one execution tick and issue a single WHERE id IN (...) query for all of them, then hand each resolver its row from that batch. DataLoader is the standard implementation of this pattern, request-scoping both the batching and a de-dup cache.

Why:

This is the canonical GraphQL N+1: 1 query for the list plus N queries for a per-item related field, and it appears anywhere a resolver naively fetches its own data instead of coordinating with its siblings. The fix isn't caching or indexing (those help latency per-query, not query count) — it's batching: collect all the author ids requested during one execution tick and issue a single WHERE id IN (...) query for all of them, then hand each resolver its own row from the batched result. DataLoader (and equivalents in other languages) is the standard pattern: it request-scopes both the batching and a de-dup cache so the same id is never fetched twice in one request.

Which statements correctly describe overfetching and underfetching, the two REST problems GraphQL's field-selection model was designed to fix? Select all that apply.#

Options

Pick every one that applies.

Show answer
  • Overfetching is a fixed-shape REST endpoint returning fields the client doesn't need for a given screen
  • Underfetching is needing multiple REST round-trips to assemble the data one screen needs
  • GraphQL lets a client request exactly the fields it needs in a single round trip, addressing both problems at once
Why:

Overfetching and underfetching are the two REST pain points GraphQL's design directly targets. A fixed REST response shape either returns more than a given screen needs (overfetching — wasted bandwidth) or forces multiple endpoint calls to assemble what one screen needs (underfetching — wasted round-trips). GraphQL's single endpoint plus client-specified field selection lets one request return exactly the shape needed, addressing both at once. PATCH is about partial writes, unrelated to underfetching on reads; and overfetching is a REST problem GraphQL was built to avoid, not something unique to GraphQL.

Most GraphQL-over-HTTP server implementations return HTTP 200 even when the response's errors array contains one or more failures, so the HTTP status code alone can't be used to detect a GraphQL-level error.#

Options

Show answer

True. GraphQL's spec doesn't mandate a specific HTTP status for field errors, and most server implementations return HTTP 200 even when the response's errors array is non-empty — reserving other statuses for transport-level failures that happen before execution starts. A client must inspect the errors array in the body, not the status code, to detect a GraphQL-level failure.

Why:

True. The GraphQL spec itself is transport-agnostic and doesn't mandate an HTTP status code for field errors; in practice, the overwhelming majority of servers (Apollo Server among them) return HTTP 200 for a request that executed and produced a partial response with populated errors, reserving non-200 statuses for transport-level failures like a malformed request or auth rejection before execution even starts. A client has to inspect the response body's errors array, not the status code, to know whether a GraphQL-level failure occurred.

What is a GraphQL fragment, and why would you use one instead of just repeating a field selection?#

Show answer

A fragment is a named, reusable selection set on a given type — you define it once (fragment PostCard on Post { id title author { name } }) and spread it into any query or mutation with ...PostCard wherever a Post is selected. The main reasons to use one: avoiding duplicating the same field list across multiple queries that all need the same shape (e.g. a list view and a detail view both showing 'post card' data), and — in client frameworks like Apollo/Relay — colocating the exact fields a specific UI component needs next to that component, so the component's data dependency is explicit and the top-level query can compose fragments from many components without anyone hand-merging field lists.

Why:

A fragment is GraphQL's mechanism for not repeating yourself: name a selection set once against a type, then spread it (...FragmentName) into as many operations as need that exact shape. Beyond avoiding literal duplication, fragments are the basis of the 'colocate data requirements with the component that needs them' pattern used by client libraries like Apollo and Relay — each UI component declares its own fragment, and a parent query composes the fragments of everything it renders without needing to know their internal field lists.

Why use GraphQL variables (query($id: ID!) { post(id: $id) { title } }) instead of string-interpolating values directly into the query text?#

Show answer

Variables keep the query document itself static and cacheable — a client (or a persisted-query system) can reuse the exact same parsed/validated query text across many calls, only the variables payload changes. Interpolating values into the query string produces a different document on every call, defeating query caching and persisted queries, and — like building SQL by string concatenation — risks injection-style bugs if a value isn't escaped correctly.

Why:

Variables separate the shape of a request (the query) from its data (the variables), which is exactly what makes a query reusable, cacheable, and safe from injection-style string-building bugs.

Order the steps a GraphQL server takes to turn an incoming request into a response.#

Put these in order

Show answer

A GraphQL server processes a request in a fixed order: parse the query text into an AST, validate that AST against the schema (rejecting unknown fields, types, or arguments before anything runs), execute by calling each selected field's resolver depth-first, then serialize the collected results into the final { data, errors } response. Validating before executing is what lets a malformed query fail fast with a clear error instead of partially executing.

Why:

This is the fixed pipeline every spec-compliant GraphQL server follows: parse the raw query text into a structured AST, validate that AST against the schema (catching typos, wrong argument types, or fields that don't exist before any resolver runs), execute by calling resolvers for each selected field — parent fields before their children, since a child resolver often needs its parent's already-resolved value — and finally serialize whatever was collected (plus any errors gathered along the way) into the response shape. Validation happening before execution is why a malformed query fails fast with a clear error instead of partially executing and returning confusing partial data.

In a GraphQL execution, if a resolver for a field declared as non-null (Int!) throws an error, the null value for that field propagates up the response tree to the nearest nullable ancestor field. That ancestor is also set to null, and fields above it in the tree remain intact with their resolved values.#

Options

Show answer

True. When a non-null field's resolver errors, GraphQL sets that field to null; since null violates a non-null type, the null bubbles up to the nearest nullable ancestor, which is set to null. Fields above that ancestor keep their resolved values, and the error is added to the errors array.

Why:

This is the GraphQL spec's null-bubbling rule. When a field error occurs, the field's value is set to null. If the field's type is non-null, that null is invalid, so the parent field is set to null instead. This continues upward until a nullable field is found, which absorbs the null. Fields above that nullable ancestor are unaffected and retain their resolved values. The error is also recorded in the errors array with a path indicating where it originated.

In Apollo Automatic Persisted Queries (APQ), the client first sends only a SHA-256 hash of the query document instead of the full query text. If the server has not yet stored that hash, it responds with the GraphQL error code _____, which signals the client to retry the same request with the complete query document included so the server can persist it for future lookups.#

Show answer

In Apollo Automatic Persisted Queries (APQ), the client first sends only a SHA-256 hash of the query document instead of the full query text. If the server has not yet stored that hash, it responds with the GraphQL error code **PERSISTED_QUERY_NOT_FOUND**, which signals the client to retry the same request with the complete query document included so the server can persist it for future lookups.

Why:

Apollo's APQ protocol works in two phases. On the first request the client sends only the extensions.persistedQuery.sha256Hash field and no query field. If the server's cache does not contain a mapping for that hash, it returns a GraphQL error whose extensions.code is PERSISTED_QUERY_NOT_FOUND. The client then re-sends the request with both the hash and the full query text; the server executes and stores the mapping so subsequent requests need only the hash. The companion error PERSISTED_QUERY_NOT_SUPPORTED is used when the server opts out of APQ entirely.

Apollo Client's InMemoryCache normalizes every response object into a flat, key-value store so that the same entity returned by different queries shares one cache entry. The default dataIdFromObject function builds each cache key by joining the object's __typename and its id field with a colon, producing the key format _____ (for example, User:42).#

Show answer

Apollo Client's InMemoryCache normalizes every response object into a flat, key-value store so that the same entity returned by different queries shares one cache entry. The default dataIdFromObject function builds each cache key by joining the object's __typename and its id field with a colon, producing the key format **__typename:id** (for example, User:42).

Why:

Apollo's defaultDataIdFromObject returns ${object.__typename}:${object.id} (falling back to _id when id is absent). This normalization key lets the cache deduplicate an entity — say, a User with id 42 — regardless of which query or mutation returned it. When a subsequent mutation or query returns the same __typename and id, the cache merges the fields into the existing entry and any component subscribed to that fragment re-renders with the updated data. You can override the key generation by passing a custom dataIdFromObject function to the InMemoryCache constructor.

This resolver for Post.comments works correctly but makes the API slow to a crawl whenever a query returns many posts. Which line is the root cause?#

1| const Post = {
2|   comments: async (post, args, ctx) => {
3|     return ctx.db.query(
4|       'SELECT * FROM comments WHERE post_id = $1',
5|       [post.id],
6|     );
7|   },
8| };

Options

Show answer

Line 3-6 — each Post.comments call issues its own database query, so a list of 50 posts fires 50 separate queries (the N+1 problem) instead of one batched query for all of them

Why:

Each post in the list independently triggers this resolver, and each invocation runs its own round trip to the database — for a page of 50 posts, that's 50 extra queries beyond the one that loaded the posts themselves. GraphQL resolvers being async is normal and required for any I/O; the parameterized query on line 4 is actually the correct, injection-safe way to write it. The fix is to replace the direct per-call ctx.db.query with a batching loader (e.g. a DataLoader keyed by post.id) that collects every id requested during one execution tick and issues a single WHERE post_id = ANY($1)-style query for all of them, then hands each resolver its own slice of the batched result.

Explain the N+1 problem as it shows up in GraphQL resolvers, and how DataLoader-style batching fixes it.#

Show answer

Each field in a GraphQL selection is resolved independently by its own resolver function, so a naive implementation of a per-item related field — say, Post.author — fires its own database lookup every time it's called. For a posts query returning 50 posts, that's 1 query for the list plus 50 more for their authors: N+1 queries where N is the list size. The fix is a batching + caching layer, most commonly DataLoader: instead of each resolver hitting the database immediately, it registers the id it needs with the loader; the loader waits until the current execution tick finishes collecting all the ids requested that tick, then issues one WHERE id IN (...) query for the whole batch and hands each resolver its row. It also caches within the request, so the same id is never fetched twice even if requested by unrelated fields in the same query.

Why:

The mechanism to get right: GraphQL resolves each field's resolver independently, so per-item related fields naturally fall into N+1 unless something coordinates across sibling resolvers within one execution tick. DataLoader's trick is exactly that coordination — it defers each individual load request to the end of the current tick (via a microtask), collects everything requested, and issues one batched query, plus a request-scoped cache so repeated requests for the same id are free. Understanding why naive resolvers produce N+1 (independent execution, no shared context) is what separates a real answer from reciting 'use DataLoader'.

Given the following GraphQL schema and JavaScript resolvers (apollo-server style), what is the JSON response for the query shown? Assume the server serializes the response as compact JSON (no extra whitespace).#

const typeDefs = `
  type Query { product: Product }
  type Product {
    id: ID!
    basePrice: Int!
    discount: Int!
    finalPrice: Int!
    reviews: [Review!]!
  }
  type Review {
    rating: Int!
    product: Product
  }
`;

const resolvers = {
  Query: {
    product: () => ({ id: "p1", basePrice: 100 })
  },
  Product: {
    discount: () => 20,
    finalPrice: (parent) => parent.basePrice - (parent.discount || 0),
    reviews: () => [{ rating: 5 }, { rating: 3 }]
  },
  Review: {
    product: () => ({ id: "p1", basePrice: 100 })
  }
};

// GraphQL query executed:
// {
//   product {
//     discount
//     finalPrice
//     reviews {
//       rating
//       product { finalPrice }
//     }
//   }
// }
Show answer
{"product":{"discount":20,"finalPrice":100,"reviews":[{"rating":5,"product":{"finalPrice":100}},{"rating":3,"product":{"finalPrice":100}}]}}
Why:

The Query.product resolver returns { id: "p1", basePrice: 100 } — note that discount is NOT a property of this object. The Product.discount field resolver returns 20, but that resolved value does NOT get written back onto the parent object; it only appears in the output. When Product.finalPrice runs, parent is still the raw { id: "p1", basePrice: 100 }, so parent.discount is undefined and (parent.discount || 0) evaluates to 0, giving 100 - 0 = 100. The same logic applies at every level: Review.product returns { id: "p1", basePrice: 100 } (again without discount), so finalPrice is 100 for the nested product too. The discount field itself resolves to 20 in the top-level output. This is the key resolver-chain insight: sibling field resolvers cannot see each other's resolved values — only what the parent resolver returned.

In the GraphQL execution algorithm, when a resolver for a field whose GraphQL type is Non-Null (e.g., String!) either throws an error or returns null, the execution engine propagates null upward. Explain the precise null-propagation rule: what determines how far the null bubbles up through the response tree, and under what specific condition does the entire data key of the GraphQL response get set to null (rather than just one field being nulled out)?#

Show answer

When a Non-Null field resolves to null or errors, the execution engine nulls out the parent field's value. If the parent field's type is also Non-Null, the null propagates up another level, and so on. The propagation stops at the first Nullable field (a field whose type is not wrapped in Non-Null) — that field becomes null and its siblings retain their resolved values. If the null bubbles all the way up to the root operation field and that root field is Non-Null (i.e., every ancestor including the top-level field is Non-Null), then there is no nullable ancestor to stop the propagation, so the entire data key of the response is set to null and the error is recorded in the errors array.

Why:

The spec's CompleteValue / HandleFieldError logic dictates that a null result for a Non-Null field invalidates the parent. Propagation continues upward through consecutive Non-Null parents until a Nullable ancestor absorbs it. If no nullable ancestor exists — meaning the root operation field itself is Non-Null — the whole data object is replaced with null. This is why Non-Null fields are sometimes called 'null propagation bombs': a single resolver failure deep in the tree can null out large portions of or the entire response.

An engineering team sets up Apollo Client with Automatic Persisted Queries (APQ) and a custom error-handling link. The server supports APQ but has not yet cached any query hashes. On the very first request, the client receives a PersistedQueryNotFound error and the operation fails instead of automatically retrying with the full query text. The link chain order is correct. Identify the single buggy line that causes the APQ retry mechanism to break.#

import { ApolloLink, Observable, HttpLink, createPersistedQueryLink } from '@apollo/client';
import { sha256 } from 'crypto-hash';

const errorLink = new ApolloLink((operation, forward) => {
  return new Observable(observer => {
    forward(operation).subscribe({
      next: response => {
        if (response.errors) {
          observer.error(new Error(response.errors.map(e => e.message).join(', ')));
        } else {
          observer.next(response);
        }
      },
      error: err => observer.error(err),
      complete: () => observer.complete(),
    });
  });
});

const persistedQueryLink = createPersistedQueryLink({ sha256 });
const httpLink = new HttpLink({ uri: '/graphql' });

export const link = ApolloLink.from([persistedQueryLink, errorLink, httpLink]);
Show answer

The bug is on line 9.

Why:

The bug is on line 9. The createPersistedQueryLink detects PersistedQueryNotFound (and PersistedQueryNotSupported) by inspecting response.errors inside the observable's next handler — it needs the server response to arrive as a next event so it can read the extensions.code. The errorLink, sitting between the PQ link and the HTTP link in the chain ApolloLink.from([persistedQueryLink, errorLink, httpLink]), intercepts the response from the HTTP link and, on line 9, converts every GraphQL-level error (including PersistedQueryNotFound) into a thrown error via observer.error(new Error(...)). Because the response never reaches the PQ link as a next event, the PQ link's error-detection logic never fires and it never retries with the full query text. The PQ link receives the error through its error handler, which simply re-throws it to the application. The fix is to pass observer.next(response) for GraphQL errors (letting the PQ link inspect them) and only throw for unexpected transport-level errors, or to place the error-handling link before the PQ link in the chain so it does not sit between the PQ link and its forward target. Lines 14–15 correctly forward transport errors and completion; line 23 has the correct link ordering; line 20 correctly creates the PQ link with a sha256 function.

Related interview questions

Job market

See graphql salaries and hiring demand from live job postings.

The other 73 questions

This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 73 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.