Software Development

REST vs GraphQL: Lessons From Migrating One API (and Watching Another Go Wrong)

I have been through one REST-to-GraphQL migration that made genuine sense, and I have watched a team spend four months on a GraphQL setup that ended up being abandoned. Here is my honest read on when each architecture actually serves you.

Norehan Norrizan
··11 min read

I will lead with the conclusion: REST and GraphQL are not competitors in the way the internet sometimes makes them out to be. They solve slightly different problems, and the teams that struggle with GraphQL are usually teams that adopted it before understanding what problem they were trying to solve.

The REST-to-GraphQL migration I was involved in was for a product with five different client types — a web app, a mobile app, an internal tooling frontend, a partner API, and a data export integration. Every client needed different slices of the same underlying data. REST meant either over-fetching (heavy endpoints that returned everything) or maintaining many custom endpoints. GraphQL genuinely solved that problem.

The one that went wrong was a standard three-tier web application with one frontend and one mobile app. The GraphQL layer added real complexity — resolver code, DataLoader for N+1 batching, a schema to maintain — with no meaningful benefit over well-designed REST endpoints. The team eventually reverted to REST after about four months.

What REST Actually Is (Not What People Think)

REST is frequently misunderstood as "HTTP with JSON". The actual REST constraints — statelessness, uniform interface, resource-based addressing — are more conceptual. In practice, "REST API" has come to mean: HTTP methods (GET, POST, PUT, PATCH, DELETE) on noun-based URLs, returning JSON. This is what most people mean and what I will mean here.

# Clear REST structure — easy to understand, easy to document
GET    /users              → list users
POST   /users              → create user
GET    /users/:id          → get one user
PATCH  /users/:id          → update user
DELETE /users/:id          → delete user
GET    /users/:id/posts    → get posts for a specific user

REST's structural strengths are real and often undersold:

  • HTTP caching works natively. GET requests can be cached by CDNs, proxies, and browsers without any special handling. This is not a small thing — for read-heavy public APIs, it is a massive scalability advantage.
  • Universally understood. Any language, any tool, any third party can consume a REST API. curl, Postman, Python requests, fetch — all work out of the box.
  • Debugging is straightforward. You can paste a URL into a browser. You can inspect network requests directly. The request/response structure is transparent.

What GraphQL Actually Solves

GraphQL's core innovation is that the client specifies the shape of the data it needs, rather than the server deciding what to return. This sounds minor until you are in a situation where different clients have genuinely different data requirements.

# Mobile app needs minimal data (bandwidth matters)
query MobileUserCard {
  user(id: "42") {
    name
    avatarUrl
  }
}

# Desktop app needs rich data for the same user
query DesktopUserProfile {
  user(id: "42") {
    name
    avatarUrl
    email
    joinDate
    posts(first: 10) { title publishedAt commentCount }
    settings { theme notifications }
  }
}

In REST, serving both clients cleanly typically means either a heavy "kitchen sink" endpoint (mobile over-fetches) or two separate endpoints (maintenance cost doubles). GraphQL handles this elegantly with one endpoint and one schema.

The other genuine strength is schema introspection. GraphQL APIs are self-documenting in a way that REST APIs require explicit tooling (OpenAPI/Swagger) to achieve.

GraphQL's Real Costs

The costs are real and not always discussed honestly in the GraphQL advocacy literature:

  • HTTP caching does not apply. GraphQL queries typically use POST. Caching requires Automatic Persisted Queries (APQ) or a CDN that understands your query structure. This adds complexity that REST simply does not have.
  • N+1 queries move to the server. When a GraphQL query asks for a list of posts and each post's author, the naive resolver implementation issues N+1 database queries. You need DataLoader or equivalent batching. This is solvable but not free.
  • The schema is a maintained surface area. As your data model evolves, so does the schema. Deprecating fields, managing breaking changes, ensuring backwards compatibility — this is ongoing work that REST versioning handles differently (not necessarily better, but differently).
  • File uploads are awkward. They are not part of the spec and require non-standard handling via multipart form data.

My Decision Framework

When I am deciding for a new project, I ask two questions:

1. Do I have multiple clients with meaningfully different data needs? If yes, GraphQL is worth the complexity investment. If no — if I have one web app and maybe a mobile app with largely similar data requirements — REST is almost certainly the right choice.

2. Is public caching important? If I am building a public API or a content-heavy site where CDN caching of GET requests provides real scalability benefit, REST wins. Full stop.

The honest answer for most early-stage or small-team products: start with REST. The flexibility of GraphQL has a carrying cost that is not worth paying until you actually need it. Premature GraphQL adoption is one of the more reliable ways I have seen teams slow themselves down.

The Hybrid Approach

Many production systems use both. GitHub exposes both a REST API (v3) and a GraphQL API (v4). Shopify's storefront is GraphQL; its admin API has both. This is not indecision — it is the right answer for systems where different surfaces have genuinely different requirements. There is no rule that says you have to pick one.

Further Reading

Filed under

RESTGraphQLAPIarchitecturebackend