REST vs GraphQL vs gRPC: Choosing an API Style
Concept
The three dominant API styles organize communication around different primitives, and that difference — not raw feature lists — is what should drive the choice. REST organizes around resources: stable nouns (a user, an order, a product) exposed as URLs that clients fetch, create, update, or delete over standard HTTP verbs, with responses typically shaped by the server rather than the client. GraphQL organizes around queries: a client sends a single request describing exactly the shape of data it wants across possibly many underlying resources, and a resolver graph on the server fulfills only those fields. gRPC organizes around methods (Remote Procedure Calls): a .proto file defines strongly-typed service methods and message shapes, compiled into client/server stubs, and calls travel as compact binary Protobuf messages over HTTP/2.
That primitive difference cascades into everything else. REST's resource model maps cleanly onto HTTP caching (GET is cacheable by URL), is trivially callable from a browser or curl, and needs no special tooling to inspect — but a client that needs data nested three resources deep either over-fetches (gets whole objects it doesn't need) or under-fetches (needs N+1 round trips to assemble a view). GraphQL's client-driven query shape solves exactly that over/under-fetching problem — a mobile client and a desktop client can each ask for only the fields their view needs from the same endpoint — at the cost of losing HTTP-level caching semantics (every request is typically a POST to one endpoint) and pushing real complexity onto the server's resolver layer. gRPC's binary Protobuf wire format and HTTP/2 multiplexing give it a real performance edge — smaller payloads, lower serialization cost, native streaming (unary, server-streaming, client-streaming, bidirectional) — but the tradeoff is that a browser can't natively speak it (gRPC-Web or a Connect proxy is required), and the strict compiled schema means both sides must be rebuilt from the same .proto when it changes.
Tradeoffs
| Dimension | REST | GraphQL | gRPC |
|---|---|---|---|
| Primitive | Resources (nouns) + HTTP verbs | Client-defined queries over a graph | Strongly-typed RPC methods |
| Wire format | Usually JSON over HTTP/1.1 | JSON over HTTP (usually POST) | Binary Protobuf over HTTP/2 |
| Browser-callable directly | Yes, natively | Yes, natively | No — needs gRPC-Web/Connect proxy |
| Caching | HTTP-native (GET + URL) | Weak — needs app-level caching | App-level; no HTTP GET semantics |
| Over/under-fetching | Common on nested/varied clients | Solved by design — client picks fields | N/A — call shape is fixed per method |
| Schema strictness | Loose unless paired with OpenAPI | Strong (GraphQL SDL), but query shape is client's choice at runtime | Strong and compiled — both sides regenerate stubs on change |
| Streaming | Not native (polling/SSE/WebSockets bolted on) | Subscriptions exist but are a secondary feature | Native: unary, server/client/bidirectional streaming |
| Typical fit | Public APIs, CRUD services, anything a browser calls directly | Data-heavy products with multiple client shapes (web/mobile) hitting shared data | Internal service-to-service calls at scale, performance-sensitive paths |
When to use / when not to
- REST: default choice for a public API or anything called directly by a browser — no special client tooling required, and HTTP semantics (status codes, caching, verbs) are broadly understood by every consumer, including third parties.
- GraphQL: justified once multiple client types (web, iOS, Android) genuinely need different slices of the same underlying data and a REST version would otherwise require either bespoke per-client endpoints or chronic over-fetching. Not justified for a small, single-client CRUD API — the resolver-layer complexity and loss of HTTP caching aren't worth it.
- gRPC: fits internal, service-to-service calls where both ends are under the team's control, throughput/latency matter, and a browser is never a direct caller. A poor fit for a public-facing API aimed at third-party developers, who overwhelmingly expect REST or GraphQL tooling.
- A single system commonly uses more than one style at different boundaries — e.g., gRPC between backend services, REST or GraphQL at the public/browser-facing edge — rather than forcing one style everywhere.
Common pitfall
Picking GraphQL or gRPC because they're the newer or more "impressive" choice rather than because a concrete over-fetching or performance problem exists. GraphQL in particular has a specific operational trap: an innocuous-looking nested query can fan out into a large number of uncontrolled backend/database calls if query depth, batching (e.g. via a DataLoader pattern), and per-field cost aren't deliberately controlled — a query that looks cheap on the wire can be expensive on the server. The fix is architectural (depth limits, batching, query cost analysis), not something that goes away by picking GraphQL and moving on.
Engineering Lens
The API style choice is a contract between how the client thinks about the data and how the server is willing to expose it — and that contract should be driven by who's calling and why, not by technology preference. A useful question in review: "if the mobile team's next feature needs a different slice of this data, does that require a new REST endpoint, or does GraphQL make that a client-side query change?" If the answer is "we'd add another REST endpoint," and that's already happened three or four times for the same resource, that's the concrete signal GraphQL would pay for itself — not a hunch. Symmetrically, if a gRPC service is being proposed for something a browser needs to call directly, that's worth pushing back on before the gRPC-Web proxy layer becomes permanent added complexity for no real benefit over REST.
Sources
- When to Use REST vs. gRPC vs. GraphQL — Kong Inc.
- REST vs. GraphQL vs. gRPC – Which API to Choose? — Baeldung
- Core concepts, architecture and lifecycle — gRPC official docs