REST GraphQL and RPC
Date: 2026-08-17
Three answers to “who decides the shape of the response”. REST says the server, GraphQL says the client, RPC says neither — there’s a function and it has a signature. The choice mostly follows from how many different clients you have and how much you trust them.
The same request, three ways
Fetching an order with its line items and the customer’s name.
REST — the server defined an /orders/:id representation, and you get that representation.
GET /orders/1234
GET /orders/1234/items ← probably a second call
GET /customers/99 ← and a third, unless the server embeds them
GraphQL — one endpoint, and the query states exactly the fields wanted.
query {
order(id: "1234") {
total
items { sku quantity }
customer { name }
}
}RPC — a named procedure with typed arguments, usually generated from a schema.
orderService.GetOrder({ order_id: "1234", include_items: true })
What each optimises for
| REST | GraphQL | RPC (gRPC, tRPC) | |
|---|---|---|---|
| Response shape decided by | Server | Client | Server, per procedure |
| Endpoints | Many | One | Many procedures |
| Over-fetching | Common | Solved | Rare — procedures are specific |
| Round trips | Often several | One | One per procedure |
| HTTP caching | Native and free | Effectively lost — POST to one URL | Lost — binary, POST |
| Discoverable by hand | curl, a browser | GraphiQL, introspection | Needs the schema and tooling |
| Typed end to end | Via OpenAPI, bolted on | Yes, built in | Yes, built in |
| Cost model | Predictable per endpoint | Unpredictable — the client writes the query | Predictable |
| Best when | Public API, many unknown consumers, cacheable reads | Many clients each wanting different shapes | Internal service-to-service, one org owns both ends |
The thing each gets wrong
REST over-fetches and under-fetches, and the usual fix is a per-screen endpoint, which is a Backend for Frontend growing inside your public API. It’s also the only one where HTTP caching works for free — a GET with an ETag is cacheable by the browser, the CDN and every proxy between, which is a large advantage to give up — HTTP Caching, CDN Caching.
GraphQL moves the cost model to the client. A nested query can be cheap or can join four tables per row, and the server can’t tell until it runs. Real deployments need query depth limits, complexity budgets, persisted queries (only pre-registered queries allowed) and DataLoader-style batching to avoid the N+1 problem — none of which are optional at scale, and all of which are work that REST didn’t require. Errors are also awkward: a partial failure returns 200 OK with an errors array, so every monitoring tool that watches status codes sees success.
RPC assumes both ends are yours. Regenerating clients on a schema change is fine internally and impossible for a public API. gRPC’s binary framing also doesn’t survive a browser without a proxy layer.
Choosing
- Public API, third parties, cacheable reads → REST. Predictable, debuggable with
curl, and the caching is worth a lot — API Design - One backend, several clients that each want different fields → GraphQL, with persisted queries. This is the case it was invented for
- Service to service, inside one organisation → RPC. Typed, fast, and the schema is enforced at build time
- A web app whose backend is the same codebase → RPC-shaped (tRPC or server functions) is usually less ceremony than either alternative
They coexist. A common commerce shape is gRPC between services, a GraphQL or BFF layer for first-party clients, and REST for the public and partner API. Picking one for the whole estate is a preference, not a requirement.
Where it interacts
- API Design — errors, pagination and versioning still need deciding in all three; only the mechanism changes
- Rate Limiting — request counting is meaningless for GraphQL, where one request can cost anything. Limit on computed complexity instead
- Rendering and SEO — a GraphQL-only storefront pushes data fetching into the client unless the server does it, which is a rendering decision before it’s an API one