REST, GraphQL, and gRPC solve the same problem, moving data between services, in three very different ways. Here is when each wins, the trade-offs nobody markets, and how they happily coexist in one system.
You can build an API, but every project picks a different style and you are not sure why. You have heard 'GraphQL fixes over-fetching' and 'gRPC is faster' and want the honest version, what each one actually trades away, and when to reach for which. No prior GraphQL or gRPC experience assumed.
Every API does the same job: a client asks for data or an action, a server answers. REST, GraphQL, and gRPC are three different contracts for that conversation. They are not ranked, there is no 'best' one. They are tuned for different callers, different networks, and different teams. Pick by the shape of your problem, not by what trended last quarter.
REST gives you a menu of resources. GraphQL gives you a kitchen and a language to order from it. gRPC gives you a private phone line to a specific colleague who already knows your shorthand.
An analogy: three ways to order lunch
Before any code, anchor the three styles to something physical. The difference between them is really a difference in *who shapes the request* and *how tightly the two sides agreed in advance*.
A fixed menu, each dish has its own number, you order item #12, you get exactly item #12REST: each resource has its own URL; GET /users/12 returns the user resource, no more, no less
A build-your-own-bowl counter, you describe the exact bowl you want and get back precisely that, nothing extraGraphQL: one endpoint, the client writes a query describing the exact fields it needs and gets that shape back
A phone order to a regular in a shared shorthand, fast, terse, both sides already memorised the codesgRPC: a strongly-typed contract compiled into both client and server; calls are compact binary over a persistent connection
Who shapes the request and how much was agreed up front is the whole story.
The picture: one client, three back-ends
In a real system you rarely choose only one. A typical setup puts a gateway in front: the browser or mobile app talks one friendly protocol to the edge, and the gateway fans out to whichever style each downstream service speaks.
A client hits one gateway; the gateway speaks REST, GraphQL, and gRPC to the services behind it.
1
Client makes one request
The app sends a single HTTPS call to the gateway, it never needs to know how many services live behind it.
2
Gateway authenticates and routes
It validates the token once, then decides which downstream service (and which protocol) serves this path.
3
Fan-out to the right style
Cacheable catalog reads go to a REST service; a screen needing many joined fields hits the GraphQL server; latency-sensitive internal calls use gRPC.
4
Compose and return
The gateway stitches the responses into one payload shaped for the client and sends it back.
This is the key mental shift: the three styles are not competitors fighting for your whole system. They are tools for different *edges* of it. See API gateways and the edge for how this front door is built.
The big comparison
Here is the whole debate on one screen. Read each row as 'what does this style optimise for, and what does it cost me?'
Dimension
REST
GraphQL
gRPC
Transport
HTTP/1.1 or 2, JSON
HTTP, JSON, one POST endpoint
HTTP/2, binary protobuf
Schema / typing
Optional (OpenAPI)
Strong, introspectable schema
Strong, contract-first .proto
Fetching
Fixed per endpoint
Client picks exact fields
Fixed per RPC method
Caching
Easy, native HTTP caching
Hard, POST, custom layer
Hard, not HTTP-cacheable
Streaming
Limited (SSE, chunked)
Subscriptions (extra setup)
First-class bi-directional
Browser support
Native, trivial
Native, trivial
Needs grpc-web + proxy
Best for
Public APIs, CRUD, caching
BFF, rich client screens
Internal microservice calls
No row makes one style 'win', each is a deliberate trade.
Read the caching row twice
REST's biggest quiet advantage is that the entire web, CDNs, browsers, proxies, already knows how to cache a GET by URL. GraphQL and gRPC throw that away and make you rebuild caching yourself. For high-read public traffic, that single fact often decides the choice.
Same request, three dialects
Nothing makes the difference concrete like one task in all three styles. The task: get a user with their orders. Watch how the request shape and the contract change.
REST: two resources, fixed shapes
http
GET /users/42 HTTP/1.1
Host: api.shop.com
Accept: application/json
# Response: the full user resource (every field, fetched or not)
{
"id": 42,
"name": "Ada",
"email": "ada@shop.com",
"createdAt": "2026-01-04T10:00:00Z"
}
# Orders are a separate resource, a second round trip
GET /users/42/orders HTTP/1.1
REST is dead simple to read, cache, and debug, but you get the *whole* user even if you wanted only the name, and you pay two round trips. That is over-fetching (too many fields) and under-fetching (too few resources per call) in one example.
GraphQL: one call, exactly the shape you asked for
graphql
query {
user(id: 42) {
name
orders(last: 3) {
id
total
}
}
}
# Response mirrors the query exactly, no extra fields, one round trip
{
"data": {
"user": {
"name": "Ada",
"orders": [
{ "id": "o_91", "total": 49.0 },
{ "id": "o_88", "total": 12.5 }
]
}
}
}
One request, only the fields the screen needs, joined across user and orders. Over- and under-fetching solved. The cost shows up elsewhere, see the N+1 warning below.
gRPC: a typed contract compiled into both sides
protobuf
syntax = "proto3";
package shop.v1;
service UserService {
rpc GetUserWithOrders(GetUserRequest) returns (UserWithOrders);
}
message GetUserRequest {
int64 user_id = 1;
int32 last_orders = 2;
}
message Order {
string id = 1;
double total = 2;
}
message UserWithOrders {
int64 id = 1;
string name = 2;
repeated Order orders = 3;
}
You define the contract once in a .proto file, then generate type-safe clients and servers in every language. Calls travel as compact binary over a reused HTTP/2 connection, fast and strict, but opaque to a casual curl and awkward from a browser. For how a contract like this evolves without breaking callers, see API design, evolution, and versioning.
The two sharpest gotchas
Each of the newer styles has one trap that bites teams hard in production. Know them before you commit.
GraphQL N+1: the resolver that quietly hammers your DB
A query for 'users and each user's orders' can naively run 1 query for the users, then 1 more per user for their orders, N+1 queries. The fix is a **DataLoader**: it batches all the per-user lookups within a tick into a single query and caches them. GraphQL does not give you this for free; you must wire batching into your resolvers or your 'one elegant query' becomes hundreds of DB hits.
typescript
import DataLoader from"dataloader";
// Batches every orders-by-userId lookup in one tick into ONE queryconst orderLoader = newDataLoader(async (userIds: readonly number[]) => {
const rows = await db.orders.findByUserIds(userIds);
// Return results in the SAME order as userIds (DataLoader's contract)return userIds.map((id) => rows.filter((r) => r.userId === id));
});
const resolvers = {
User: {
orders: (user: { id: number }) => orderLoader.load(user.id),
},
};
gRPC in the browser: it does not just work
Browsers cannot speak raw gRPC, they do not give JavaScript the low-level control of HTTP/2 frames that gRPC needs. You need **grpc-web**, a variant that runs through a proxy (Envoy or a sidecar) translating between the browser and your gRPC backend. It works, but it is extra moving parts and it loses client-streaming. This is why gRPC's sweet spot is service-to-service, not service-to-browser.
How to choose
Skip the religion. Walk down this list and stop at the first match, it is right far more often than agonising over benchmarks.
Public API for third parties, or heavy cacheable reads? Reach for REST. Everyone knows it, every tool speaks it, and HTTP caching is free money.
One client (web/mobile) pulling many fields from many sources per screen? Reach for GraphQL, usually as a BFF (backend-for-frontend). It kills over- and under-fetching and lets the front end evolve without new endpoints.
Internal service-to-service calls where latency and strict typing matter? Reach for gRPC. Compact binary, HTTP/2 multiplexing, generated clients, and real streaming.
Need real-time bidirectional streams (chat, telemetry, live updates) between services?gRPC streaming is purpose-built; GraphQL subscriptions or SSE are the browser-facing alternatives.
Small team, simple CRUD, ship today?REST. Do not add a query language or a protobuf toolchain to a problem that a dozen endpoints solve.
They coexist, that is the norm
Mature systems use all three: REST at the public edge, GraphQL as the BFF for the app, gRPC humming between internal services. The gateway is what lets each style live where it is strongest. Choosing one for the whole company is usually a mistake.
Common mistakes that cost hours
Picking GraphQL to look modern, then re-implementing caching and rate-limiting by hand. You gave up free HTTP caching; make sure the flexibility was worth it.
Ignoring the N+1 problem until the DB melts under load. Add DataLoaders from day one in any resolver that fans out to children.
Exposing gRPC to browsers without grpc-web and a proxy, then wondering why nothing connects. Use REST or GraphQL at the browser edge; keep gRPC internal.
Treating REST as 'no schema needed'. Untyped JSON drifts. Write an OpenAPI spec so clients and tests have a contract, see REST API design.
Letting a GraphQL query depth or cost go unbounded. A single nested query can become a denial-of-service. Add depth limits and query cost analysis.
Versioning a gRPC contract by editing field numbers. Field tags are forever, only add new ones; never renumber or reuse, or you silently corrupt old clients.
Takeaways
The whole article in seven lines
All three move data; they differ in who shapes the request and how strictly both sides agreed up front.
REST: resources + HTTP verbs, simple, and the only one with free web-wide caching, ideal for public APIs and cacheable reads.
GraphQL: one endpoint, client-shaped queries that solve over/under-fetching, ideal as a BFF, but you own caching and the N+1 risk (DataLoader).
gRPC: HTTP/2 + protobuf, fast, strongly typed, real streaming, ideal internal, but needs grpc-web for browsers.
Caching is REST's quiet superpower; flexibility is GraphQL's; speed and typing are gRPC's.
Choose by problem shape, not hype, and expect to use all three behind one gateway.
Whatever you pick, write down the contract: OpenAPI, a GraphQL schema, or a .proto.
Where to go next
You now have a decision framework. Deepen each branch with these, then practise designing a contract end to end:
REST API design, resources, status codes, and how to keep a REST surface clean.
Practise in the networking lab to build intuition for HTTP/1.1 vs HTTP/2 under the hood.
Want to go deeper?
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.