Skip to main content
All posts

Tutorials · April 10, 2025 · 5 min read

REST to GraphQL: What the Switch Actually Costs

GraphQL solves one real problem well and introduces two new ones. Where the work goes when you move it off the client, and how to tell whether your API needs any of it.

I built REST APIs for years before I used GraphQL properly, and the switch was less dramatic than the posts advocating for it suggest. GraphQL solves one real problem well, introduces two new ones, and moves a third somewhere less obvious. It is worth understanding which is which before you commit a team to it.

The problem it actually solves

Take a dashboard that needs a user, their recent orders, and some recommendations. With REST you make three calls:

GET /api/user/123
GET /api/user/123/orders?limit=5
GET /api/user/123/recommendations

Three round trips, serialised if any of them depends on the previous one, and each endpoint returns whatever its designer decided was useful rather than what this particular screen needs. On a desktop connection that is invisible. On a phone on a bad network it is the difference between a screen that feels instant and one that does not.

Note that this is not the N+1 problem, despite how often it gets called that. N+1 is a server-side issue about how many database queries you issue while resolving one request, and GraphQL makes it worse rather than better. More on that below.

The GraphQL version is one request that describes exactly what the screen needs:

query GetUserDashboard($userId: ID!) {
  user(id: $userId) {
    name
    email
    avatar
  }
  recentOrders(limit: 5) {
    id
    total
    status
  }
  recommendations {
    productId
    name
    price
  }
}

The client decides the shape of the response. That is the whole idea, and everything good about GraphQL follows from it.

The schema as a contract

The second benefit is organisational rather than technical. A typed schema is a written agreement between the people building the frontend and the people building the backend, and it can be checked by a machine. Frontend work can start against the schema before any resolver exists. Introspection means the API documents itself, and the tooling can generate client types from it, so a field rename becomes a compile error rather than a runtime surprise.

Versioning changes shape too. Instead of standing up /api/v2/users alongside /api/v1/users and maintaining both, you add fields and deprecate old ones:

type User {
  id: ID!
  name: String!
  email: String!
  phone: String @deprecated(reason: "Use primaryContact instead")
}

This is genuinely better than REST versioning, with the caveat that deprecated fields tend to live forever unless someone owns removing them. The tooling tells you who is still querying a field. It does not make them stop.

Where the cost goes

The work does not disappear when you move to GraphQL, it moves from the client into the resolvers, and it gets harder to see.

The real N+1 problem lives here. A query asking for fifty orders and the customer on each one will, in a naive implementation, run one query for the orders and fifty more for the customers. Nothing about the request looks expensive. The fix is batching with something like DataLoader, which collects the individual customer lookups within a single tick and issues one query instead:

const customerLoader = new DataLoader(async (ids) => {
  const rows = await db.customer.findMany({ where: { id: { in: ids } } });
  const byId = new Map(rows.map((row) => [row.id, row]));
  return ids.map((id) => byId.get(id) ?? null);
});

This is not optional. A GraphQL API without batching will fall over under a query that looks entirely reasonable to the person who wrote it.

Two other costs are worth naming before you commit:

  • Caching gets harder. REST gets HTTP caching for free because a URL identifies a resource. With GraphQL every query is a POST to the same endpoint, so you need normalised client-side caching or persisted queries to get any of it back.
  • A public GraphQL endpoint is a query engine you have exposed to the internet. Deeply nested queries can be expensive to serve, so you need depth limiting, complexity scoring, or an allowlist of known queries. None of this is difficult, but all of it is work that a REST API simply does not require.

What I would do now

The question is not which is better, it is how many different clients consume the API and how much their needs differ.

A web app, an iOS app and a partner integration all pulling from the same data, each wanting a different slice of it, is exactly the situation GraphQL was designed for. The schema pays for itself the first time you avoid building a bespoke endpoint for one screen.

A single frontend talking to a service you also own is a different situation. There the client-driven query model is solving a coordination problem you do not have, and you are paying the caching and complexity costs for nothing. Plain REST, or typed RPC where the client and server share a type definition, will get you further with less to maintain.

Mixing them is also fine, and more common than the framing of these posts usually admits. File uploads, webhooks and health checks belong on REST endpoints regardless of what the rest of the API looks like.

Let’s connect

I read every message about a role, and I reply. Start with the resume, then reach out however suits you.

2026 © Kobiljon Muhammadov

Bern, Switzerland