REST or GraphQL: which API to choose
How REST and GraphQL differ: fourteen criteria, which approach for which project, one product card through both APIs, what to set up in each and common mistakes.
In short
REST and GraphQL are two ways to give data to a website, an app or another system. REST is a set of addresses, one per resource: simple, cached by any CDN, understood by every tool and the standard for integrations with CRMs, payments and partners. GraphQL is one address and a schema of types: the client asks for exactly the fields it needs in one request, which is convenient for complex screens and many different clients, but caching, error handling and load protection have to be built separately. For most sites, shops and integrations REST with an OpenAPI description is enough; GraphQL pays off when there are many clients with different data needs.
In short: which one to choose
Look at who will use the API. If it is your own site, a mobile app with a few screens, a CRM, a payment system or a partner — REST: every developer knows it, every tool supports it, and responses are cached by address. With an OpenAPI description another team connects without calls and letters.
GraphQL is worth its extra setup when many clients need different slices of the same data: a web interface, two mobile apps and a partner portal, each with its own screens. Then one schema replaces dozens of special addresses, and the front-end team stops waiting for the back end to add a field.
- Site, shop, integrations — REST
- Many clients, different data — GraphQL
- Either way — a described contract
REST and GraphQL: a detailed comparison
Fourteen criteria side by side — from the shape of a response to monitoring and protection.
| Criterion | REST | GraphQL |
|---|---|---|
| Model | many addresses, one per resource | one address and a schema of types |
| Shape of the response | decided by the server | decided by the client |
| Extra fields | common | none, only what was asked |
| A complex screen | several requests | one request |
| HTTP caching | by address, CDN out of the box | POST requests, needs its own cache |
| Errors | HTTP status codes | often status 200 with errors inside |
| Contract | OpenAPI, written alongside | the schema itself |
| Changes | new fields or a /v2/ version | new fields, old ones @deprecated |
| File upload | simple | usually a separate REST address |
| Load protection | limits per address | limits on query depth and cost |
| Monitoring | by address in any log | by operation name, needs setup |
| Client | any HTTP client, even curl | any, but a library is more convenient |
| Integrations with CRMs and payments | the standard, plus webhooks | rare |
| Entry bar | low | higher |
Which approach for which project
Ten typical projects with a recommendation and the reason.
| Project | Take | Why |
|---|---|---|
| Site or shop with its own front end | REST | a few clear addresses, cached by CDN |
| Exchange with a CRM or accounting | REST | the other side expects REST and webhooks |
| Payments | REST | payment systems work through REST and signed webhooks |
| Public API for partners | REST | connects with any tool, described in OpenAPI |
| Telegram bot or Mini App | REST | a few addresses are enough |
| Mobile app with several screens | REST | simpler, unless the screens are very different |
| Web, iOS, Android and a partner portal | GraphQL | each client takes its own slice of one schema |
| Dashboard with many widgets | GraphQL | one request instead of dozens |
| A headless CMS that already offers GraphQL | GraphQL | use what the system gives |
| Files and large exports | REST | streams and uploads are native to it |
One product card, two APIs
The same data — a product and its reviews — through REST and GraphQL. Both ran on one test server; the responses in the comments are copied from real requests.
REST: the contract
Two addresses described in OpenAPI 3.1; the file passes the Redocly validator.
# REST: the contract is described in OpenAPI — another team connects from it
openapi: 3.1.0
info:
title: Shop API
version: 1.0.0
servers:
- url: https://shop.example.com
security: [] # public catalogue, no key needed for reading
paths:
/api/products/{sku}:
get:
operationId: getProduct
summary: One product with all its fields
parameters:
- { name: sku, in: path, required: true, schema: { type: string } }
responses:
"200":
description: The product
content:
application/json:
schema: { $ref: "#/components/schemas/Product" }
"404":
description: No such product
/api/products/{sku}/reviews:
get:
operationId: getProductReviews
summary: Reviews of the product — a separate request
parameters:
- { name: sku, in: path, required: true, schema: { type: string } }
responses:
"200":
description: The reviews
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/Review" }
"404":
description: No such product
components:
schemas:
Product:
type: object
required: [sku, title, price, stock, description]
properties:
sku: { type: string }
title: { type: string }
price: { type: integer }
stock: { type: integer }
description: { type: string }
Review:
type: object
required: [author, rating]
properties:
author: { type: string }
rating: { type: integer, minimum: 1, maximum: 5 }
REST: the requests
The card needs two requests, and the first brings fields the page does not use.
# REST: a product card with ratings takes two requests,
# and the first returns every field even if the page needs only the title
curl https://shop.example.com/api/products/A-100
# {"sku":"A-100","title":"Oak table","price":24000,"stock":3,
# "description":"Solid oak, 160 × 90 cm, oil and wax finish"}
curl https://shop.example.com/api/products/A-100/reviews
# [{"author":"Anna","rating":5},{"author":"Mark","rating":4}]
GraphQL: the schema
The schema is both the contract and the documentation; reviews are a field of the product.
# GraphQL: one schema, one address — the client chooses the fields
type Product {
sku: String!
title: String!
price: Int!
stock: Int!
description: String!
reviews: [Review!]!
}
type Review {
author: String!
rating: Int!
}
type Query {
product(sku: String!): Product
}
GraphQL: the request
One request, only the needed fields. The error for an unknown field came with HTTP status 200.
# GraphQL: one POST request to /graphql — the client lists exactly the fields it needs
query ProductCard {
product(sku: "A-100") {
title
reviews {
rating
}
}
}
# Answer:
# {"data":{"product":{"title":"Oak table","reviews":[{"rating":5},{"rating":4}]}}}
# A field that is not in the schema is rejected before execution:
# Cannot query field "color" on type "Product".
A REST API that is easy to connect to
Most complaints about REST are about a poorly designed API, not about REST. Six rules that remove them.
-
01
OpenAPI from day one
The description is the contract: documentation, tests and client code are built from it.
-
02
Field selection
?fields=title,priceremoves the main argument for GraphQL — extra data. -
03
Related data on request
?include=reviewsreturns the product with its reviews in one response. -
04
Honest status codes
404, 409, 422 and 429 instead of 200 with an error inside — monitoring sees problems by itself.
-
05
Pages and limits
Lists are always paginated, and the client knows its request limit from the headers.
-
06
Signed webhooks
Events go to the other system by themselves, and the signature does not let a stranger fake them.
If GraphQL: what to set up from the start
GraphQL hands the client a lot of freedom. These settings keep it from turning against the server.
-
01
Depth and cost limits
Without them one nested request can load the database as much as thousands of ordinary ones.
-
02
Batching against N+1
DataLoader collects the reviews of fifty products into one query instead of fifty.
-
03
Persisted queries
Known queries go by a hash — they can be cached and nobody sends arbitrary ones.
-
04
Operation names in logs
All requests go to one address, so only the name shows which one is slow.
-
05
Error codes inside
Errors carry a code in
extensions, and monitoring reads the body, not only the status. -
06
Rights on fields
Access is checked in every resolver — one schema is open to every client.
Common mistakes when choosing
-
GraphQL for one site
A schema, resolvers and limits for one client that would get by with five addresses.
-
REST without a description
The other team learns the API by trial and error and by letters.
-
Trusting status 200
In GraphQL a response with errors often comes as 200 — monitoring by status misses it.
-
An open GraphQL without limits
One crafted request is enough to stop the server.
-
An address for every screen
REST turns into dozens of special endpoints — field selection would have been enough.
-
Choosing by fashion
The partners, the CRM and the payment system decide what the API must speak.
Questions about REST and GraphQL
Is GraphQL replacing REST?
No: REST remains the standard for integrations and public APIs, GraphQL holds its place in products with many clients.
Which is faster?
GraphQL saves requests on complex screens; REST wins on caching. Speed depends more on the database.
Can they be used together?
Yes, often: GraphQL for the product’s interfaces, REST and webhooks for partners and payments.
What is OpenAPI?
A standard description of a REST API: addresses, parameters and responses. Documentation and clients are generated from it.
Does GraphQL need a special client?
No, it is an ordinary POST with JSON; libraries such as Apollo or urql add a cache and convenience.
Is GraphQL secure?
As secure as its settings: depth limits, rights on fields and persisted queries are mandatory.
What about gRPC?
It is for exchange between internal services; browsers and partners still get REST or GraphQL.
Online form
Discuss
the API
I build REST APIs described in OpenAPI, with signed webhooks — another system connects from the documentation. Tell me about the project — I answer within one working day.