GraphQL vs REST — N+1 Query That Took Down a Product Page
When a GraphQL product page slowed from 200ms to 8 seconds due to N+1 queries, the DataLoader fix proved crucial—and REST avoids the problem entirely..
20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.
- REST organizes APIs around resources and HTTP verbs — predictable, cacheable, and universal.
- GraphQL lets clients declare exact data shapes — solves overfetching and underfetching but adds complexity.
- Choose REST when you have few clients, simple CRUD, or need aggressive HTTP caching.
- Choose GraphQL for multiple client types, rapid iteration, or mobile-heavy apps.
- The hybrid pattern — GraphQL at the edge, REST internally — is what senior teams actually run in production.
Imagine you're at a restaurant. With REST, there's a fixed menu — you order the 'burger meal' and you get a burger, fries, AND a drink, even if you only wanted the burger. With GraphQL, you tell the waiter exactly what you want: 'just the burger, no fries, add extra cheese.' REST gives you predefined plates; GraphQL lets you build your own plate every time. Neither is wrong — it just depends on whether your customers always want the same thing.
Every modern app lives or dies by its API. Whether you're building a mobile app that has to load fast on a 3G connection in rural Brazil, or a dashboard that needs to stitch together data from five different services, the API architecture you choose on day one will haunt you — or reward you — for years. GraphQL and REST are the two dominant players in that space, and the choice between them is one of the most hotly debated decisions in backend engineering.
REST has been the industry standard for over two decades. It's predictable, HTTP-native, and every developer on earth has used it. But as UIs grew more complex and mobile clients became first-class citizens, REST started showing its cracks. Teams were hitting endpoints and getting mountains of data they didn't need, or worse, firing off six requests just to render a single screen. GraphQL was Facebook's answer to that pain, built internally in 2012 and open-sourced in 2015.
By the end of this article you'll be able to clearly explain the structural difference between REST and GraphQL, identify the specific scenarios where each one wins, spot the classic mistakes teams make when choosing between them, and walk into a system design interview with a confident, nuanced answer instead of a vague 'it depends.'
Why GraphQL and REST Are Not Interchangeable
GraphQL and REST are both API query paradigms, but they differ fundamentally in how they fetch data. REST exposes a set of endpoints, each returning a fixed structure. GraphQL exposes a single endpoint and lets the client specify exactly which fields it needs. This sounds like a minor difference, but it changes the entire data-fetching model from server-driven to client-driven.
In practice, REST works well when the client's data needs align with the server's predefined resources. GraphQL shines when clients have varied or nested data requirements — it eliminates over-fetching (getting 20 fields when you need 3) and under-fetching (needing multiple round trips to assemble a view). However, GraphQL shifts complexity to the server: a single query can trigger dozens of database calls if resolvers are naive, leading to the infamous N+1 problem.
Use REST when your API is simple, cache-friendly, and clients are predictable. Use GraphQL when you have multiple clients (web, mobile, IoT) with divergent needs, or when you need to aggregate data from multiple services. The choice isn't about which is 'better' — it's about which trade-offs your system can absorb.
How REST Actually Works — and Where It Starts to Break
REST (Representational State Transfer) organizes your API around resources. A resource is a noun — a User, an Order, a Product. You expose that resource at a URL, and HTTP verbs tell the server what to do with it: GET to read, POST to create, PUT/PATCH to update, DELETE to remove. This mapping is clean, intuitive, and stateless by design.
The trouble starts when your UI needs don't map neatly onto individual resources. Say you're building a Twitter-style profile page. You need the user's name and avatar, their last three tweets, and their follower count. In REST, that's three separate endpoints: GET /users/:id, GET /users/:id/tweets, GET /users/:id/followers. Three round trips. On mobile, each round trip is expensive.
The other side of that coin is overfetching. GET /users/:id might return 40 fields — date of birth, billing address, preferences, internal flags — when your profile page only needs four of them. You're downloading data you'll throw away, every single time.
These two problems — underfetching (too few fields, too many requests) and overfetching (too many fields, wasted bandwidth) — are not bugs in REST. They're structural consequences of organizing an API around fixed resource shapes instead of around what the client actually needs.
How GraphQL Solves Overfetching — and the Trade-offs It Introduces
GraphQL flips the model on its head. Instead of the server deciding what data a response contains, the client declares exactly what it needs in a query. The server exposes a single endpoint (typically POST /graphql) and a typed schema that describes every piece of data available. The client sends a query document describing the shape it wants, and the server returns exactly that shape — nothing more, nothing less.
That same profile page that needed three REST requests? One GraphQL query handles it. The client asks for user name, avatar, their last three tweets, and follower count in a single request. The server resolves all of it and returns one response shaped exactly like the query.
But GraphQL isn't a free lunch. That flexibility comes with real costs. Caching gets harder — REST leverages HTTP caching at the URL level natively, while GraphQL's single endpoint makes URL-based caching useless by default (you need tools like Apollo Client's normalized cache or persisted queries). Query complexity is another concern: a malicious or poorly-written client can craft a deeply nested query that brings your database to its knees. You need query depth limiting and complexity analysis in production. The learning curve for schema design and resolvers is also steeper than standing up REST routes.
Real-World Decision Framework — When to Actually Pick Each One
The honest answer isn't 'GraphQL is better' or 'REST is simpler.' The right answer depends on the shape of your problem. Here's how senior engineers actually think about this decision.
Choose REST when your data model is simple and resource-oriented, your clients are few and controlled (e.g., internal services talking to each other), you need aggressive HTTP caching (CDN caching of GET endpoints is trivially easy with REST), or your team is small and you want zero-overhead tooling. Public APIs that third-party developers will consume are also often better served by REST — it's more universally understood and doesn't require GraphQL client libraries.
Choose GraphQL when you have multiple client types with different data needs — a mobile app, a web app, and a third-party integration all hitting the same backend. It's the perfect fit for a BFF (Backend for Frontend) layer. Also choose it when your product is iteration-heavy: adding a new field to a GraphQL schema is non-breaking by default, whereas adding a new REST endpoint requires versioning discussions. Rapid product teams love this.
The hybrid approach is increasingly common: REST for simple CRUD microservices talking to each other, GraphQL at the edge as an API gateway that stitches them together for clients. This is the architecture used by major platforms like GitHub (which offers both APIs) and Shopify.
The Hybrid Architecture — GraphQL Gateway + REST Microservices
You don't have to pick one. The most mature production architectures use both: REST between internal microservices and GraphQL at the edge. Here's why that pattern wins.
Internal services talk to each other over REST (or gRPC). These calls are simple, cacheable at the service proxy level, and don't benefit from GraphQL's flexibility because each service has exactly one consumer. Adding GraphQL between internal services adds schema overhead and latency with no payoff.
At the edge, a GraphQL gateway sits between your clients and your internal services. The gateway resolves queries by making REST calls to the backend services. Clients get the benefits of GraphQL — declarative data fetching, reduced overfetching, single round trip — while your internal architecture stays simple and decoupled.
This pattern is what GitHub, Shopify, and Netflix run in production. GitHub's v4 GraphQL API is a gateway that aggregates data from over 200 internal REST services. Clients never know. They just send a query and get back exactly what they need.
Production Considerations — Caching, Security, Monitoring
Both REST and GraphQL have production pitfalls that you won't find in tutorials. Here's what actually matters when your API is handling millions of requests.
Caching is the biggest practical difference. REST endpoints are trivially cacheable at the CDN layer because each URL is unique. CDNs can cache GET /users/42 for 300 seconds and serve the cached copy to the next thousand requests. GraphQL's single endpoint means every request is a POST with a different query body — CDN caching is useless unless you use persisted queries (where each query has a unique hash ID). If your team cares about CDN cache hit rates, REST wins hands down.
Security: REST benefits from the full HTTP security toolkit — rate limiting by URL, WAF rules per path, IP allowlisting per resource. GraphQL requires you to implement depth limiting, query cost analysis, and allowlisting of known operations. Without these, a single malicious query can DDoS your entire API.
Monitoring: REST’s HTTP status codes map cleanly to error budgets and alerting — 5xx rate goes up, pager goes off. GraphQL returns 200 even for partial failures, so your monitoring must parse response bodies for errors. Most teams miss this and only realize after an incident that their error rate looked fine while users were seeing partial data.
Neither architecture is more secure or more observable by nature — they just require different tooling. The team that plans for these differences will sleep better at night.
The Schema Contract: Why Your Code Editor Becomes Your Best Debugger
REST APIs are basically a gentleman's agreement. You hope the docs are accurate, you pray the endpoint returns what you expect, and you lose a Friday afternoon tracing a missing field. GraphQL forces honesty through a strongly typed schema. That schema isn't just documentation — it's the source of truth your client and server both compile against. When we shipped a GraphQL gateway over twenty REST microservices, the schema caught three data shape mismatches before they hit production. The tooling matters more than you think. IDEs auto-complete queries. Linters reject invalid field requests at build time. You shift failure from runtime to compile time. That changes your entire deployment cadence. The trade-off? Schema design requires upfront discipline. REST lets you throw endpoints together. GraphQL demands you define your data graph before you write resolvers. Most teams underestimate that cost until they're rewriting schemas in month two.
Overfetching Is the Symptom — The Real Disease Is N+1 Queries in GraphQL
Everyone loves to hate REST for overfetching. Fair. But GraphQL introduces a nastier problem: N+1 queries. Your client requests a list of orders, each with line items. One query becomes one-plus-N database calls. Your response time explodes from 50ms to 2 seconds. REST does this too with nested resources, but at least REST forces you to think about endpoints. GraphQL's flexibility makes it too easy to write an innocent-looking query that murders your database. We fixed this with DataLoader — batching and caching per request. But here's the thing: you don't know you have an N+1 problem until it hits production under load. REST's endpoint-per-resource pattern limits the blast radius. GraphQL's single endpoint means one bad query takes down everything. You need query cost analysis, depth limiting, and pagination enforcement. Otherwise, your users trigger an accidental DDoS every time they refresh a dashboard.
The N+1 Query That Took Down a Product Page at 2 PM
- Any resolver that fetches a list of related entities (reviews per product, tweets per user) is a candidate for the N+1 problem.
- Never ship a GraphQL resolver that does a single DB call per parent item without batching.
- Add query depth limiting (max 5 levels) and complexity analysis to every production GraphQL server.
npx apollo studio agent --graph=MyGraph --variants=production trace <query_id>tail -f /var/log/app/graphql.log | grep 'resolver' | awk '{print $NF}' | sort | uniq -c | sort -rnKey takeaways
Common mistakes to avoid
5 patternsUsing GraphQL for microservice-to-microservice communication
Ignoring the N+1 query problem in GraphQL resolvers
Assuming REST endpoints always need versioning
Not implementing query depth limits or cost analysis on GraphQL
Treating GraphQL error handling like REST error handling
Interview Questions on This Topic
You're designing the API layer for an e-commerce platform with an iOS app, an Android app, a web storefront, and a third-party affiliate integration. Each client needs different subsets of product data. Walk me through how you'd decide between REST and GraphQL, and what architecture you'd land on.
Frequently Asked Questions
20+ years shipping large-scale distributed systems. Notes here come from systems that actually shipped.
That's Architecture. Mark it forged?
8 min read · try the examples if you haven't