Part 5 · APIs & Communication

GraphQL vs REST: When to Use Which

Six REST calls to draw one profile screen, and most of the data thrown away. How GraphQL lets clients ask for exactly what they need in one request, what that costs the server — N+1, caching, expensive queries — and when to pick REST, GraphQL or gRPC.

Lesson 328 min readintermediate#api#graphql

On this page

  1. The Problem
  2. The Core Idea
  3. How It Works
  4. Trade-offs
  5. In the Real World
  6. Interview Questions
  7. Key Takeaways
  8. Further Reading

The Problem

Your mobile app's profile screen shows a user's name and photo, their last 3 orders with product thumbnails, and their loyalty points. A REST API gives one fixed answer for each URL. To fill this screen, the app must make several calls:

step 1 / 7
Mobile app
REST API
  1. GET /users/42
    40 fields, 2 needed
  2. GET /users/42/orders?limit=3
  3. GET /products/991
  4. GET /products/405
  5. GET /products/17
  6. GET /users/42/loyalty
  7. With GraphQL: one POST /graphql returning exactly the fields the screen draws
The profile screen over REST. Six round trips on a slow mobile network, most fields discarded. · sequence · step through

That is six round trips (six requests, each waiting for its answer) on a slow mobile network. Most of the data downloaded is thrown away. Meanwhile, the web team wants a different set of fields for the same screen. The backend team is tired of adding a new endpoint for every screen.

GraphQL was created to solve this. GraphQL is a query language for APIs. The client sends a query that lists the fields it wants, and the server returns only those fields. The client asks for exactly the data it needs, in one request.

The Core Idea

Think about ordering food. (A set meal is a fixed plate. A buffet lets you pick.)

REST is like set meals. "Meal #3" always comes with rice, dal, sabzi, roti and a sweet. If you only want dal and roti, you still get the whole plate. If you want dal from Meal #3 and paneer from Meal #5, you place two orders.

GraphQL is like a buffet counter where you write your exact order. "Dal, 2 rotis, and the paneer from the other counter, please." You get exactly that, on one plate, in one trip.

The kitchen (the server) needs to be smarter to handle any combination. This is the trade-off.

How It Works

The schema: a typed menu

A GraphQL server publishes a schema. A schema is a written description of every type of data and how the types connect, like a menu:

GraphQL
type User {  id: ID!  name: String!  avatarUrl: String  loyaltyPoints: Int!  orders(last: Int): [Order!]!}type Order {  id: ID!  total: Int!  createdAt: String!  items: [OrderItem!]!}type OrderItem {  quantity: Int!  product: Product!}type Product {  id: ID!  name: String!  thumbnailUrl: String}type Query {  user(id: ID!): User}type Mutation {  updateProfile(name: String, avatarUrl: String): User!}

(! means "never null", so the value is always there. [Order!]! means a list that is never null and has no null items. ID is a unique identifier.)

Queries: ask for exactly what you need

A query reads data. You send one request to a single endpoint (usually POST /graphql):

GraphQL
query ProfileScreen {  user(id: "42") {    name    avatarUrl    loyaltyPoints    orders(last: 3) {      total      items {        product { name thumbnailUrl }      }    }  }}

The response has exactly that shape:

JavaScript
{  "data": {    "user": {      "name": "Asha",      "avatarUrl": "…/42.jpg",      "loyaltyPoints": 1250,      "orders": [        { "total": 129900, "items": [ { "product": { "name": "Running shoes", "thumbnailUrl": "…" } } ] }      ]    }  }}

Six REST calls became one request, with no wasted fields.

Mutations and subscriptions

  • Mutations are operations that change data (like POST, PUT and DELETE in REST):
    GraphQL
    mutation { updateProfile(name: "Asha K") { id name } }
  • Subscriptions are long-lived requests that push new data to the client when something changes. They usually run over WebSockets (a two-way connection that stays open):
    GraphQL
    subscription { orderStatusChanged(orderId: "5521") { status } }

Resolvers: how the server fills in the data

On the server, each field has a resolver. A resolver is a function that knows how to get the value of that one field:

JavaScript
const resolvers = {  Query: {    user: (_, { id }) => db.users.findById(id),  },  User: {    orders: (user, { last }) => db.orders.findRecent(user.id, last),    loyaltyPoints: (user) => loyaltyService.getPoints(user.id),  // this can call another service!  },  OrderItem: {    product: (item) => db.products.findById(item.productId),  },};

The GraphQL engine reads the query and calls the resolvers it needs. A resolver can get data from a database, a REST API, a gRPC service or a cache. GraphQL is a layer in front of your data sources. It is not a database.

The N+1 problem (again!)

The N+1 problem means running one query to get a list, then one more query for each item in the list. For N items that is N+1 queries, which is slow.

12
queries without DataLoader
one per item
1
query with DataLoader
WHERE id IN (…12 ids)
per request
batching + caching scope
no stale data across users
Three orders with four items each, resolving OrderItem.product. · numbers

Look at OrderItem.product above. If the query returns 3 orders with 4 items each, that resolver runs 12 times, which is 12 separate database queries. This is the same N+1 problem as in lesson 5, and it is easy to create by accident in GraphQL.

The fix: DataLoader. DataLoader is a small library that batches (groups) many small lookups into one. It also caches results for the length of one request. It collects all the product IDs asked for during one tick (one turn of the JavaScript event loop), then fetches them in one query:

JavaScript
// One query for all ids. The function must return the results in the same order as the ids.const productLoader = new DataLoader(ids => db.products.findByIds(ids));OrderItem: {  product: (item) => productLoader.load(item.productId),}

Twelve queries became 1. Almost every serious GraphQL server uses this pattern.

The caching challenge

REST GETs are easy to cache. Each URL is a resource, and CDNs (networks that keep copies of content near users) understand Cache-Control (lesson 14). GraphQL usually sends POST requests to one URL with different queries in the body, so HTTP and CDN caching does not work by default.

Solutions:

  • Persisted queries: the server stores approved queries in advance. The client sends only a hash or ID of the query (a hash is a short fingerprint), and it can use GET. This makes responses cacheable and also blocks queries that nobody approved.
  • Client-side normalised caches (Apollo Client, Relay) store each object once, under its ID, so the same User:42 is reused on every screen.
  • Server-side caching in resolvers or DataLoaders, plus Redis, a fast in-memory cache (lessons 15–16).

Security and performance risks

Because clients can write any query, a harmful or careless client can send a huge one. This example goes deeper and deeper through friends of friends:

GraphQL
{ user(id: "1") { friends { friends { friends { friends { name } } } } } }

Protections:

  • Query depth limits: refuse queries that nest too deep, for example more than 7 levels.
  • Query cost analysis: give each field a cost and reject queries whose total is over a budget.
  • Pagination required on lists (lesson 34).
  • Timeouts and rate limits based on query cost, not just request count.
  • Persisted queries only for your own apps (no arbitrary queries in production).
  • Authorisation in resolvers. Authorisation means checking what a user may see or do. Check it for each field and object, not only at the endpoint.
  • Consider turning off introspection in production for private APIs. Introspection lets any client ask the server to describe its whole schema.

Errors are different

GraphQL often returns HTTP 200 even when parts of the query fail, with an errors array next to partial data. This is useful, because you still get the parts that worked. But monitoring tools that only look at HTTP status codes will miss these failures. Track GraphQL errors on purpose.

Federation: one graph, many teams

In large companies, many teams own different parts of the data. GraphQL Federation (for example, Apollo Federation) lets each team run its own subgraph (a small GraphQL API for users, orders or products). A gateway (also called a router) then combines them into one unified schema for clients:

  1. 1
    Mobile and web clients
    one endpoint, one schema
  2. 2
    GraphQL router / gateway
    plans the query across subgraphs
  3. 3
    Users subgraph (Team A)
  4. 4
    Orders subgraph (Team B)
  5. 5
    Products subgraph (Team C)
Federation. Each team owns a subgraph; clients see one schema. · flow

When to use which

SituationBest fit
Public API for many third-party developersREST (simple, cacheable, familiar)
Many client types (web, iOS, Android, TV) needing different data from many sourcesGraphQL
Mobile apps on slow networks with complex screensGraphQL (fewer round trips, smaller payloads)
Internal high-performance service-to-service callsgRPC (lesson 31)
Simple CRUD app with one frontendREST
Heavy file uploads/downloads, simple caching via CDNREST
Real-time updatesWebSockets/SSE (lesson 33), or GraphQL subscriptions

Many companies use all three: REST for public APIs, GraphQL for their own apps, and gRPC between services.

REST
  • +Simple, cacheable by CDNs, universal tooling
  • −Over- and under-fetching, many round trips
Use whenPublic APIs, simple CRUD, file transfer
GraphQL
  • +Exactly the fields needed, in one request
  • +Typed schema; one front door over many services
  • −Harder caching, N+1 risk, expensive queries
Use whenMany client types with complex screens
gRPC
  • +Fast binary, typed contracts, streaming
  • −Poor browser support, not human-readable
Use whenInternal service-to-service calls
Three API styles, three best uses. Many companies run all three. · compare

Trade-offs

GraphQL

  • ✅ Exactly the data needed, in one request, with a strong typed schema and great tools (auto-complete, docs, code generation).
  • ✅ Frontend teams move faster because they do not wait for new endpoints. It also works as a single front door over many services.
  • ❌ Server complexity: resolvers, DataLoaders, cost limits, federation.
  • ❌ Caching is harder than REST.
  • ❌ Risk of expensive queries without limits.
  • ❌ Monitoring is harder: one endpoint, and 200 responses with errors inside.

REST

  • ✅ Simple, cacheable, universal, and easy to monitor and debug.
  • ❌ Over- and under-fetching, many round trips, and endpoint sprawl for varied clients.

In the Real World

Facebook created GraphQL in 2012 to power its mobile News Feed, because its REST-style APIs made the native apps slow and hard to change. It was released as open source in 2015 and moved to the independent GraphQL Foundation in 2018.

GitHub launched a public GraphQL API (v4) alongside its REST API, explaining that integrators often needed many REST calls to get what one GraphQL query could fetch. Both APIs are still offered, which is a good example of using both styles.

Shopify offers GraphQL APIs for building storefronts and apps, and applies cost-based rate limiting: each query has a calculated cost, and apps get a budget. This is the protection described above.

Netflix has written about moving to a federated GraphQL architecture, so that many backend teams add to one graph used by Netflix's apps. This replaces one giant API layer that a single team would maintain.

Interview Questions

Q1What problems does GraphQL solve compared with REST?

Over-fetching means a fixed response has fields you do not need. Under-fetching means you need several round trips to build one screen. Endpoint sprawl means more and more endpoints, because each client wants a different shape. In GraphQL, the client lists exactly the fields it needs, checked against a typed schema, and gets them in one request.

Q2What is the N+1 problem in GraphQL, and how do you fix it?

A field resolver runs once for each parent object. So resolving the product of 12 order items sends 12 queries. DataLoader collects the keys asked for in one tick and fetches them in a single batched query. It also keeps a cache for that request.

Q3Why is caching harder with GraphQL?

Requests are usually POSTs to one URL with the query in the body, so caching by URL, in HTTP or a CDN, does not work. Use persisted queries (sent by ID, even with GET), normalised client caches that store objects by ID, and caching inside resolvers.

Q4How do you protect a GraphQL API from abusive queries?

Limit query depth. Work out a cost for each query, and reject or rate-limit by cost. Require pagination on lists and set timeouts. Allow only persisted queries for your own apps. Check authorisation for each field and object in the resolvers.

Q5When would you choose REST over GraphQL?

For public APIs used by many third parties, simple CRUD with one client, heavy file transfer, or when CDN caching and straightforward monitoring matter more than flexible data fetching.

Key Takeaways

  • GraphQL lets clients request exactly the fields they need, in one request, based on a typed schema. It is a layer in front of your data sources.
  • Resolvers fetch each field. Use DataLoader to batch and avoid N+1 queries.
  • Caching is harder than REST, so use persisted queries and client-side normalised caches.
  • Protect the server with depth and cost limits, pagination, per-field authorisation and cost-based rate limiting.
  • Use REST for simple and public APIs, GraphQL for varied, complex clients, and gRPC for internal performance. Mixing them is normal.

Further Reading

  • The official GraphQL website: "Learn" and "Best Practices"
  • The GraphQL specification
  • The DataLoader project on GitHub
  • GitHub's engineering blog post introducing its GraphQL API
  • Netflix Tech Blog posts on GraphQL federation
  • Shopify's documentation on GraphQL rate limiting and query cost

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related series