ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

REST vs GraphQL for Frontend Developers

Short answer

REST exposes many URLs, each returning a fixed resource shape, and gets HTTP caching for free. GraphQL exposes one endpoint where the client specifies exactly which fields it wants, eliminating over-fetching and extra round trips. Neither is better — you are trading simplicity and cacheability against flexibility.

RESTGraphQL
EndpointsMany — one per resourceOne, usually /graphql
Response shapeFixed by the serverChosen by the client
MethodGET, POST, PUT, PATCH, DELETEAlmost always POST
Over-fetchingCommonEliminated by design
Under-fetchingCommon — several round tripsOne request can span the graph
HTTP cachingFree, by URLNot usable — needs a client cache
ErrorsHTTP status codes200 with an errors array
TypingOpenAPI, if maintainedThe schema is mandatory and introspectable
Versioning/v1, /v2Additive changes plus deprecation
Server complexityLowResolvers, batching, depth limits
Both are HTTP APIs; almost everything else differs.

Two different shapes of API

In REST, the server decides what a resource looks like and gives it a URL. GET /users/7 returns a user, and whatever fields the server includes are what you get. In GraphQL there is a single endpoint and a schema describing every type and field available; the client sends a query naming the fields it wants, and the response mirrors that query exactly.

The same screen, two ways of askingjavascript
1// REST — three round trips to render one profile page
2const user  = await fetch("/api/users/7").then(r => r.json());
3const posts = await fetch("/api/users/7/posts").then(r => r.json());
4const stats = await fetch("/api/users/7/stats").then(r => r.json());
5// → three requests, and each returns every field the server
6//   defines even though the page shows four of them
7
8// GraphQL — one request, exactly the fields the page renders
9const { data } = await gql(`
10  query Profile($id: ID!) {
11    user(id: $id) {
12      name
13      avatarUrl
14      posts(last: 5) { title slug }
15      stats { followers }
16    }
17  }
18`, { id: "7" });
19// → one round trip, and the response has no field the UI
20//   does not use

That example is the entire sales pitch for GraphQL, and it is genuinely compelling on a slow mobile connection where each round trip costs a couple of hundred milliseconds. What it hides is everything the server now has to do to answer that query efficiently, which is where the trade-off actually lives.

Over-fetching and under-fetching

These two words are the origin story, and being precise about them is worth marks. Over-fetching is receiving more data than the screen needs — a user endpoint returning forty fields when the header shows a name and an avatar. Under-fetching is receiving less than the screen needs, so the client makes another request, often in a waterfall where each one depends on the last.

The waterfall REST produces, and why it hurtsjavascript
1const post = await fetch("/api/posts/1").then(r => r.json());
2// → 120ms
3const author = await fetch(`/api/users/${post.authorId}`).then(r => r.json());
4// → 120ms, and it could not start until the first finished
5const comments = await fetch(`/api/posts/1/comments`).then(r => r.json());
6// → 120ms
7
8// Total: ~360ms of sequential latency before anything renders,
9// because each request needs an id from the previous response.
10
11// The same data in GraphQL:
12// → one request, ~130ms — the server resolves the graph internally

REST has answers to both. Sparse fieldsets — GET /users/7?fields=name,avatar — address over-fetching. Compound or expanded endpoints — GET /posts/1?include=author,comments — address under-fetching. They work, and large public APIs use them. What they are not is systematic: each one is a server-side decision made per endpoint, which is precisely the coordination cost GraphQL is designed to remove.

Caching is the real trade-off

This is the section most comparisons skip, and it is the one that decides real architectures. A REST GET has a URL, which means the entire HTTP caching stack applies for free: the browser cache, the CDN, a reverse proxy, ETags and conditional requests. A popular endpoint can be served from an edge node without your server ever seeing the request.

GraphQL sends queries as POST bodies to one URL, so none of that works. Two different queries share a URL and a method, and POST is not cacheable. The caching moves into the client library — Apollo, urql or Relay — which normalises responses into a store keyed by type and id, so a user fetched by one query is reused by another.

Where the cache lives in eachjavascript
1// REST: the platform caches it, and you configure with headers
2// Cache-Control: public, max-age=300, stale-while-revalidate=60
3await fetch("/api/kits/react-kit");
4// → second call within 5 minutes never leaves the browser;
5//   a CDN serves everyone else's first call too
6
7// GraphQL: the client library caches it, keyed by entity
8const { data } = useQuery(GET_KIT, { variables: { slug: "react-kit" } });
9// → Apollo stores Kit:react-kit in a normalised cache. Another
10//   query asking for the same kit reads it without a network call,
11//   but only inside this browser tab's memory.

This is 1 of 200+ questions in the Complete Frontend Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the Complete Frontend Kit → ₹499

Errors and status codes

REST uses HTTP status codes as the error channel: 404 for a missing resource, 401 for unauthenticated, 422 for validation failures, 500 for a server fault. Every HTTP tool understands them without knowing anything about your API.

GraphQL returns 200 for almost everything and puts failures in an errors array beside the data. Because a query can ask for several things at once, it can also partially succeed — one field resolves, another errors — which HTTP status codes cannot express. That is coherent, but it means monitoring built on status codes reports a healthy API while users see failures.

Partial success, which has no REST equivalentjavascript
1// HTTP 200 OK
2{
3  "data": {
4    "user": { "name": "Arun" },
5    "billing": null
6  },
7  "errors": [
8    { "message": "Not authorised", "path": ["user", "billing"] }
9  ]
10}
11// → the name rendered fine; the billing panel must show an error.
12//   res.ok is true, so a naive `if (!res.ok) throw` misses it entirely.

The costs GraphQL adds on the server

Letting clients shape queries means the server can no longer predict its own workload, and three problems follow. The first is N+1: a query for fifty posts each with an author naively triggers one query for the posts and fifty for the authors. DataLoader-style batching fixes it, but it is machinery you must add and keep correct.

N+1, and the batching that fixes itjavascript
1// Naive resolver
2const resolvers = {
3  Post: { author: (post) => db.user.findUnique({ where: { id: post.authorId } }) },
4};
5// → 50 posts triggers 51 database queries
6
7// With a per-request DataLoader
8const userLoader = new DataLoader(ids =>
9  db.user.findMany({ where: { id: { in: ids } } })
10);
11const resolvers = {
12  Post: { author: (post) => userLoader.load(post.authorId) },
13};
14// → 2 queries: one for posts, one for all authors at once

The second is unbounded queries. A client can request deeply nested data — friends of friends of friends — or ask for a million records, and a public GraphQL endpoint without depth limiting, complexity scoring and pagination enforcement is a denial-of-service vector that requires no special skill to exploit. The third is that per-field authorisation is genuinely harder: in REST you protect an endpoint, in GraphQL you must consider every path through the graph that could reach a sensitive field.

The middle ground: BFF

A backend-for-frontend is a thin server owned by the frontend team that sits between the client and whatever exists behind it. It calls the underlying REST services, assembles exactly what a screen needs, and returns one response. It is worth knowing because it gets you GraphQL's main benefit — one round trip, no over-fetching — without adopting a schema, resolvers or a new client cache.

A BFF route that collapses a waterfalljavascript
1// app/api/profile/[id]/route.ts
2export async function GET(req, { params }) {
3  const [user, posts, stats] = await Promise.all([
4    svc.users.get(params.id),
5    svc.posts.byUser(params.id, { last: 5 }),
6    svc.stats.get(params.id),
7  ]);
8
9  return Response.json({
10    name: user.name,
11    avatarUrl: user.avatarUrl,
12    posts: posts.map(p => ({ title: p.title, slug: p.slug })),
13    followers: stats.followers,
14  });
15  // → one client request, parallel fetches server-side, and the
16  //   payload is shaped for this screen. Still a cacheable GET URL.
17}

React Server Components are arguably the same idea folded into the framework: the component fetches on the server, composes whatever it needs, and only the result crosses the network. Mentioning that connection in an interview reads well, because it shows you see the pattern rather than the product names.

Choosing between them

The decision usually turns on how many different clients consume the API and how varied their data needs are. One web app talking to a handful of stable resources rarely justifies GraphQL. A web app plus two mobile apps plus a partner integration, each wanting different slices of an entity graph, is exactly the case GraphQL was built for.

SignalPoints toWhy
Multiple clients with different data needsGraphQLEach asks for its own slice, no new endpoints
Screens that compose several servicesGraphQL or BFFOne round trip instead of a waterfall
Mobile clients on slow networksGraphQLSmaller payloads, fewer round trips
Public, read-heavy, cacheable contentRESTCDN and browser caching for free
File uploads and downloadsRESTGraphQL needs a multipart spec to manage it
Small team, small API surfaceRESTFar less machinery to build and secure
Rapidly changing frontend requirementsGraphQLNew screens need no backend change
Strict per-field authorisationRESTEndpoint-level rules are simpler to audit
Signals pointing each way.

Is GraphQL a replacement for REST?

No — it is an alternative with a different set of trade-offs, and the two are frequently used together. GraphQL wins where clients need varied slices of connected data; REST wins where responses are stable, public and worth caching in shared infrastructure.

Why does GraphQL use POST for everything?

Because queries are sent in the request body and can easily exceed practical URL length limits. The cost is that POST is not cacheable by browsers or CDNs. Persisted queries work around it: the client sends a short hash over GET, and the server looks up the registered query.

What is the N+1 problem in GraphQL?

A nested field resolved once per parent — fifty posts each fetching their author individually, producing fifty-one database queries. The standard fix is DataLoader, which collects the ids requested within one tick and issues a single batched query for all of them.

How do you version a GraphQL API?

Usually you do not. The convention is additive evolution: add fields freely, mark old ones with @deprecated, and remove them once usage tracking shows nobody queries them. Because clients request fields explicitly, adding a field cannot break an existing query — which is the strongest structural argument for GraphQL.

Does React Query work with GraphQL?

Yes — it is transport-agnostic, so any function returning a promise works, including a graphql-request call. You lose the normalised entity cache that Apollo and urql provide, keeping a simpler per-query cache. That is a reasonable trade if your app does not need cross-query entity updates.

Frequently asked questions

What is the difference between REST and GraphQL?
REST exposes many endpoints, each returning a resource in a shape the server decides. GraphQL exposes one endpoint and a schema, and the client sends a query naming exactly the fields it wants. The response mirrors the query, so no field arrives unused and related data comes back in one request.
Is GraphQL better than REST?
Not inherently. GraphQL is better when several clients need different slices of connected data or when screens would otherwise require a waterfall of requests. REST is better for public, cacheable, read-heavy resources, for file transfer, and for small APIs where the extra machinery is not justified.
What is over-fetching and under-fetching?
Over-fetching is receiving fields the screen does not use — a forty-field user object for a name and avatar. Under-fetching is receiving too little, forcing extra requests, often in a sequential waterfall. GraphQL addresses both; REST addresses them per endpoint with sparse fieldsets and include parameters.
How is caching different in GraphQL and REST?
A REST GET has a URL, so the browser, CDN and proxies cache it for free and one cached response serves everyone. GraphQL posts to a single URL, so HTTP caching does not apply and the work moves into a client library that normalises entities by type and id — a cache local to each browser.
What is the N+1 problem in GraphQL?
One query for a list plus one query per item for a nested field — fifty posts each resolving an author becomes fifty-one database queries. DataLoader solves it by batching the ids requested in a single tick into one query, and by de-duplicating repeats within a request.
Does GraphQL use HTTP status codes?
Rarely for application errors. A GraphQL response is usually 200 with an optional errors array beside the data, because a query can partially succeed in ways a single status code cannot express. Transport-level failures — a 500 or a 401 from a gateway — still use status codes.
How do you secure a GraphQL API?
Disable introspection publicly, cap query depth and total complexity, require pagination on list fields, apply authorisation per field rather than per endpoint, and prefer persisted queries so only pre-registered operations are accepted. An open endpoint without depth limits is a denial-of-service risk.
Can you use REST and GraphQL together?
Yes, and many production systems do. REST serves cacheable public resources and file transfer while GraphQL serves composed application views. A GraphQL layer is also often built on top of existing REST services rather than replacing them.
What is a BFF (backend-for-frontend)?
A thin server owned by the frontend team that calls the underlying services and returns exactly what a screen needs in one response. It gives you GraphQL's main benefit — a single round trip with no over-fetching — while keeping cacheable GET URLs and requiring no schema or resolver layer.
How do you version a GraphQL API?
Additively. Add new fields, deprecate old ones with the @deprecated directive, and remove them once usage tracking shows they are unused. Because clients name the fields they want, adding a field cannot break an existing query, which is why /v2 endpoints are uncommon in GraphQL.
Is GraphQL good for file uploads?
It is workable but awkward. The spec has no native binary type, so uploads rely on the multipart request specification supported by some servers and clients. Most teams keep file transfer on a plain REST endpoint or a direct-to-storage signed URL and use GraphQL for the metadata.
Should a frontend developer learn GraphQL?
Enough to be productive in it: writing queries and mutations, using variables and fragments, and understanding how a normalised client cache updates after a mutation. It appears in a meaningful share of job descriptions, and interviews tend to probe the caching and N+1 trade-offs rather than syntax.

This is 1 of 200+ questions in the Complete Frontend Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the Complete Frontend Kit → ₹499
Written by Arun Karthikeyan · Last updated

Full kit

Complete Frontend Kit · ₹499

Get it →