Our mobile app was making six separate REST calls just to render a single product page, a header endpoint, a pricing endpoint, a reviews endpoint, an inventory endpoint, and two more for related items and shipping estimates. On a decent connection that was fine. On the 3G test devices our QA team actually used, that page took upward of four seconds to fully populate, and I got tired of explaining to product managers why we couldn't just combine the calls.
I want to be honest that a well-designed REST API with proper composite endpoints could have solved this without touching GraphQL at all. Our problem was years of endpoints added by different teams with no shared philosophy, some returning nested objects, some returning flat IDs you had to resolve separately. GraphQL didn't fix bad API design, it forced us to finally agree on a single schema, which was the real fix wearing GraphQL's clothes.
We used Apollo Server on top of our existing Express services rather than rewriting the backend from scratch. Defining the schema meant sitting in a room with three other engineers for the better part of a week arguing about whether "price" should be a scalar or an object with currency and amount fields. We landed on an object, which felt like overkill at the time and turned out to be exactly right eighteen months later when we added multi-currency support without a breaking schema change.
type Price {
amount: Float!
currency: String!
formatted: String!
}
type Product {
id: ID!
name: String!
price: Price!
reviews(limit: Int = 5): [Review!]!
inventory: InventoryStatus!
}
Our first working version of the reviews resolver looked innocent and performed terribly. For a page listing twenty products, it fired twenty separate database queries for reviews instead of one batched query, because each product resolved its own reviews independently with no awareness of its siblings. I found this by accident watching our database's slow query log spike during a load test, not because anyone flagged it in code review.
I added Facebook's DataLoader library to batch and cache those review lookups within a single request. My first implementation still didn't work because I was instantiating a new DataLoader instance in the wrong scope, at the module level instead of per-request, which meant cached results were leaking between completely unrelated requests from different users. That bug sat in production for eleven days before a support ticket about a user seeing someone else's cached review count made it obvious.
function createLoaders() {
return {
reviewsByProductId: new DataLoader(async (productIds) => {
const reviews = await db.reviews.findByProductIds(productIds);
return productIds.map(id =>
reviews.filter(r => r.productId === id)
);
})
};
}
// instantiate fresh loaders per request, in context()
The mobile team could finally request exactly the fields they needed in a single round trip, and that product page dropped from six requests to one, with load time on our slow test devices falling to under a second and a half. What we didn't anticipate was clients requesting deeply nested queries that were technically valid but computationally expensive, a mobile developer once wrote a query that recursively fetched related products three levels deep, which nearly took down a staging database before we added query depth limiting.
REST's simplicity around HTTP caching, a GET request to a specific URL with standard cache headers, doesn't map cleanly onto GraphQL, where a single endpoint serves wildly different queries. We ended up building response-level caching keyed on a hash of the query and variables, which works but took real engineering effort that HTTP caching gave us for free under REST.
GraphQL solved our specific over-fetching problem well, but it introduced query complexity risks and caching complexity we hadn't budgeted time for honestly during planning. If your actual problem is a handful of poorly designed endpoints, fixing those endpoints directly is probably faster than adopting an entirely new query layer. We made the switch because our problem was systemic across dozens of endpoints and teams, not because GraphQL is inherently superior to REST, and I'd make that same distinction clearly for anyone weighing this decision now.