Skip to content
Backend & systems

REST vs GraphQL vs gRPC: An Interview-Ready Decision Guide

“REST, GraphQL, or gRPC?” is a system design question disguised as a technology quiz. Each style optimizes for a different consumer: REST for broad HTTP interoperability and caching, GraphQL for many clients that need differently shaped data, and gRPC for typed service-to-service calls. This guide compares them on contracts, transport, caching, N+1 queries, streaming, browser support, and errors, runs a small resolver-batching demo, and avoids benchmark numbers that would not survive contact with your own payloads.

By Published

8 min readEditorial analysisUpdated
  • REST
  • GraphQL
  • gRPC
  • API design
  • system design
What to remember

Choose by consumer and constraint, not fashion: public or cache-heavy resource APIs lean REST, many UI clients with varied data needs lean GraphQL, and internal polyglot services with streaming or strict contracts lean gRPC. Then name the cost you accept, such as caching work for GraphQL or a proxy for gRPC in browsers.

Start with the consumer, then compare

Before naming a protocol, ask four questions. Who calls the API: browsers, mobile apps, partners, or your own services? How varied are the data shapes those clients need? Do responses benefit from HTTP caches and CDNs? Do you need streaming in one or both directions? The answers usually decide more than raw speed does, and they give the interviewer reasoning they can evaluate rather than a preference.

REST models resources and leans on HTTP semantics from RFC 9110: methods, status codes, and caching headers. GraphQL exposes one typed schema and lets clients select fields per request. gRPC defines services and messages in Protocol Buffers and generates clients and servers from them. The table summarizes the defaults; each cell has exceptions you can mention once the core trade-off is clear.

Defaults that drive the decision
DimensionRESTGraphQLgRPC
ContractOptional, often OpenAPIRequired typed schemaRequired .proto file
TransportHTTP methods and URLsUsually POST to one endpointHTTP/2 POST, Protobuf
HTTP cachingNatural for GETNeeds GET or persisted queriesNot via HTTP caches
StreamingSSE or chunked responsesSubscriptions, transport variesServer, client, bidirectional
BrowsersNativeNativegRPC-Web through a proxy
ErrorsHTTP status codeserrors array, often with 200grpc-status in trailers

Contracts, schemas, and versioning

A REST API has no mandatory schema; teams usually add OpenAPI so clients, docs, and tests share one description. Versioning tends to happen in the URL or a header, or by evolving additively and never removing fields that clients rely on. GraphQL makes the schema the contract: every field has a type, clients can introspect it, and the specification's @deprecated directive lets you retire fields gradually, so many teams evolve one schema instead of shipping versions.

gRPC's contract is the .proto file, and field numbers are the wire format. The Protobuf best-practices page says never to re-use a tag number and to reserve the numbers of deleted fields, because old binaries elsewhere may still send or expect them. Adding a field with a new number is safe; changing a field's type almost never is. Mentioning this rule shows an interviewer that you know binary contracts break differently from JSON ones.

Transport and speed without invented benchmarks

gRPC is often called faster, and there are real mechanisms behind the claim. The Protobuf overview describes the format as like JSON but smaller and faster, and gRPC carries length-prefixed binary messages over HTTP/2 with status in trailers, according to its protocol specification. Generated stubs also remove hand-written parsing. None of that is a number, though, and HTTP/2 multiplexing is equally available to REST and GraphQL when they are served over HTTP/2.

In practice, latency is often dominated by round trips, payload size, server-side fan-out, and caching rather than by serialization. A REST screen that needs four sequential calls can lose to one GraphQL query; a GraphQL query that triggers hundreds of database calls can lose to a purpose-built REST endpoint; a CDN cache hit can beat all three. If performance decides the choice, benchmark your own payloads and state the conditions you measured.

Caching: where REST wins by default

A REST GET for /cards/42 has a stable URL, so browsers, CDNs, and reverse proxies can store it using Cache-Control and revalidate it with ETag, as RFC 9111 describes. Most GraphQL traffic is a POST to /graphql, and RFC 9110 states that a POST request cannot be satisfied by a cached POST response. The GraphQL over HTTP guidance allows GET for queries, and persisted or trusted documents replace long query text with a short ID so those URLs stay cacheable.

GraphQL clients compensate with normalized client-side caches keyed by a globally unique id field, which graphql.org recommends exposing for that purpose. That works well inside one app but does not help a CDN. gRPC calls are POSTs over HTTP/2 and are not cached by ordinary HTTP intermediaries, so caching happens in the application or a dedicated cache layer. If cacheable public reads dominate your traffic, that is a strong argument for REST.

Over-fetching, under-fetching, and N+1 resolvers

Fixed REST responses can over-fetch fields a screen ignores, or under-fetch and force extra round trips. GraphQL fixes the client side by letting one query select exactly the fields it needs, but it moves the fan-out to the server. Each field has a resolver, so a list of posts with an author field can call the author lookup once per post. The script resolves posts and their authors for three posts written by two authors, first with a naive resolver and then with a batched one.

Running node nplus1.mjs on Node 26 prints naive Ada,Linus,Ada -> 4 queries, then batched Ada,Linus,Ada -> 2 queries, then SELECT * FROM authors WHERE id IN (7,8). The batched resolver collects ids during one tick and issues a single IN query, which is the idea behind the DataLoader library. Add per-request batching, query depth or cost limits, and timeouts before exposing a public GraphQL endpoint; the specification does not provide them for you.

const posts = [{ id: 1, authorId: 7 }, { id: 2, authorId: 8 }, { id: 3, authorId: 7 }];
const authors = new Map([[7, "Ada"], [8, "Linus"]]);
let queries = [];
const db = {
  posts: async () => (queries.push("SELECT * FROM posts"), posts),
  authorsByIds: async (ids) => (queries.push(`SELECT * FROM authors WHERE id IN (${ids})`),
    ids.map((id) => authors.get(id))),
};
// Naive field resolver: one author query per post
const naiveAuthor = (post) => db.authorsByIds([post.authorId]).then(([a]) => a);
// Batched resolver: collect ids for one tick, then send one query (the DataLoader idea)
let pending = null;
function batchedAuthor(post) {
  if (!pending) {
    const batch = (pending = { ids: new Set() });
    batch.result = Promise.resolve().then(() => {
      pending = null;
      const ids = [...batch.ids];
      return db.authorsByIds(ids).then((rows) => new Map(ids.map((id, i) => [id, rows[i]])));
    });
  }
  pending.ids.add(post.authorId);
  return pending.result.then((byId) => byId.get(post.authorId));
}
for (const [label, resolve] of [["naive", naiveAuthor], ["batched", batchedAuthor]]) {
  queries = [];
  const names = await Promise.all((await db.posts()).map(resolve));
  console.log(label, names.join(","), "->", queries.length, "queries");
}
console.log(queries.at(-1));

Streaming, browsers, errors, and where WebSocket fits

gRPC defines four method kinds, unary, server streaming, client streaming, and bidirectional streaming, with deadlines and cancellation built in. Browser code cannot use native gRPC directly, so gRPC-Web goes through a proxy, Envoy by default, and its README lists only unary and server streaming as supported. Error models differ too: gRPC returns a status such as NOT_FOUND or DEADLINE_EXCEEDED in trailers, REST uses HTTP status codes, and GraphQL can return partial data together with an errors array.

That partial-result model is why GraphQL often answers 200 even when a field failed; the GraphQL over HTTP guidance recommends a 2xx status whenever data is present. WebSocket and Server-Sent Events are transports rather than API styles. SSE, defined in the HTML Standard, streams server-to-client events over ordinary HTTP and reconnects automatically; WebSocket, RFC 6455, opens a full-duplex channel after an HTTP upgrade. Many systems pair REST or GraphQL for commands with SSE or WebSocket for live updates.

How to answer in an interview

A strong answer names the consumer, picks a default, and states its cost: a public catalog API as REST with CDN caching; a product whose web, iOS, and Android clients need different views as GraphQL behind a batching layer and cost limits; internal services exchanging high-volume typed messages as gRPC with deadlines. Mixed architectures are normal, such as GraphQL or REST at the edge and gRPC between services.

Common mistakes: calling GraphQL faster by definition, saying gRPC cannot run in browsers at all, treating REST as CRUD over JSON without HTTP semantics, and quoting benchmark numbers without conditions. When you pick one for a scenario, also say what evidence would change your mind, such as a measured cache hit rate or a client team that needs streaming. The articles below go deeper on HTTP semantics, GraphQL operations, and safe retries.

Quick answers

Frequently asked questions

Is gRPC faster than REST?

Often for internal calls, thanks to compact Protobuf encoding, generated code, and long-lived HTTP/2 connections, but not by a fixed factor. Round trips, payload design, server work, and caching can matter more. Treat any published number as valid only for its payloads and setup, and measure your own.

Can REST and GraphQL be used together?

Yes. A common pattern keeps REST for public, cacheable resources, webhooks, and file uploads, and adds a GraphQL layer for product UIs that compose data from several services. The cost is two contracts to secure, monitor, and document, so the second style should solve a concrete client problem.

Why does my GraphQL API return 200 with errors?

GraphQL can return partial results: some fields resolve while others fail, and HTTP has no partial-success status. The GraphQL over HTTP guidance therefore recommends a 2xx status whenever the response contains data, with failures listed in the errors array. Monitoring must inspect that array, not only status codes.

Do I need gRPC for microservices?

No. REST with JSON works well for many internal services. gRPC earns its tooling cost when you want strict generated contracts across several languages, streaming, built-in deadlines, or very high call volumes. Without those needs, the extra build step and the browser proxy are costs with little return.

Source notes

References and review policy

Information checked on November 7, 2026. Section links identify sources for factual claims and technical explanations. Interpretations, practice scenarios and preparation recommendations are RecallDeck’s editorial work.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going