Migrating from REST to GraphQL: What We'd Do Differently

A client's mobile team was driving a REST-to-GraphQL migration for a good reason: their REST endpoints were badly over-fetching, a single mobile screen needed data from six different REST calls, some returning far more fields than the screen actually used. GraphQL's single endpoint and field-level querying solved that cleanly — the mobile app's initial dashboard load went from six requests to one, and the response payload shrank because clients only requested fields they'd render.
What we underestimated was caching. REST's caching story leans on HTTP semantics — cache by URL, respect Cache-Control headers — and that mostly worked for free with a CDN in front of the API. GraphQL's single endpoint with POST requests breaks that model entirely, since there's no URL to key a cache on. We ended up adopting Apollo Client's normalized cache on the frontend and persisted queries on the backend, which took real implementation time we hadn't budgeted for in the original estimate.
The N+1 query problem hit us on day one of the new schema going live. A query for a list of orders, each with a nested customer field, was firing one database query per order to resolve the customer relationship, turning a page load into 50 separate queries instead of one join. DataLoader solved it with per-request batching and caching, but it's not something GraphQL prevents by default — it's something you have to deliberately build in, and we've since made it a non-negotiable part of any resolver that touches a relationship field.
Error handling needed rethinking too. REST's HTTP status codes gave us free, standardized error semantics — a 404 clearly means not found, a 403 clearly means forbidden. GraphQL typically returns 200 for everything, with errors nested in the response body, which meant our frontend error-handling code, written with REST assumptions, silently treated GraphQL errors as successful responses for the first few weeks after launch until we caught it in a support ticket.
If we were starting the same migration today, we'd build the caching layer and DataLoader batching before the first resolver ships, not after production traffic exposes the gaps. GraphQL delivered on reducing over-fetching, but it traded that problem for two others — caching and N+1 queries — that needed just as much deliberate engineering as the REST problems it replaced.

