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 POST to 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 OK with an errors array, 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: 200 success, 201 created, 400 your fault, 401 unauthenticated, 403 unauthorised, 404 missing, 409 conflict, 422 validation failed, 429 rate limited, 5xx our fault. Returning 200 {"error": ...} breaks every retry and monitoring layer.
  • GET must be safe and repeatable. Never mutate in a GET — 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.

Go deeper