The GraphQL vs REST decision is less about which is “better” and more about who calls your API and how. GraphQL gives clients one endpoint where they ask for exactly the fields they need, which is a real win when a web app, a mobile app and partner integrations all want different slices of the same data. REST gives you plain HTTP resources that caches, proxies, monitoring tools and every developer already understand. I build both, and I pick REST more often than people expect. Here is how they compare on the things that actually bite in production.
GraphQL vs REST at a glance
| Area | REST | GraphQL |
|---|---|---|
| Endpoints | One URL per resource (/orders/42) | Usually a single /graphql endpoint |
| Response shape | Fixed by the server | Chosen by the client, per query |
| Over/under-fetching | Common; extra fields or extra round trips | Largely solved by design |
| HTTP caching | Native: GET, Cache-Control, ETag, CDNs | Needs persisted queries or GET requests; mostly client-side caching |
| Errors | HTTP status codes (404, 422, 500) | Usually HTTP 200 with an errors array; partial data possible |
| Versioning | URL or header versions (/v2/) | Evolve one schema; deprecate fields with @deprecated |
| Contract & docs | OpenAPI (optional, can drift) | Strongly typed schema, introspection built in |
| Security surface | Per-endpoint rate limits and auth | Needs depth, complexity and field-level auth controls |
| Server complexity | Low | Higher: resolvers, batching, query cost analysis |
| File upload/download | Straightforward | Awkward; usually handled by a separate REST endpoint or signed URLs |
Over-fetching and under-fetching
This is GraphQL’s headline benefit, and it is real. A mobile screen showing an order list needs the order number, date, total and the first product image. A typical REST API returns the full order object with every field and makes the app call /orders/42/items for each order to get the image. That is over-fetching on the first call and under-fetching on the rest. With GraphQL the client asks for exactly that shape in one request:
query RecentOrders {
orders(first: 20) {
number
placedAt
total { amount currency }
items(first: 1) { product { thumbnailUrl } }
}
}REST can close most of this gap without switching paradigms: sparse fieldsets (?fields=number,total), embedding related resources (?include=items), or a purpose-built endpoint for a screen (the backend-for-frontend pattern). If you have one or two clients, those are usually enough. If you have many clients evolving independently, GraphQL stops you writing a new endpoint for every screen.
GraphQL vs REST API performance and the N+1 problem
GraphQL is not faster by itself. It reduces round trips for the client, but it can easily make the server slower. The classic trap is the N+1 problem: the orders resolver runs one query, then the items resolver runs once per order, and the product resolver once per item. A query that looks innocent becomes hundreds of database calls.
The standard fix is batching with a DataLoader, which collects all the IDs requested during one tick and loads them in a single query:
const productLoader = new DataLoader(async (ids) => {
const rows = await db.query('SELECT * FROM products WHERE id = ANY($1)', [ids]);
const byId = new Map(rows.map(r => [r.id, r]));
return ids.map(id => byId.get(id) ?? null);
});
// resolver
product: (item, _args, ctx) => ctx.loaders.product.load(item.productId)Create loaders per request, never globally, or one user’s cached data can leak into another’s response. I check every new GraphQL API with query logging on, because N+1 is invisible until you count the queries. The database side of that, including spotting N+1 patterns in pg_stat_statements, is covered in fixing PostgreSQL slow queries.
Caching: where REST still wins
REST sits on top of HTTP caching, which is decades of free infrastructure. A GET /products/42 with Cache-Control: public, max-age=300 and an ETag is cached by browsers, CDNs such as Cloudflare or CloudFront, and reverse proxies with no application code at all.
GraphQL requests are typically POSTs to one URL with the query in the body, so none of that works by default. The options are:
- Client-side normalized caches such as Apollo Client or urql, which cache by object type and ID in the app.
- Persisted queries: the client sends a hash instead of the full query, which allows GET requests and makes CDN caching possible.
- Server-side response or resolver caching with Redis, keyed carefully by user and permissions.
All of these work, but each is something you build and maintain. For public, read-heavy data such as a product catalog, plain REST behind a CDN is hard to beat.
Versioning and schema evolution
REST APIs usually version by URL, and running /v1 and /v2 side by side for years is common and tiring. GraphQL’s approach is a single evolving schema: add fields freely, mark old ones @deprecated(reason: "Use placedAt"), watch field-level usage metrics, and remove fields once nobody queries them. That works well when you control the clients or can see their usage. It works less well for public APIs used by third parties you cannot contact, where an explicit version number is clearer.
The strongly typed schema is a genuine advantage for developer experience. Types generated from the schema with GraphQL Code Generator give frontend teams compile-time checks against the API. REST gets close with OpenAPI and generated clients, but only if the spec is kept accurate, which in my experience it often is not.
Security: depth limits, complexity and authorization
A GraphQL endpoint lets clients compose arbitrary queries, including very expensive ones. A deeply nested query like author { posts { author { posts { ... } } } } can bring down a server with a single request. Before a GraphQL API goes public, I put these in place:
- Depth limiting, typically to around 7 to 10 levels, with a library such as
graphql-depth-limit. - Query cost or complexity analysis, rejecting queries above a budget, and rate limiting by cost rather than by request count.
- Pagination limits enforced on every list field (
firstcapped at, say, 100). - Introspection disabled in production for private APIs, or persisted-query allowlists so only known queries run.
- Authorization in resolvers or the business layer, per field and per object, not just at the endpoint. With one endpoint there is no URL-level rule to fall back on.
REST is not automatically secure either, but its attack surface maps onto endpoints, which WAFs, rate limiters and API gateways understand out of the box.
When to use GraphQL, and when REST is the better choice
Use GraphQL when
- Several clients (web, iOS, Android, partners) need different views of the same data.
- The data is a rich graph of related entities and screens combine many of them.
- Frontend teams ship often and you want them unblocked from backend endpoint changes.
- You are putting one API in front of several backend services or databases.
REST is the better choice when
- It is a public API for third-party developers, who expect REST, status codes and HTTP caching.
- The service is simple CRUD, or there is one client that you control.
- Responses are highly cacheable and CDN caching matters for cost or speed.
- You are handling file uploads, downloads, streaming or webhooks.
- The team is small and does not have time to build batching, cost analysis and GraphQL-specific monitoring.
Mixing is normal: a GraphQL API for your own apps, REST for webhooks and public integrations. On tooling, both are mature. For GraphQL in Node.js I usually reach for Apollo Server or GraphQL Yoga; in Python, Strawberry works well alongside FastAPI. For REST, FastAPI generates OpenAPI docs automatically, and Express or Fastify cover the Node.js side through my Node.js development work, or FastAPI development for Python.
When it is worth getting help
If you are designing a new API, or you have a GraphQL API that has become slow or hard to secure, an outside review of the schema, resolvers and database access is usually quick and pays for itself. My GraphQL API development service covers schema design, DataLoader batching, query cost limits and auth, and I will tell you honestly if REST would serve you better. Small reviews and fixes are $20/hour; new APIs are built for a fixed price agreed in writing. See the GraphQL API development page for what that includes.
Frequently asked questions
Is GraphQL faster than REST?
Not inherently. GraphQL can reduce the number of round trips a client makes, which helps on slow mobile networks, but a naive GraphQL server often runs more database queries than an equivalent REST endpoint. With batching and good resolvers the server cost is similar; with HTTP caching, REST is often faster for public read-heavy data.
Is GraphQL replacing REST?
No. GraphQL is widely used for app-facing APIs, but REST remains the default for public APIs, webhooks and service-to-service calls. Most organizations that adopt GraphQL still run REST alongside it.
Can I use GraphQL and REST together?
Yes, and it is common. A frequent pattern is a GraphQL layer for your own web and mobile apps that calls existing REST services underneath, while REST endpoints stay in place for webhooks, file handling and third-party integrations.
Does GraphQL work with any database?
Yes. GraphQL is an API layer, not a database technology. Resolvers can read from PostgreSQL, MongoDB, other APIs or a cache. The database still needs proper indexes and batched access, or the flexibility of GraphQL turns into slow queries.