GraphQL vs REST API: A Practical Decision and Implementation Guide
A step-by-step guide to choosing and running GraphQL and REST APIs together, with caching, errors, and testing.
By the end of this guide, you will have a clear rule set for whether a new API surface should be REST, GraphQL, or a GraphQL aggregation layer over REST, plus the guardrails that keep the choice from breaking in production. You need only a list of current client screens or partners, rough traffic numbers, and a map of how your data entities connect.
- Audit client requests and network patterns. Count how many round trips each screen or partner needs and whether the returned payloads waste bytes. A mobile home screen that calls
/user,/orders, and/productsin sequence is under-fetching round trips; a dashboard that receives forty fields when it needs three is over-fetching. These numbers, not the technology's marketing, decide where GraphQL pays for itself.
- Route cacheable reads to REST. Any high-traffic, public, read-heavy resource where the URL can be the cache key—
GET /products/123,GET /orders/1001—should stay REST. HTTP caching, ETag, and CDN edge rules work because GET URLs are transparent to caches; a GraphQLPOST /graphqlendpoint is opaque by default.
- Put GraphQL on top as the aggregation layer. Use it where one client needs nested, interconnected data in a single request, such as a user plus their last three orders plus each order's line items. The GraphQL server can call REST services, databases, or other backends. The detail that decides success: expose only the fields clients actually use and reject arbitrary query strings.
- Replace raw GraphQL queries with persisted queries for any public or CDN-fronted API. Store query text server-side and send
GET /graphql?id=a1b2c3. This restores URL-level caching and blocks attackers from running arbitrary nested queries. Zalando, for example, disables raw GraphQL in production and accepts only hash-identified persisted queries.
- Enforce query depth, complexity limits, and DataLoader. Without them, a client can request
user -> friends -> friends -> posts -> commentsand hammer the database. Depth and complexity limits reject expensive queries at the edge; DataLoader batches resolver lookups inside one request and removes the N+1 problem.
- Handle status codes and errors per protocol. REST should return 404 for missing resources and 400/401/403/500 where the semantics expect them. GraphQL historically returns 200 OK even with an
errorsarray, though the newer GraphQL-over-HTTP spec allows 4xx for request errors. Whichever you use, tests must assert both status and body shape.
- Test differently. For REST, assert verbs, paths, and status codes first; then check field presence. For GraphQL, assert both that
datacontains the expected fields and thatbody.errorsis absent, because a 200 response can hide resolver failures.
- Version REST with URL versions and evolve GraphQL with deprecations. Use
/v1/ordersand/v2/ordersfor breaking REST changes. In GraphQL, deprecate fields, track production usage through the persisted-query registry, and remove fields only after they are unused.
Common mistakes:
- Using GraphQL for a simple CRUD admin and giving up free HTTP caching.
- Treating a 200 OK from GraphQL as proof the query succeeded without checking
body.errors. - Exposing raw GraphQL to the public internet without depth limits or persisted queries.
- Trying to cache GraphQL
POSTrequests and wondering why the CDN returns the same payload for every operation. - Adding a new REST endpoint every time a front-end screen changes instead of using GraphQL for the client slice.
- Migrating all endpoints to GraphQL because a vendor blog called it the future.
People also search for
Discussion 0
Nothing has been said yet. Start it.
Log in to join the discussion