REST, GraphQL, gRPC: pick by shape of change
The right API style follows from who calls it and how often the contract changes.
What you will be able to do
- Match an API style to public, mobile, and service-to-service callers
- Explain N+1 request and over-fetching problems and who solves them
- Describe how gRPC's binary protocol and streaming change performance
- Choose a pagination and versioning strategy
Three shapes
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Model | Resources and verbs | Graph queried by the client | Remote methods with typed messages |
| Transport | HTTP/1.1 or 2, JSON | Usually HTTP POST, JSON | HTTP/2, protobuf |
| Who usually calls it | Public clients, browsers, partners | Apps with varied screens | Internal services |
| Caching | Free via HTTP semantics | Hard — single endpoint, custom keys | Limited; HTTP caching mostly lost |
| Versioning | URL or header versioning | Schema evolution, deprecation fields | Field numbers, backward compatible |
| Streaming | Not really | Subscriptions over websockets | Built in, both directions |
The problem REST creates on a phone
A feed screen needs a post, an author, three comments, and a like count. REST gives you four or five endpoints, so the phone makes four or five round trips — the N+1 problem at the client. GraphQL lets the client ask for exactly those fields in one request, which on a mobile network is worth a lot.
query FeedScreen($id: ID!) {
post(id: $id) {
text
author { name avatar }
comments(first: 3) { text }
likeCount
}
}The costs are real. Every field is a resolver, so a naive implementation becomes its own N+1 problem on the server — solved with batching (DataLoader-style). Caching is no longer free, so you need persisted queries, per-field cache hints, or a cache in front of the resolver layer. And an unbounded query can cost more than a thousand REST calls, so you must price queries and refuse expensive ones.
gRPC specifics worth naming
- Protobuf payloads are compact and schema-checked at build time, which removes a class of integration bugs.
- HTTP/2 multiplexes many calls over one connection, so a chatty service mesh does not pay a handshake per call.
- Streaming modes — client, server, bidirectional — make it the natural fit for uploads, logs, and live updates.
- It is awkward in browsers (needs a proxy) and hostile to eyeball debugging, so keep it inside the network.
Pagination, filtering, versioning
| Approach | How | Problem |
|---|---|---|
| Offset/limit | ?offset=40&limit=20 | Rows shift when items are inserted; deep offsets scan everything |
| Cursor | ?after=<opaque cursor>, server returns next | Slightly more work to implement; no jumping to page 7 |
| Keyset | WHERE (created_at, id) < (?, ?) | Needs a stable sort key; excellent for infinite scroll |
Version by never breaking: add fields, deprecate loudly, keep old shapes alive for a measured period. If a breaking change is unavoidable, put the version in the path (/v2/) so it is visible in logs and easy to route. Header versioning is cleaner in theory and harder to debug in practice.
/doCheckout)Worth remembering
- REST: cacheable, obvious, best default for public and browser traffic.
- GraphQL: one round trip for a screen that needs many resources; pays with caching complexity and query-cost limits.
- gRPC: typed, compact, streaming — the default inside your own network.
- Cursor pagination beats offsets once data changes underneath the reader.