Skip to the solution
MediumSystem Design12 min read

What is the difference between REST and GraphQL?

1.1k views

The problem

Question presented to candidate: "Our mobile app makes six or seven REST calls to draw its profile screen, and someone has proposed moving to GraphQL. Explain how REST and GraphQL differ, what GraphQL would actually fix for us, and what new problems it would bring."

What a strong answer should cover:

  • REST models resources at URLs with HTTP methods and status codes; GraphQL exposes one typed schema at one endpoint and the client sends a query naming exactly the fields it wants
  • What GraphQL fixes: over-fetching, under-fetching and extra round trips, one schema as a contract across many clients; quantified with a worked example
  • What it costs: HTTP caching by URL no longer works by default (POST to one URL), so you need persisted queries or client caches; errors arrive inside a 200 response
  • Server-side risks: the N+1 problem in resolvers (fixed with batching such as DataLoader), expensive or deeply nested queries (depth limits, query cost analysis, rate limits by cost)
  • When each fits: REST for public, cacheable, simple resource APIs and file transfer; GraphQL for many clients with different data needs over a rich, connected domain; both can live behind the same gateway

Code / implementation expected: Optional โ€” sketching a query and its resolvers helps; the example counts requests, bytes and resolver calls for one screen.

  • api
  • rest

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read ยท 11 sections

Read the code

One screen via REST vs GraphQL: requests, bytes, and N+1 resolvers vs batching
// One screen fetched two ways: REST endpoints vs a tiny GraphQL engine.
// Data lives in arrays (standing in for a database); "requests" and "db calls"
// are counted, not sent. The GraphQL part parses a real query string.

const USERS = [
  { id: 1, name: "Asha", email: "asha@example.com", bio: "Writes about databases and distributed systems.", joined: "2021-04-02" },
  { id: 2, name: "Ben", email: "ben@example.com", bio: "Frontend engineer.", joined: "2022-01-15" },
  { id: 3, name: "Chen", email: "chen@example.com", bio: "SRE, coffee, bikes.", joined: "2020-09-30" },
];
const POSTS = [
  { id: 10, authorId: 1, title: "Indexes 101", body: "A B-tree keeps keys sorted so a lookup touches few pages. ".repeat(4), tags: ["db"] },
  { id: 11, authorId: 1, title: "Replication lag", body: "Asynchronous replicas trail the primary by some delay. ".repeat(4), tags: ["db"] },
  { id: 12, authorId: 1, title: "Why queues", body: "A queue absorbs bursts so producers and consumers decouple. ".repeat(4), tags: ["arch"] },
  { id: 13, authorId: 2, title: "CSS grid", body: "Grid lays out two dimensions. ".repeat(4), tags: ["css"] },
];
const COMMENTS = [
  { id: 100, postId: 10, authorId: 2, text: "Great intro" }, { id: 101, postId: 10, authorId: 3, text: "What about LSM trees?" },
  { id: 102, postId: 11, authorId: 3, text: "We hit this in prod" }, { id: 103, postId: 12, authorId: 2, text: "Kafka or SQS?" },
  { id: 104, postId: 12, authorId: 3, text: "Backpressure matters" },
];

// ---- REST: fixed resources, the client stitches them together ----
const rest = {
  "/users/1": () => USERS[0],
  "/users/1/posts": () => POSTS.filter((p) => p.authorId === 1),
};
let restCalls = 0, restBytes = 0;
function get(url) {
  restCalls++;
  const m = url.match(/^\/posts\/(\d+)\/comments$/), u = url.match(/^\/users\/(\d+)$/);
  const data = rest[url] ? rest[url]() : m ? COMMENTS.filter((c) => c.postId === Number(m[1])) : USERS.find((x) => x.id === Number(u[1]));
  restBytes += JSON.stringify(data).length;
  return data;
}
const user = get("/users/1");                               // round trip 1
const posts = get("/users/1/posts");                        // round trip 2
const comments = posts.map((p) => get(`/posts/${p.id}/comments`)); // round trip 3 (parallel)
const authorIds = [...new Set(comments.flat().map((c) => c.authorId))].sort((a, b) => a - b);
authorIds.forEach((id) => get(`/users/${id}`));              // round trip 4 (parallel)
console.log("REST");
console.log(`  requests: ${restCalls}, sequential round trips: 4, bytes received: ${restBytes}`);

// ---- GraphQL: one query, the server resolves the shape ----
const query = "{ user(id: 1) { name posts { title comments { text author { name } } } } }";
function parse(src) {
  const toks = src.match(/[A-Za-z_]\w*|\d+|[{}():]/g);
  let i = 0;
  function selection() {
    i++; // {
    const fields = [];
    while (toks[i] !== "}") {
      const f = { name: toks[i++], args: {}, sub: null };
      if (toks[i] === "(") { i++; while (toks[i] !== ")") { f.args[toks[i]] = Number(toks[i + 2]); i += 3; } i++; }
      if (toks[i] === "{") f.sub = selection();
      fields.push(f);
    }
    i++; // }
    return fields;
  }
  return selection();
}

let dbCalls = 0;
const resolvers = { // each takes the LIST of parents, so a batched engine calls it once per level
  user: (parents, args) => { dbCalls++; return parents.map(() => USERS.find((u) => u.id === args.id)); },
  posts: (parents) => { dbCalls++; return parents.map((u) => POSTS.filter((p) => p.authorId === u.id)); },
  comments: (parents) => { dbCalls++; return parents.map((p) => COMMENTS.filter((c) => c.postId === p.id)); },
  author: (parents) => { dbCalls++; return parents.map((c) => USERS.find((u) => u.id === c.authorId)); },
};
function execute(fields, parents, batched) {
  const out = parents.map(() => ({}));
  for (const f of fields) {
    let values;
    if (!resolvers[f.name]) values = parents.map((p) => p[f.name]);
    else if (batched) values = resolvers[f.name](parents, f.args);
    else values = parents.map((p) => resolvers[f.name]([p], f.args)[0]); // one call per parent: N+1
    if (f.sub) {
      const flat = values.flatMap((v) => (Array.isArray(v) ? v : [v]));
      const done = execute(f.sub, flat, batched);
      let k = 0;
      values = values.map((v) => (Array.isArray(v) ? v.map(() => done[k++]) : done[k++]));
    }
    out.forEach((o, j) => { o[f.name] = values[j]; });
  }
  return out;
}
const ast = parse(query);
for (const batched of [false, true]) {
  dbCalls = 0;
  const data = { data: execute(ast, [{}], batched)[0] };
  const body = JSON.stringify(data);
  console.log(`GraphQL, ${batched ? "batched resolvers (DataLoader style)" : "naive resolvers"}`);
  console.log(`  requests: 1, round trips: 1, bytes received: ${body.length}, db calls on server: ${dbCalls}`);
  if (batched) console.log(`  response: ${body}`);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 6 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

What is the CAP theorem and how does it drive consistency vs availability tradeoffs?

Continue