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.
On this page
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:
- GET /users/4240 fields, 2 needed
- GET /users/42/orders?limit=3
- GET /products/991
- GET /products/405
- GET /products/17
- GET /users/42/loyalty
- With GraphQL: one POST /graphql returning exactly the fields the screen draws
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:
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):
query ProfileScreen { user(id: "42") { name avatarUrl loyaltyPoints orders(last: 3) { total items { product { name thumbnailUrl } } } }}The response has exactly that shape:
{ "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:
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.
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:
// 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:42is 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:
{ 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:
- 1Mobile and web clientsone endpoint, one schema
- 2GraphQL router / gatewayplans the query across subgraphs
- 3Users subgraph (Team A)
- 4Orders subgraph (Team B)
- 5Products subgraph (Team C)
When to use which
| Situation | Best fit |
|---|---|
| Public API for many third-party developers | REST (simple, cacheable, familiar) |
| Many client types (web, iOS, Android, TV) needing different data from many sources | GraphQL |
| Mobile apps on slow networks with complex screens | GraphQL (fewer round trips, smaller payloads) |
| Internal high-performance service-to-service calls | gRPC (lesson 31) |
| Simple CRUD app with one frontend | REST |
| Heavy file uploads/downloads, simple caching via CDN | REST |
| Real-time updates | WebSockets/SSE (lesson 33), or GraphQL subscriptions |
Many companies use all three: REST for public APIs, GraphQL for their own apps, and gRPC between services.
- +Simple, cacheable by CDNs, universal tooling
- −Over- and under-fetching, many round trips
- +Exactly the fields needed, in one request
- +Typed schema; one front door over many services
- −Harder caching, N+1 risk, expensive queries
- +Fast binary, typed contracts, streaming
- −Poor browser support, not human-readable
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