Skip to main content
Blog

GraphQL vs REST: Choosing the Right API Paradigm

Last updated API

The GraphQL versus REST argument is usually conducted as though one is a replacement for the other. It is more useful to see them as optimising for different constraints. REST optimises for a stable, cacheable, widely consumable interface where the server decides what a resource looks like. GraphQL optimises for clients with varying needs over a connected graph, where the client decides what it receives. Those are genuinely different problems, and the right answer depends on which one you have.

This guide explains how each actually works, where each breaks down, what the operational differences mean in production, and how to decide without relying on preference. It also covers running both, which is what a large number of organisations end up doing for entirely good reasons.


What you will learn
  • What each paradigm actually optimises for
  • The problems GraphQL was designed to solve, and whether you have them
  • Where GraphQL is genuinely harder: caching, authorisation, cost control
  • How performance and the N+1 problem work in each
  • Versioning, tooling and operational differences
  • A decision framework, and how to run both sensibly
In this article
  1. What each one is
  2. The problems GraphQL addresses
  3. How a GraphQL request works
  4. Caching: the biggest practical difference
  5. Authorisation
  6. Query cost and abuse
  7. The N+1 problem
  8. Errors and status
  9. Versioning and evolution
  10. Tooling and developer experience
  11. Federation and multiple services
  12. Operational differences
  13. A decision framework
  14. Running both
  15. Twelve mistakes
  16. A worked example: the same screen, both ways
  17. Frequently asked questions

1. What each one is

REST is an architectural style built on HTTP. Resources are identified by URLs, manipulated with standard methods, and represented in a format the server decides. Its defining property is that it uses HTTP as intended, which means every intermediary — proxies, caches, gateways, monitoring — understands it without knowing your domain.

GraphQL is a query language and runtime. A single endpoint accepts a query describing exactly what the client wants, and returns precisely that shape. Its defining property is that the client specifies the response, which decouples client requirements from server-defined resource boundaries.

The consequences of that single difference propagate through everything else. Server-defined responses are cacheable, predictable and easy to reason about, but may not match what any particular client needs. Client-defined responses fit every client exactly, at the cost of making caching, authorisation and cost control harder.

2. The problems GraphQL addresses

Three specific problems, and it is worth checking honestly whether you have them.

Over-fetching. An endpoint returns forty fields; the screen needs four. Bandwidth and parsing time are wasted, which matters most on mobile networks and low-end devices. Genuinely a problem at scale; frequently irrelevant on a fast connection.

Under-fetching and waterfalls. A screen needs a user, their recent orders, and each order's items. In REST that may be three sequential round trips, each waiting on the previous. On a high-latency connection this dominates load time. This is the strongest argument for GraphQL and the one most likely to apply.

Client diversity. A web application, a mobile application and a partner integration each need different subsets of the same data. In REST this produces either one bloated response or a proliferation of purpose-built endpoints that multiply as clients change.

If none of these describes your situation — a single first-party client, a well-shaped API, low latency — GraphQL is solving problems you do not have, and you will pay its costs for no return.

3. How a GraphQL request works

Understanding the execution model explains most of the operational differences.

A schema defines the types available and how they relate. A query names fields from that schema, following relationships as deep as the client wants. The runtime walks the query, calling a resolver for each field — a function that knows how to fetch that piece of data.

Three consequences follow directly:

  • Every field is potentially a separate fetch. Which is why the N+1 problem is structural rather than accidental, and why batching is not optional.
  • The server cannot know in advance what will be asked. Which is why cost analysis and depth limiting are necessary rather than defensive extras.
  • Authorisation must be evaluated per field, not per endpoint, because there is only one endpoint.

The mutation side is deliberately separate: queries are expected to be side-effect free and may execute in parallel, while mutations execute sequentially. Subscriptions add a real-time channel, typically over a persistent connection.

4. Caching: the biggest practical difference

This is where the paradigms diverge most sharply in production, and it is the difference most often underestimated.

REST caches naturally. A GET request to a URL is cacheable by browsers, proxies and content delivery networks using standard headers. Nobody has to build anything: the infrastructure between your client and your server already knows how to do this, and it works at the edge, close to the user.

GraphQL does not. Requests are typically POSTs to a single endpoint, which HTTP caching treats as uncacheable by default. There is no URL identifying a resource, so an intermediary has nothing to key on. Caching must therefore be built at other layers:

  • Client-side normalised caching, where the client library stores entities by identifier and serves subsequent queries from local state. This is genuinely powerful and is a real advantage for interactive applications.
  • Persisted queries where clients send a hash rather than a query string, which allows GET requests and therefore restores some HTTP caching.
  • Server-side caching at the resolver or data-source level, which is where most practical caching ends up.

The decision rule that follows: if your traffic is dominated by public, cacheable content — a catalogue, articles, anything many users request identically — REST plus a content delivery network is dramatically simpler and faster. If your traffic is authenticated and personalised, edge caching was never going to help you much, and GraphQL's caching disadvantage largely disappears.

5. Authorisation

In REST, authorisation is naturally per endpoint: this caller may access this resource. It is straightforward to reason about and straightforward to enforce in a shared layer.

In GraphQL, a single query can traverse from a user to their orders to the items to the supplier. Authorisation must be evaluated at each step, because the traversal path is chosen by the client. Checking only at the entry point means a client can reach data through a relationship you did not anticipate.

The patterns that work: enforce in the data layer so every fetch is scoped to the caller regardless of how it was reached; attach rules to types and fields declaratively rather than scattering checks through resolvers; and be careful with relationships that cross ownership boundaries, which is where the interesting vulnerabilities live.

The failure mode worth naming: a field that is safe on one type becomes unsafe when reached through a different path. This class of bug does not exist in REST, because there is no arbitrary traversal — and it is the reason GraphQL authorisation deserves deliberate design rather than incremental accumulation.

6. Query cost and abuse

REST endpoints have bounded cost: you know what each one does. GraphQL queries have cost determined by the client, which means a single request can be arbitrarily expensive.

A deeply nested query following relationships in a cycle can produce enormous work from a small request. This is not hypothetical; it is a standard denial-of-service technique against unprotected GraphQL endpoints.

Four protections, all necessary for a public endpoint:

  • Depth limiting. Reject queries nested beyond a threshold.
  • Complexity analysis. Assign a cost to each field, sum it, and reject above a budget — with list fields costing proportionally to the requested page size.
  • Allowlisted queries. Accept only queries you have registered. This is the strongest protection and is entirely practical for first-party clients, since it also enables persisted queries and their caching benefits.
  • Timeouts and rate limits based on cost rather than request count, since one query is not equivalent to another.

An internal API consumed only by your own allowlisted clients needs much less of this. A public GraphQL API needs all of it, and that is a meaningful part of the cost of choosing it.

7. The N+1 problem

Fetching a list of items and then fetching related data for each item individually — one query becoming a hundred — is possible in any system and structural in GraphQL, because each field resolves independently and the resolver for a nested field runs once per parent.

The standard solution is a batching loader: within a single execution, requests for the same type are collected and satisfied in one underlying query. This is not an optimisation to add later; a GraphQL server without it will fall over under ordinary use.

Two further considerations. The batching layer typically deduplicates within a request, so requesting the same entity through several paths costs one fetch. And query planning becomes harder to reason about than a hand-written REST handler, where the queries executed are visible in one place — which is why observability at the resolver level matters more than it does for REST endpoints.

8. Errors and status

REST uses HTTP status codes, which every client library, proxy and monitoring tool already interprets. A 404 means not found; a 429 means slow down; a 500 means the server failed. Retry logic, alerting and dashboards all work without configuration.

GraphQL returns 200 for almost everything, with errors in the response body — because a single query can partially succeed. This is a coherent design for partial results and it costs you the entire HTTP error ecosystem: monitoring reports a healthy service while every request is failing, unless you build error tracking specifically.

The practical requirements: a structured error shape with machine-readable codes in the extensions field so clients can branch reliably; a decision about which errors belong in the errors array versus being modelled as result types in the schema; monitoring that inspects response bodies rather than status codes; and care not to leak internal detail in messages, since the default behaviour of many implementations is verbose.

9. Versioning and evolution

This is the clearest advantage for GraphQL and it is genuine.

Because clients request specific fields, the server knows exactly which fields each client uses. Adding a field affects nobody. Removing one affects only clients that request it — and you can know precisely who they are from your own traffic. Deprecation becomes evidential rather than nervous: mark a field deprecated, watch usage fall, remove it when nobody asks for it.

REST evolution requires more discipline. Adding fields is safe if clients ignore unknown properties. Removing anything requires a version or a deprecation cycle, and knowing who is affected requires instrumenting responses rather than requests. In practice this means REST APIs accumulate fields nobody uses, because removing them is risky.

The caveat: this advantage depends on knowing your clients. For a public GraphQL API with many unknown consumers, field removal is as fraught as it is in REST — you simply have better data about who would break.

10. Tooling and developer experience

RESTGraphQL
ContractOpenAPI specification, if maintainedSchema, intrinsic and always current
ExplorationDocumentation renderer or an HTTP clientInteractive query explorer with introspection
Type safetyGenerated from the specificationGenerated from the schema
Client cachingManual or via a data-fetching libraryNormalised caching built into major clients
DebuggingStandard HTTP tooling everywhereRequires GraphQL-aware tooling
Learning curveLow — everyone knows HTTPModerate on the client, higher on the server

GraphQL's strongest practical advantage is that the schema is intrinsic. A REST API's specification can drift from reality; a GraphQL schema cannot, because it is what the server executes. Introspection means tooling works without anyone maintaining documentation, and that is a real ongoing saving.

Against that, REST benefits from the entire HTTP ecosystem. Every proxy, load balancer, cache, log analyser and monitoring tool understands it. GraphQL requires purpose-built equivalents for several of these, which exist and are good, and are additional things to run.

11. Federation and multiple services

In a system of many services, a client needing data from several of them faces a choice: call each directly, or call something that composes them.

REST typically solves this with a backend-for-frontend — a service per client type that calls the underlying services and returns exactly what that client needs. Simple, explicit, and it duplicates composition logic per client type.

GraphQL federation solves it differently: each service owns part of a combined schema, and a gateway composes them into one graph. A client issues a single query spanning several services without knowing they are separate.

Federation is genuinely powerful for large organisations and is a substantial commitment. It introduces a gateway in the critical path, requires schema governance across teams so types compose correctly, and makes performance debugging harder because a slow query may implicate any service in the path. It is appropriate when many teams serve many clients over a genuinely connected domain, and considerable overhead when applied to three services.

12. Operational differences

ConcernRESTGraphQL
Edge cachingWorks nativelyNeeds persisted queries, or does not apply
MonitoringStatus codes and paths, out of the boxBody inspection and per-resolver instrumentation
Rate limitingPer endpoint, straightforwardCost-based, must be built
Debugging a slow requestOne handler to inspectResolver-level tracing required
Denial-of-service exposureBounded by endpoint designClient-controlled cost; needs explicit limits
DeprecationRequires request instrumentationField usage known intrinsically
Client payload efficiencyFixed shape; over-fetching commonExact shape requested

The summary that emerges: REST inherits its operational tooling; GraphQL requires you to build or adopt equivalents. That is a real and recurring cost, and it is the aspect most often omitted from enthusiastic comparisons.

13. A decision framework

Answer in order and stop at the first strong signal.

Is your traffic dominated by public cacheable content?

A catalogue, articles, listings — anything where many users request the same thing. REST with edge caching is dramatically simpler and faster. This single factor decides many cases.

How many distinct clients consume the API?

One first-party client suggests REST — you can shape endpoints to fit it. Several clients with genuinely different data needs is the strongest argument for GraphQL.

Is the data a connected graph or a set of independent resources?

Deeply related data traversed in varying ways suits GraphQL. Largely independent resources fetched individually suit REST, and forcing them into a graph adds machinery for no benefit.

Who consumes it, and how well do you know them?

A public API with unknown consumers benefits from REST's ubiquity, predictable cost and standard error semantics. Internal or first-party consumption removes most of GraphQL's operational objections, because allowlisted queries become practical.

What does your team know?

GraphQL done badly is worse than REST done well. If nobody has run a GraphQL server in production, budget for learning authorisation patterns, batching and cost control — the three areas where inexperience produces real incidents.

The shortcut
  • Public API, many unknown consumers, cacheable content → REST
  • Several first-party clients, connected data, high-latency users → GraphQL
  • One client, straightforward resources → REST; you have neither problem
  • Many teams, many clients, one connected domain → GraphQL with federation
  • Both audiences → both, deliberately

14. Running both

A large number of organisations end up with REST at the public boundary and GraphQL for first-party clients, and this is a coherent design rather than indecision.

The reasoning: partners and public consumers benefit from REST's ubiquity, predictable cost, cacheability and standard error handling. Internal clients benefit from GraphQL's flexibility, and the operational objections largely disappear when every consumer is known and queries can be allowlisted.

The essential discipline is that both must sit on the same underlying domain layer. Two independent implementations of the same business rules will diverge, and the divergence will produce bugs that depend on which interface the caller used. The API layer should be a thin projection over shared logic, in both cases.

The cost is a second interface to document, monitor and secure. That is acceptable when both audiences genuinely exist, and pure overhead when one of them is hypothetical.

15. Twelve mistakes

  1. Choosing GraphQL without the problems it solves. Costs paid, no benefit obtained.
  2. No batching layer. The N+1 problem is structural; the server will not survive ordinary load.
  3. Authorisation at the entry point only. Data reachable through relationships you did not consider.
  4. No query cost limits on a public endpoint. A denial-of-service vector by construction.
  5. Assuming edge caching will work. It will not, without persisted queries.
  6. Monitoring on status codes with GraphQL. A healthy dashboard while everything fails.
  7. Exposing internal errors in the response. The default is frequently too verbose.
  8. Modelling the schema on database tables. Leaks storage structure into a client-facing contract.
  9. Federation for three services. Substantial governance overhead for no benefit.
  10. Two independent implementations of business rules. Interfaces that disagree with each other.
  11. REST with verbs in URLs and 200 for errors. Discards everything HTTP already provides.
  12. Treating the choice as permanent. Adding an interface over a good domain layer is far cheaper than a rewrite.

16. A worked example: the same screen, both ways

Consider a customer account screen showing profile details, the five most recent orders, each order's line items, and the delivery status of anything in transit.

In REST, without care, this is four round trips — profile, orders, items per order, delivery status — with the last two dependent on the first two. On a fast connection nobody notices; on a mobile connection with two hundred milliseconds of latency, that sequence adds most of a second before anything renders. The usual fix is a composed endpoint returning the whole screen's data in one response, which works well and creates a per-screen endpoint that must evolve as the screen does.

In GraphQL, it is one query requesting exactly those fields, resolved server-side in a single round trip. The client gets precisely what it needs, and when the design changes to show four orders instead of five, no server change is required. This is the scenario GraphQL was built for, and the advantage is real.

But look at what the GraphQL version requires to be production-ready. A batching loader, or fetching line items for five orders becomes five queries and delivery status becomes five more. Authorisation checked at each traversal — the orders belong to this customer, and so do their line items, and the delivery record must not expose the courier's internal notes. Cost limits, so a client cannot request a thousand orders with all relationships. And resolver-level tracing, because when this query is slow, the question "which part" has no answer from an HTTP log.

And look at what the REST version gets free. If the account screen were public — say, a public profile — the composed endpoint would be cached at the edge and served in milliseconds without touching your servers. Monitoring shows the endpoint's latency and error rate without instrumentation. A rate limit is one line of gateway configuration.

The screen is authenticated and personalised, so edge caching was never available, which removes REST's largest advantage and makes GraphQL the better fit here. Change one detail — make it a public catalogue page requested identically by a hundred thousand users — and the conclusion reverses entirely.

That reversal is the point. The right answer is determined by properties of the traffic and the clients, not by properties of the technologies.

17. Frequently asked questions

Is GraphQL faster than REST?

Sometimes, for a specific reason: fewer round trips and less over-fetching, which matters most on high-latency connections. It is frequently slower for cacheable public content, where REST is served from the edge without touching your infrastructure. Both are dominated by your database queries and your architecture rather than by the protocol.

Can we migrate an existing REST API to GraphQL?

Yes, incrementally, by adding a GraphQL layer over the same domain services rather than over the REST endpoints. Wrapping REST calls in resolvers is a common shortcut that produces a slow, awkward layer inheriting the endpoint boundaries you were trying to escape. Both interfaces should be thin projections over shared logic.

Does GraphQL replace a backend-for-frontend?

Often, yes — a client requesting exactly what it needs removes much of the reason a per-client backend existed. What it does not remove is client-specific logic that is genuinely computation rather than data shaping, and cases where a client needs orchestration across systems with side effects. Those still belong somewhere, and a resolver is usually the wrong place.

How do we handle file uploads?

Neither paradigm handles binary well inside the query or body. The practical answer for both is the same: request a signed upload location from the API, upload directly to storage, and then reference the resulting identifier. This keeps large payloads out of your application servers entirely and is better than any in-band alternative.

What about real-time updates?

GraphQL has subscriptions as a first-class concept, which is convenient when you are already using it. REST has no equivalent and pairs with server-sent events or websockets. Neither is clearly better — the underlying operational work of managing persistent connections, reconnection and backpressure is the same either way, and it is where the real difficulty lies.

Should a public API be GraphQL?

Usually not. Public APIs benefit from REST's ubiquity, predictable cost, standard error semantics and cacheability, and public GraphQL endpoints require the full set of cost protections against unknown consumers. Several large platforms do offer public GraphQL successfully, and they invest heavily in exactly those protections. For most organisations, REST publicly and GraphQL internally is the lower-risk arrangement.

How do we prevent the schema becoming a mess?

Treat it as a product with an owner and a review process. Model it on domain concepts rather than database tables, keep naming consistent, deprecate rather than remove, and require review for schema changes as you would for any public interface. Schemas decay through many individually reasonable additions, so the defence has to be continuous.

What is the most common reason GraphQL adoption disappoints?

Adopting it without the problems it solves, and then discovering the costs. A single first-party client over well-shaped resources gains little and pays for caching complexity, per-field authorisation, cost control and additional tooling. The teams that are glad they chose it almost always had several clients, a genuinely connected domain, and latency-sensitive users.

Glossary

TermWhat it means
ResolverA function that knows how to fetch one field. The unit of execution in GraphQL, and the reason the N+1 problem is structural.
SchemaThe typed definition of everything the API exposes. Intrinsic to the server rather than a document maintained alongside it.
IntrospectionThe ability to query the schema itself, which is what makes explorers and code generation work without documentation.
Over-fetchingReceiving fields the client does not need. Wasted bandwidth and parsing, most noticeable on mobile.
Under-fetchingNeeding several sequential requests to assemble one screen. The waterfall that dominates load time on high-latency connections.
Batching loaderCollects requests for the same type within one execution and satisfies them in a single underlying query. Not optional in production.
Persisted queryA registered query the client references by hash. Enables GET requests, restores HTTP caching, and doubles as an allowlist.
Complexity analysisAssigning a cost to each field and rejecting queries above a budget. The main defence against client-controlled expense.
Normalised cacheA client-side store keyed by entity identifier, so data fetched by one query serves another. GraphQL's strongest caching story.
FederationComposing one graph from schemas owned by several services, behind a gateway.
Backend-for-frontendA service per client type that composes underlying services into exactly the shape that client needs. The common REST answer to the same problem.
Safe and idempotentREST properties determining whether a request may be cached and whether it may be retried. Central to why HTTP tooling works without configuration.

Two of these explain most production difficulty. Resolver is why a GraphQL server without a batching loader collapses under ordinary load — each field resolves independently, so a list of fifty items with one relationship becomes fifty-one queries. And persisted query is the mechanism that recovers several of GraphQL's operational disadvantages at once: it restores HTTP caching, bounds query cost, and removes the need for general-purpose complexity analysis against first-party clients.

Key takeaways

  • They optimise for different constraints. Server-defined and cacheable, versus client-defined and flexible.
  • Caching is the biggest practical difference. Public cacheable content strongly favours REST.
  • GraphQL requires batching, per-field authorisation and cost limits. None of these are optional.
  • Field-level usage data makes deprecation evidential, which is GraphQL's clearest genuine advantage.
  • REST inherits the HTTP ecosystem; GraphQL requires purpose-built equivalents.
  • Running both is legitimate, provided both are thin projections over one domain layer.

Choose by the properties of your traffic and your clients rather than by the properties of the technologies. And keep the business logic underneath both, so that if you choose wrongly, adding the other interface is a project rather than a rewrite.

Enjoyed this article?

Get more engineering insights from ELIVTECH — or talk to us about your project.

Get in touch