A typical product has at least three conversations happening at once: a mobile app talking to your backend, two backend services talking to each other, and a third-party developer talking to your public API.
REST, GraphQL and gRPC are the three common answers. They differ less in what they can express than in who decides the shape of a response — and that single difference is what makes each one suit a different conversation.
REST: the server owns the shape
Everything is a resource with a URL, and the HTTP verb says what to do with it.
GET /users/1— readPOST /users— createPUT /users/1— replacePATCH /users/1— modifyDELETE /users/1— remove
Responses are JSON over ordinary HTTP, which is why REST works everywhere: every language has an HTTP client, every proxy understands the verbs, and GET responses are cacheable by browsers and CDNs without you writing a line of code.
Statelessness is part of the same bargain. Any server can answer any request, so scaling out behind a load balancer needs no extra thought.
Where REST costs you
The server decided the shape, so the client takes what it is given. Two symptoms follow.
Over-fetching. The client needs a name and receives id, name, email, age, address and phone. Wasted bandwidth on every request, and it hurts most on the connection that can least afford it — mobile.
Under-fetching. The client needs a user and their posts, so it makes two calls, then another for each post's comments. Round trips multiply, and on a slow mobile network they dominate the latency budget.
You can paper over both with bespoke endpoints — /users/123/profile-page — but then you maintain one endpoint per screen and version them all.
GraphQL: the client states its needs
One endpoint, usually POST /graphql. The client sends a query naming exactly the fields it wants and receives exactly those fields.
query {
user(id: 123) {
name
posts {
title
comments {
text
author { name }
}
}
}
}Over-fetching disappears because unrequested fields are never serialised. Under-fetching disappears because related data nests into one round trip. The schema is strongly typed, so it doubles as documentation, and adding a field breaks no existing client — which is why GraphQL APIs rarely carry version numbers.
Mutations cover writes with the same shape: name the operation, name the fields you want back.
The other trap is the N+1 problem. A query for 100 users each with their posts naively becomes one query for the users plus one per user for posts, because each resolver runs independently. The standard fix is a batching loader that collects the ids requested in a single tick and issues one query for all of them.
GraphQL earns its complexity when several different clients need different slices of the same data. Behind a single web app doing simple CRUD, it is overhead.
gRPC: a contract, compiled
gRPC is built for backend-to-backend traffic, where nobody reads the payload by hand.
You define the service in a .proto file. That file is the contract, and it generates both client and server code in every language you need.
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
rpc StreamUsers (StreamRequest) returns (stream UserResponse);
}
message UserRequest { int32 id = 1; }
message UserResponse {
int32 id = 1;
string name = 2;
string email = 3;
}Two consequences. Messages travel as Protocol Buffers, a binary encoding that is markedly smaller and cheaper to parse than equivalent JSON, because field names are replaced by the numeric tags you can see above. And the transport is HTTP/2, which multiplexes many calls over one connection and carries streams in both directions.
That streaming is a capability difference, not an optimisation. gRPC calls come in four shapes: unary (one request, one response), server streaming (one request, many responses), client streaming (many requests, one response) and bidirectional.
The cost is legibility. You cannot curl a gRPC endpoint, browsers cannot speak it without a grpc-web proxy, and debugging requires tooling that understands the schema.
Choosing per connection
Each style moves the same data; what differs is who decides the shape and where the cost lands.
Diagram, as text
- Client needs data leads to REST: server fixes the shape
- Client needs data leads to GraphQL: client states the shape
- Client needs data leads to gRPC: shape compiled into a contract
- REST: server fixes the shape leads to Simple and cacheable, may over-fetch
- GraphQL: client states the shape leads to Exact fit, cost moves to resolvers
- gRPC: shape compiled into a contract leads to Compact and checked, needs tooling
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Response shape decided by | Server | Client | Shared contract |
| Encoding | JSON | JSON | Binary (protobuf) |
| HTTP caching | Works by default | Effectively unavailable | Not applicable |
| Browser support | Native | Native | Needs grpc-web |
| Streaming | Needs WebSockets or SSE | Subscriptions | Built in, four modes |
| Type safety | Runtime | Schema-checked | Compile-time |
| Best for | Public and third-party APIs | Varied clients, mobile | Internal service-to-service |
The useful habit is to choose per edge, not per system.
Mobile app to backend — GraphQL. Bandwidth is scarce and variable, different screens need different slices, and one round trip beats four.
Service to service — gRPC. High volume, latency-sensitive, no browser involved, and the generated contract catches mismatches at build time rather than in production.
Public API for third parties — REST. Every developer already knows it, it is testable with curl, and GET responses cache for free.
Check your understanding
Common mistakes
Picking one paradigm for the whole system. The right answer is usually per edge — REST outward, gRPC inward, GraphQL where clients differ.
Treating GraphQL as free flexibility. You inherit caching, rate limiting and N+1 problems that REST avoided by accident.
Exposing gRPC directly to browsers. Native support does not exist. You need grpc-web and a proxy, which gives back part of the benefit.
Using POST for everything in a REST API. You lose caching, safe retries, and any ability for proxies to reason about the request.
Versioning by duplicating the API. Running /v1 and /v2 side by side doubles the surface you maintain. Add fields instead of endpoints where you can.
Leaving N+1 until production. Batch loaders exist for this, and retrofitting them is far harder than writing them first.
Practice
Your public API is REST. A partner complains that rendering their dashboard takes 14 requests and returns ten times more data than they use. Give three fixes that keep REST, and say when you would switch to GraphQL instead.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Over-fetching and under-fetching are two different complaints with two different remedies.
Show a strong answer
Three fixes that stay within REST:
- Sparse fieldsets. Let the caller name the fields they want (
?fields=id,status,total). This addresses over-fetching directly, is simple to implement, and keeps responses cacheable if the field list is part of the cache key. - Embedded sub-resources. Allow related data to be included in one response (
?include=items,customer) instead of requiring a follow-up request per relationship. This addresses under-fetching — the 14 requests — without giving the client arbitrary query power. - A purpose-built endpoint. If this dashboard is a known, stable screen, give it one endpoint shaped exactly for it. This is the least elegant answer and often the best one: one request, one cache entry, one query the database team can optimise.
When to switch to GraphQL instead: when the number of distinct client shapes is large and growing, and you cannot keep adding endpoints without the API becoming a list of one-offs. GraphQL earns its complexity when many clients each want a different slice. For one partner and one dashboard, a tailored endpoint is cheaper to build, cheaper to cache and cheaper to operate.
A team proposes gRPC for a public API that browsers and third-party developers will use. Argue the case against, then say what you would offer instead and why.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Ask what a stranger needs in order to make their first successful call.
Show a strong answer
The case against gRPC here is not performance — it is adoption and reach.
- Browsers cannot speak it directly. gRPC needs features of the underlying protocol that browsers do not expose, so a proxy translation layer is required. You have now added a component to the critical path of every public call.
- A stranger cannot explore it. REST's real advantage is that a developer can make a first call with a single command-line request and read the JSON. gRPC requires obtaining the schema, generating a client and compiling it before anything happens at all. For a public API, time-to-first-successful-call is a product metric.
- Debugging is opaque. Binary payloads cannot be read from a log or a browser's network tab without tooling. Third parties will file support tickets you cannot resolve by asking them to paste the response.
- Every integration inherits your schema toolchain, in whichever languages your partners use — including ones with poor support.
What I would offer instead: REST with JSON as the public surface, documented with a machine-readable specification so clients can still be generated for those who want them. Keep gRPC internally, where both ends are yours, and let the public edge translate. This is the common shape: a compact, contract-checked protocol inside, and an approachable one at the boundary.
Key takeaways
- The three paradigms differ mainly in who decides the shape of the response — server, client, or a shared compiled contract.
- REST is resource-based JSON over plain HTTP: universally supported and cacheable by default, at the cost of over- and under-fetching.
- GraphQL lets the client request exact fields and nest related data into one round trip, which is why it suits mobile and varied clients.
- GraphQL's single
POSTendpoint breaks HTTP caching and simple rate limiting, and resolvers make N+1 easy to write by accident. - gRPC uses a
.protocontract, binary protobuf encoding and HTTP/2, giving compile-time type safety and four streaming modes — but it is not browser-native or human-readable. - Choose per connection: REST for public APIs, gRPC for internal service-to-service, GraphQL where one dataset feeds many different clients.
- Large systems normally run all three, each where its trade-off is the right one.
Further reading
- Roy Fielding, "Architectural Styles and the Design of Network-based Software Architectures" (doctoral dissertation, 2000), chapter 5 — the constraints that define REST.
- RFC 9110, HTTP Semantics — methods, status codes and the caching properties REST relies on.
- Lee Byron and Nick Schrock, "GraphQL: A data query language" (Facebook engineering paper, 2015) — the problem it was built to solve.
- Andrew Birrell and Bruce Nelson, "Implementing Remote Procedure Calls" (ACM TOCS, 1984) — the ancestry of gRPC-style interfaces.
- Sam Newman, Building Microservices, second edition (O'Reilly, 2021), chapter 4 — choosing a style per boundary rather than per company.