5 min read
GraphQL failed to replace REST. That sentence will annoy people who have bet a chunk of their stack on it, but the data backs it up: Postman’s 2024 State of the API report still has REST at ~89% of public API usage, with GraphQL parked around 11%. GitHub moved large parts of its v4 surface back to REST + webhooks. Shopify, one of the loudest GraphQL success stories, now ships a REST Admin API alongside it and tells merchants to pick whichever fits the call. HMRC still publishes its Making Tax Digital endpoints as REST. If GraphQL were the replacement it was sold as in 2017, none of that would be true.
The honest read is that GraphQL solved a real problem — over-fetching and under-fetching in mobile clients with chatty, nested data — and then escaped the lab into places it does not belong. Most teams adopted it because a conference talk told them REST was legacy. Most teams never measured the trade-off. Here is the audit most UK engineering orgs should have run in 2020 and didn’t.
The dirty secret of GraphQL is that it abandons the parts of HTTP that the rest of your infrastructure relies on. URL-level caching at Cloudflare, Fastly, or Akamai is a solved problem you get for free with REST. With GraphQL, your CDN sees one URL — /graphql — for every request, so you either pay for a persisted-query cache, run Apollo Router, or accept that you have no edge cache. The BBC has years of muscle memory tuning REST cache headers for iPlayer metadata. They are not going to throw that away because a Medium post said GraphQL is the future.
Then there is the operational reality. REST endpoints are easy to rate-limit, easy to mock with json-server, easy to load-test with k6, and easy to monitor with stock OpenTelemetry instrumentation on HTTP servers. GraphQL requires you to instrument resolvers individually, persist queries, and write custom dashboards because your APM tool still thinks you have one endpoint. Monzo’s public engineering posts have repeatedly stressed how much they value being able to reason about a single endpoint in isolation. It is no accident they stayed on REST for the core banking API.
# app.py — a Flask endpoint a new hire can debug on day one
from flask import Flask, jsonify, request
from functools import lru_cache
app = Flask(__name__)
@lru_cache(maxsize=1024)
def get_customer(customer_id: str) -> dict:
# pretend this hits Postgres
return {"id": customer_id, "name": "Ada", "tier": "gold"}
@app.get("/customers/<customer_id>")
def customer(customer_id):
resp = jsonify(get_customer(customer_id))
resp.headers["Cache-Control"]] = "public, max-age=60"
return resp
@app.errorhandler(429)
def ratelimit(e):
return jsonify({"error": "slow down"}), 429
Try shipping that equivalent in GraphQL: schema, resolver, persisted query, custom cache plugin, rate-limit plugin. By the time you have rebuilt the bits HTTP gave you for free, you have a small product inside your product.
I am not here to bury GraphQL. It is genuinely excellent in a narrow band: product surfaces with deeply nested, heterogeneous data where the client owns the query shape. The classic example is an admin dashboard like the one GOV.UK uses for caseworking, where one screen wants claimant details, linked notes, document metadata, and an audit trail, and the next screen wants a different slice of the same graph. Forcing that into REST means either fat endpoints or a forest of N+1 calls. GraphQL earns its complexity there.
It also earns its keep on mobile-first products with offline sync, where a normalised client cache (Apollo, Relay) lets you write optimistic UI without hand-rolling merge logic. A few UK fintechs use this pattern legitimately. The mistake is taking the mobile use case and copy-pasting it onto the internal admin tool, the public read API, the partner integration, and the webhook pipeline.
The bill arrives in year two. GraphQL shifts complexity from the server to the gateway, and from the server team to the platform team. You need:
graphql-inspector in CI, deprecation policy, a registry. REST has had Swagger/OpenAPI discipline for a decade; GraphQL teams routinely ship undocumented fields.{ user { email } } and walk off with your PII. The NHS would rightly fail you at a DSPT audit for this.orders { items { refunds { auditEvents } } } will DOS your database in production. You need DataLoader, depth limiting, and query cost analysis from day one.None of these are deal-breakers individually. The aggregate is a platform team that did not exist in the proposal that sold GraphQL to the director.
Stop defaulting to GraphQL. Default to REST, then ask four questions:
If the answer to all four is yes, you have a real GraphQL use case. Most teams answer yes to zero. That is why REST is still 89% of the API market and GraphQL is the special-purpose tool it always should have been. The pretence is not that GraphQL is bad. The pretence is that it is a default. It is not. Pick the boring thing, ship the feature, and stop writing conference talks about your schema when you could be writing them about your retention.
No. It is settling into a healthy niche for nested, client-driven queries. What is dying is the idea that it is a universal REST replacement. Adoption is flatlining, not collapsing, which is the more interesting story.
Almost certainly no. GOV.UK and HMRC publish REST for a reason: tooling, caching, and accessibility audits are all built around it. GraphQL adds accessibility and observability work that is hard to justify for a public-facing API.
Apollo Server or Strawberry, persisted queries at the gateway, graphql-query-complexity for cost limits, DataLoader for N+1, and OpenTelemetry on every resolver. If you cannot commit to those five, stay on REST.