Learn / APIs, auth and backend architecture
REST vs RPC vs GraphQL
Lesson 15 of 37 · 8 min read ·
The three models
REST — you expose resources and manipulate them with HTTP verbs. GET /orders/42, DELETE /orders/42. The URL names a thing; the method names the action.
RPC — you expose procedures. POST /createOrder, POST /cancelOrder. The URL names the action. gRPC is this with a binary protocol, HTTP/2 and generated clients from a .proto schema.
GraphQL — you expose a type graph and one endpoint. The client sends a query describing exactly the fields it wants and gets back that shape.
What each actually optimises for
REST optimises for the HTTP ecosystem. You get caching for free (GET responses are cacheable by browsers, CDNs and proxies), standard status codes everyone understands, and you can debug it with curl. It is the right default for public APIs and for anything where a CDN sits in front.
Where it strains: not everything is a resource. "Publish this post", "retry this payment", "merge these two accounts" are actions, and forcing them into resources produces awkward URLs like POST /posts/42/publication. When that happens, just accept a small RPC-shaped endpoint. A mostly-REST API with three verb endpoints is better than a contorted one.
RPC/gRPC optimises for service-to-service calls. Binary encoding is compact, HTTP/2 multiplexes, and the generated client means a schema change breaks the build rather than production. Streaming is first-class. The cost: not human-readable, awkward from browsers without a proxy, and no HTTP caching. Excellent inside your infrastructure, rarely right facing the public.
GraphQL optimises for clients with varied and changing data needs — several front-ends, a mobile app that wants less data than the web app, screens that would otherwise need five round trips. The client stops waiting on you to ship a new endpoint.
The costs are real and consistently understated:
- HTTP caching is gone. Everything is a
POSTto one URL. You rebuild caching at the application layer. - N+1 by default. A nested query resolves per-field per-item unless you add DataLoader-style batching.
- Query cost is unbounded. A client can request a deeply nested graph that takes your database down. You need depth limiting, complexity analysis and persisted queries. This is a security requirement, not a nicety.
- Errors are
200 OKwith anerrorsarray, so every monitoring tool you own needs teaching.
Choosing
The honest heuristic:
- Public API, third-party developers → REST. It is what people expect, it is cacheable, it documents itself with
curl. - Internal service-to-service, high volume → gRPC.
- One backend, many demanding first-party clients → GraphQL is worth its costs.
- A CRUD app with one front-end you also own → REST. GraphQL here is almost always more machinery than the problem justifies.
You can also mix: REST for the public surface, gRPC between services. That is what most large systems actually look like.
Gotchas
- Use the status codes properly:
200success,201created,400your fault,401unauthenticated,403unauthorised,404missing,409conflict,422validation failed,429rate limited,5xxour fault. Returning200 {"error": ...}breaks every retry and monitoring layer. GETmust be safe and repeatable. Never mutate in aGET— crawlers and prefetchers will find it.- Paginate every list endpoint from day one. Adding a limit later is a breaking change.
- Return a consistent error body with a stable machine-readable
code, not just a human string.
Prove you know it
Take an API you have built and name three endpoints that are RPC-shaped pretending to be REST. Decide, for each, whether the honest RPC version would be clearer. Then check whether every list endpoint has a bounded page size.