Networking
Intermediate 10 min read

REST vs GraphQL

In one line

REST exposes many resource URLs and uses HTTP methods and status codes for meaning, while GraphQL exposes one endpoint with a typed schema where the client asks for exactly the fields it needs.

What it is

REST is an architectural style (described by Roy Fielding in 2000) where every thing is a resource with its own URL, such as /users/42 or /users/42/orders. Clients use HTTP methods for the action (GET reads, POST creates, PUT or PATCH updates, DELETE removes) and the server answers with a status code and a representation, usually JSON, whose shape the server decides.

GraphQL is a query language and runtime created at Facebook in 2012 and open-sourced in 2015. The server publishes a typed schema, and clients send queries (reads), mutations (writes) or subscriptions (live updates) to a single endpoint, usually POST /graphql. Each field in the schema is backed by a resolver function, and the response mirrors the shape of the query.

The core trade-off: REST is simple and uses the web's built-in caching, status codes and tooling, while GraphQL removes over-fetching (getting fields you do not need) and under-fetching (needing several calls to build one screen) at the cost of more server complexity around performance, caching and security.

Why interviewers ask

API design is a standard part of backend and system design interviews. Interviewers expect you to explain over-fetching and under-fetching, the N+1 problem and DataLoader, why GraphQL is harder to cache, how each style handles versioning and errors, and to pick the right style (or gRPC) for a given client and team instead of following hype.

How it works

  1. 1REST: the client calls a resource URL with a method, for example GET /users/42. The server routes it to a handler, which returns a fixed JSON representation with a status code such as 200, 404 or 409, plus caching headers like Cache-Control and ETag.
  2. 2REST: related data needs either more calls (GET /users/42/orders, then GET /products/7 for each order) or server-side options such as Stripe's expand parameter or a purpose-built endpoint.
  3. 3GraphQL: the server defines a schema, for example type User { id: ID!, name: String!, orders(first: Int): [Order!]! }, with root Query and Mutation types and optionally a Subscription type.
  4. 4GraphQL: the client sends a query document naming the fields it wants, for example { user(id: 42) { name orders(first: 3) { total product { title } } } }. The server parses it, validates it against the schema, then executes it by calling the resolver for each field.
  5. 5GraphQL: resolvers run level by level. A naive orders.product resolver runs one database query per order (the N+1 problem); a DataLoader collects all product ids requested during one tick and fetches them in a single batched query, caching results for that request.
  6. 6GraphQL: the response is JSON shaped like the query, under a data key. Errors appear in an errors array, often alongside partial data, and the HTTP status is traditionally 200 even when a field failed.
  7. 7Mutations run their top-level fields one after another in order, while query fields may be resolved in parallel; subscriptions are usually delivered over a WebSocket.

Worked example · Loading a mobile order history screen

  1. The screen needs the user's name and their last 3 orders, each with its total and product title.
  2. REST: GET /users/42 returns 30 fields when the screen needs 1 (over-fetching). GET /users/42/orders?limit=3 returns the orders, then 3 calls to GET /products/{id} fetch titles (under-fetching). That is 5 requests, several of them sequential round trips on a slow mobile network.
  3. GraphQL: one POST /graphql with { user(id: 42) { name orders(first: 3) { total product { title } } } } returns exactly those fields in one round trip.
  4. On the server, a naive product resolver would run 1 query for the user, 1 for the orders and 3 for products. With DataLoader the 3 product lookups become one SELECT ... WHERE id IN (7, 9, 12), so 3 queries in total.
  5. If the product service is down, GraphQL can still return the name and order totals, with product set to null and an entry in errors, while the HTTP status stays 200. The client must check errors, not only the status code.
  6. Caching differs: the REST GET /products/7 can be cached by the browser and a CDN using Cache-Control. The GraphQL POST is not cached by HTTP caches, so the app relies on a client cache (Apollo Client or Relay normalise objects by id) or persisted queries sent as GET.

Picture it

REST is a restaurant with a fixed menu of set plates: each dish (endpoint) comes the way the kitchen prepares it, and if you want a starter, a main and a dessert you place three orders. GraphQL is a single counter where you hand over one list naming exactly what you want on your plate, and the kitchen (the resolvers) has to work out how to assemble it efficiently.

Key patterns

DataLoader batching

When: GraphQL resolvers that fetch related objects by id

How: Collect all keys requested in one tick, load them with one batched query (WHERE id IN ...), and cache results for the lifetime of a single request.

Persisted queries

When: Public or mobile GraphQL clients where caching and security matter

How: Register queries ahead of time and send only a hash; the server can reject unknown queries, and requests can be GETs that HTTP caches and CDNs can store.

Query depth and cost limits

When: Any GraphQL endpoint exposed to clients

How: Reject queries deeper than a set limit, compute a cost from fields and requested page sizes before executing, and rate-limit by cost rather than by request count.

Backend for frontend (BFF)

When: Several client types need differently shaped data from the same services

How: Put a GraphQL or tailored REST layer in front of internal services so each client gets the data for one screen in one call.

REST resource design

When: Public, partner or simple CRUD APIs

How: Plural nouns for collections, methods for actions, precise status codes, cursor pagination, ETag and Cache-Control headers, and an Idempotency-Key for POST.

Real-world use

GitHub REST v3 and GraphQL v4

GitHub offers both. Its REST API (v3) has an endpoint per resource, while its GraphQL API (v4, announced in 2016) lets one query fetch a repository, its pull requests and their reviews together. GitHub rate-limits GraphQL by a calculated point cost per query rather than by request count, and caps how many nodes one query can request.

Shopify

Shopify's Storefront API is GraphQL, and Shopify has made GraphQL the primary Admin API, marking the REST Admin API as legacy. Each query gets a calculated cost and is throttled with a leaky-bucket limit, a common way to stop expensive queries.

Stripe

Stripe is a well-known REST API: resource URLs such as /v1/customers and /v1/payment_intents, Idempotency-Key headers for safe retries, date-based versions pinned per account or sent in the Stripe-Version header, and an expand parameter to embed related objects instead of making extra calls.

gRPC for internal services

Many companies use gRPC between backend services: it sends Protocol Buffers over HTTP/2 with generated client code, strict contracts and streaming. Browsers cannot call it directly without a proxy layer such as gRPC-Web, so public APIs usually stay REST or GraphQL.

In a project

Your team runs a REST backend for a marketplace, and the new mobile app needs 6 to 8 sequential REST calls to render its home screen, which is slow on poor mobile networks. A partner integration also uses the public REST API.

  1. Keep the public REST API for partners: it is stable, cacheable, simple to document and easy to call from any language or with curl.
  2. Add a GraphQL layer (a backend for frontend) for the mobile and web apps, with resolvers calling the existing services, so the home screen becomes one query.
  3. Use DataLoader in every resolver that loads by id, and count database queries per request in tests to catch N+1 regressions.
  4. Protect the endpoint: maximum query depth, a cost or complexity limit per query, pagination with a maximum page size, timeouts, and introspection disabled in production since the API is private to your own apps.
  5. Use persisted queries: the app ships query hashes built at compile time, the server only accepts known hashes, and reads go over GET so a CDN can cache the public parts.
  6. Evolve the schema by adding fields and marking old ones @deprecated, tracking field usage before removing anything, instead of creating /v2 endpoints.

Impact: The home screen drops from about 7 round trips to 1 and loads noticeably faster on slow networks, mobile releases no longer wait for new backend endpoints, partners keep a stable REST API, and the limits stop a single deeply nested query from overloading the database.

Trade-offs

Use it when

  • REST: public and partner APIs, simple CRUD, and resources that benefit from HTTP and CDN caching
  • REST: file uploads and downloads, webhooks, and teams that want the simplest tooling
  • GraphQL: several clients (web, iOS, Android) needing different shapes of the same data
  • GraphQL: screens that combine data from many services or deeply related objects
  • gRPC: internal service-to-service calls needing low latency, strict contracts and streaming

Avoid it when

  • GraphQL for a small CRUD service with one client; resolvers, limits and tooling add work for little gain
  • GraphQL when heavy public HTTP or CDN caching is essential and you cannot use persisted queries
  • REST when mobile screens need many sequential calls or clients keep asking for new tailored endpoints
  • gRPC for APIs called directly from browsers or by third parties expecting JSON over HTTP

Cost: REST cost per screen grows with the number of round trips, each adding network latency. GraphQL makes it one round trip, but server cost depends on the query: without batching, a list of n items with a nested field costs 1 + n database queries, and without limits the work of a nested query can grow exponentially with its depth.

Common mistakes

  • Saying GraphQL is always faster: it saves round trips, but a single complex query can be far more expensive on the server than a simple REST call.
  • Forgetting the N+1 problem in resolvers; without DataLoader or batching, a list of 100 items with a nested field triggers 101 database queries.
  • Assuming a GraphQL 200 response means success; clients must check the errors array, and monitoring based only on HTTP status codes misses failures.
  • Exposing a GraphQL endpoint with no depth, cost or pagination limits, so one nested query can overload the database.
  • Thinking GraphQL is a database or replaces SQL; it is an API layer, and resolvers can read from any database, service or REST API.
  • Claiming GraphQL cannot be cached; HTTP caching is harder because queries go to one URL via POST, but client caches, persisted queries over GET and server-side caching all work.
  • Calling any JSON-over-HTTP API RESTful even when it uses POST for everything and ignores status codes.

Interview questions

Quick revision

  • REST: many URLs (resources), HTTP methods for actions, status codes for outcomes, server decides the response shape.
  • GraphQL: one endpoint, typed schema, client picks fields; query (read), mutation (write), subscription (live).
  • Resolvers produce each field; the N+1 problem is fixed with DataLoader (batch per tick, cache per request).
  • REST caches naturally with Cache-Control, ETag and CDNs; GraphQL uses client caches and persisted queries over GET.
  • REST versions with /v1 or headers; GraphQL evolves one schema with @deprecated.
  • GraphQL errors: errors array, possibly with partial data, HTTP status traditionally 200.
  • Secure GraphQL with depth limits, cost limits, pagination caps, timeouts and query allow-lists.
  • gRPC: Protocol Buffers over HTTP/2, generated code, streaming; best for internal services.
  • Examples: GitHub REST v3 and GraphQL v4, Shopify GraphQL Admin and Storefront APIs, Stripe REST.

Check yourself

1. A mobile screen makes 5 REST calls, and most responses contain fields it never uses. Which two problems does this show?

2. A GraphQL query lists 50 posts with each author, and the server runs 51 database queries. What is the standard fix?

3. A GraphQL response arrives with HTTP 200, some data and a non-empty errors array. What does this mean?

4. Why do browser and CDN caches work more naturally with REST than with typical GraphQL?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free