REST or GraphQL: Which Should You Use?
Use REST by default, GraphQL when your clients justify it. A developer's decision rule, the hidden costs of GraphQL, and a copyable comparison table.
Use REST by default. Reach for GraphQL only when your clients' data needs clearly justify the extra backend complexity. That's not a hedge — it's the actual industry reality. Most production systems still default to REST because it's simpler to build, cheaper to operate, and easier to scale and debug. GraphQL is genuinely powerful, but you pay for that power with complexity you now own and maintain forever. This is one decision inside the larger tech stack decision; here's how I make it.
The core difference
Everything else follows from one architectural distinction:
- REST exposes multiple endpoints (
/users,/orders/123), each returning a fixed shape, riding directly on HTTP verbs and status codes. - GraphQL exposes a single endpoint (
/graphql) and lets the client ask for exactly the fields it wants, nested however it needs, in one request.
Concretely, imagine a screen that needs a user, their recent orders, and product recommendations. With REST that's often three round-trips:
1GET /users/42
2GET /users/42/orders?limit=5
3GET /users/42/recommendationsWith GraphQL it's one request, shaped by the client:
1query DashboardData($id: ID!) {
2 user(id: $id) {
3 name
4 orders(limit: 5) { id total status }
5 recommendations { id title price }
6 }
7}So the real trade is REST = simplicity plus HTTP infrastructure for free, versus GraphQL = flexible, client-shaped fetching plus fewer round-trips. Keep that framing and the decision gets easy.
When REST is the right call (most of the time)
Lean REST, hard, if any of these describe you:
- You're building CRUD APIs — users, products, orders, payments.
- You have one main frontend (a typical web or mobile app).
- You want caching for free via CDNs and standard HTTP cache headers.
- You want easy debugging with curl, Postman, and plain server logs.
- You care about operational simplicity and predictable performance.
The underrated point: REST integrates naturally with the entire HTTP toolchain. A GET /products/123 can be cached at the CDN edge with a single header —
1Cache-Control: public, max-age=3600— and rate limiting, conditional requests (ETag, If-None-Match), and observability tooling all understand it out of the box. GraphQL gets none of that automatically, because to a cache every request is an opaque POST /graphql. You rebuild that infrastructure yourself. On the e-commerce and plugin backends I've shipped — Lightfunnels, the Lumin AI plugins — REST covered the overwhelming majority of endpoints without complaint, and free HTTP caching was a real performance win, not a footnote.
When GraphQL earns its complexity
GraphQL starts making real sense when:
- You have multiple clients — web, native mobile, and partner integrations — each needing a different slice of the same data.
- Your UI renders deeply nested data — dashboards, activity feeds, complex screens that would otherwise fan out into many REST calls.
- You're chronically fighting over-fetching (endpoints returning far more than a screen needs, wasting mobile bandwidth) and under-fetching (needing three calls to paint one view).
- You want the frontend to evolve without waiting on new backend endpoints every time a screen changes.
The mobile dashboard above is the canonical case: four REST round-trips on a flaky mobile connection, or one GraphQL query returning exactly what the screen renders. When you have several clients each wanting a different shape, that flexibility stops being a luxury and starts saving real coordination cost between frontend and backend teams.
The hidden costs nobody warns you about
This is where teams get surprised, so be honest before you commit:
- Caching is harder. You lose the simple HTTP caching model and have to add application-level or normalized client caching.
- Backend complexity goes up. Resolvers, schema design, and type definitions are real, ongoing work.
- The N+1 query problem will bite. A query for 10 orders that each resolve a customer can fire 1 + 10 database queries unless you batch. The fix is a DataLoader-style batching layer:
1// Without batching: resolving `customer` for each order = N+1 queries
2// With DataLoader: all customer lookups in one tick are batched into ONE query
3const customerLoader = new DataLoader(async (ids: readonly string[]) => {
4 const rows = await db.customer.findMany({ where: { id: { in: [...ids] } } });
5 return ids.map((id) => rows.find((r) => r.id === id));
6});
7
8const resolvers = {
9 Order: {
10 customer: (order) => customerLoader.load(order.customerId),
11 },
12};- Security surface. A single deeply nested or recursive query can be extraordinarily expensive. You need query-depth limits and query-cost analysis to prevent a malicious or careless client from taking down the server.
- Everything funnels through one endpoint, which changes how you monitor, log, and rate-limit.
In practice, GraphQL doesn't remove complexity — it moves it from the frontend to the backend. That's sometimes a great trade. Just make it on purpose, not because GraphQL is trendy.
The 3-question decision filter
Start with REST unless you can clearly answer yes to at least one of these:
- Do I need flexible queries from many different clients with genuinely different data needs?
- Do I have significant over-fetching or under-fetching pain today — not hypothetically, but in code I'm shipping now?
- Do I need to aggregate many backend services into one API layer for the frontend?
If none are true, REST is almost always right. And remember it's not binary: a very common modern setup uses REST for public and CRUD endpoints, with GraphQL as a backend-for-frontend (BFF) aggregation layer only where the UI's shape justifies it.
The comparison at a glance
| REST | GraphQL | |
|---|---|---|
| Endpoints | Many, fixed responses | One, client-shaped responses |
| Caching | Free via HTTP / CDN | Manual, application-level |
| Round-trips for nested data | Often several | One query |
| Best for | CRUD, single client, simple ops | Multiple clients, nested data |
| Main risk | Endpoint sprawl, over/under-fetching | N+1, query-cost attacks, backend complexity |
| Debugging | curl / Postman / logs | Specialized tooling (GraphiQL, Apollo) |
| Learning curve | Low | Moderate to high |
Whichever you choose, it's one column of a bigger decision — see the full tech stack guide, and pair this with your backend choice, since where GraphQL's complexity lands depends heavily on whether you're running Node or Django.