← Back to blog
API Design·September 28, 2026·6 min read

REST vs. RPC-Style API Design: The Tradeoffs Nobody Puts in the Style Guide

Most "REST APIs" in production are actually resource-shaped RPC — here is what you really give up or gain by choosing REST, JSON-RPC, gRPC, or tRPC for a given piece of your system.

Every API design doc eventually hits the same argument: someone wants POST /orders/123/archive, someone else wants POST /archiveOrder, and both call what they're building "a REST API." Only one of them is right about the label, but that's the least interesting part of the disagreement — the actual decision underneath it has real consequences for caching, tooling, versioning, and how painful the API is to evolve five years from now.

Most of what gets called "REST" in production is not REST in the sense Roy Fielding defined it in his 2000 dissertation. Fielding's REST requires, among other things, hypermedia controls — a client should be able to navigate an API by following links in responses, the way a browser follows links in HTML, without hardcoding URL templates. Almost nobody does this. What ships as "our REST API" is usually resource-oriented HTTP: URLs name nouns (/orders/123), HTTP verbs express the action (GET, PATCH, DELETE), and status codes carry outcome. That's a perfectly good design style — it's just not HATEOAS-REST, and calling out the gap matters because the benefits people actually want from "REST" (cacheability, uniform semantics, tooling support) come from the HTTP-verb discipline, not from the hypermedia part nobody implements.

RPC-style APIs skip the resource pretense entirely. You're calling a named procedure — archiveOrder(orderId) — and the transport is incidental. JSON-RPC, gRPC, and tRPC are all RPC in this sense: the unit of design is the function signature, not the URL.

The same operation, two ways

Take "cancel an order and refund the customer." In resource-oriented HTTP, you're choosing between:

PATCH /orders/123
Content-Type: application/json

{ "status": "cancelled" }

or, when the operation doesn't map cleanly onto a field update (which is most non-trivial operations):

POST /orders/123/cancel
Content-Type: application/json

{ "refund": true }

That second form is already a concession — it's a verb bolted onto a noun-shaped URL, which is what most "REST" APIs actually look like once they have more than basic CRUD. The RPC equivalent skips the pretense:

POST /rpc
{ "method": "cancelOrder", "params": { "orderId": 123, "refund": true } }

or, with gRPC, a typed procedure call over a .proto-defined service:

rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);

The gRPC and JSON-RPC versions are honest about what's happening — you're invoking a function — whereas the HTTP version is asking you to pretend "cancel" is a property you're setting. For simple CRUD, resource semantics fit naturally. For workflows (approve, cancel, refund, archive, retry), they get forced, and teams either sprawl into custom sub-resource actions (/orders/123/cancel, /orders/123/refund, /orders/123/retry-payment) or give up and go full RPC for that slice of the API.

What the table doesn't show

REST (resource-oriented HTTP)JSON-RPCgRPCtRPC
Unit of designResource + HTTP verbNamed methodProtobuf-defined service methodTypeScript function
CachingNative via HTTP (GET + ETag/Cache-Control)None built-inNone built-in (streaming-oriented)None built-in
Browser-native toolingYes — curl, browser devtools, any HTTP clientNeeds a client libraryNeeds grpc-web/gateway in browsersTypeScript-only, same-repo
Wire formatJSON (usually)JSONProtobuf (binary)JSON (via superjson/similar)
StreamingAwkward (SSE/chunked as a bolt-on)Not standardizedFirst-class (HTTP/2 bidi streaming)Supported via subscriptions
Schema/codegenOpenAPI (separate artifact, can drift)None standard.proto is the schema, codegen is coreTypeScript types, zero codegen step
Cross-language clientsAny HTTP clientAny JSON-RPC clientStrong (protoc plugins for most languages)TypeScript/JavaScript only
Error semanticsHTTP status codes + bodyBody only, transport is opaqueRich status codes + metadataTypeScript exceptions across the wire

The columns that matter most in practice rarely show up on a feature-comparison table at all.

Caching is the real REST advantage, and it's easy to throw away. A GET /orders/123 response can sit in a CDN, a browser cache, or a reverse proxy, validated cheaply with an ETag and a conditional If-None-Match request — no application code involved. That's not a REST ideology payoff, it's an HTTP infrastructure payoff, and it evaporates the moment a team tunnels everything through POST (which a lot of "REST" APIs quietly do, because POST is easier to reason about for auth and logging). If you go RPC over HTTP, you're forgoing that layer entirely and need your own caching story — which is fine for read patterns that don't fit HTTP caching anyway (personalized data, high write-churn resources), and a real loss for anything that's genuinely cacheable, like public catalog data.

gRPC's real win is streaming and typed cross-language contracts, not raw speed. Protobuf's binary encoding is smaller and faster to (de)serialize than JSON, but for most APIs that difference is noise next to database and network latency. What gRPC actually buys you is HTTP/2 multiplexed bidirectional streaming (useful for anything long-lived — live updates, log tailing, chat) and a .proto file that's simultaneously the contract, the documentation, and the codegen source for every language on both sides. That's why gRPC dominates internal service-to-service traffic at companies running polyglot microservices — the alternative is an OpenAPI spec that's generated after the fact and drifts the first time someone edits a handler without touching the YAML.

tRPC's win is eliminating the client/server schema gap, at the cost of coupling. If your frontend and backend are TypeScript in the same repo, tRPC lets you import server procedure types directly into the client — no OpenAPI generation step, no manually-kept-in-sync SDK, no runtime validation drift between what the server accepts and what the client sends. The tradeoff is that this only works because client and server share a language and (usually) a repo; it's not a shape you can hand to a third-party integrator or a mobile team on a different stack without wrapping it in something else.

Error handling is where REST's "uniform interface" claim pays rent. A well-designed resource API uses status codes as part of the actual contract: 404 means the resource doesn't exist, 409 means a conflict the client can resolve by retrying with fresh state, 422 means the payload was well-formed but semantically invalid. A client — or a piece of generic middleware, or a monitoring dashboard — can branch on the status code without parsing the body. RPC-style APis, JSON-RPC in particular, tend to return 200 OK for everything and bury the real outcome in an error field inside the JSON body, which means every consumer has to parse the body to know if the call even succeeded. gRPC avoids this by having its own typed status codes (NOT_FOUND, ALREADY_EXISTS, FAILED_PRECONDITION) that map reasonably well onto HTTP semantics — it just doesn't reuse HTTP's status line to carry them.

Picking one

The decision usually comes down to who's on the other end of the wire:

None of these are permanent commitments — plenty of systems run gRPC internally and expose a REST facade at the edge for external consumers, which is arguably the most common real-world answer: pick the RPC style that fits the audience closest to the wire, and translate at the boundary rather than forcing one style all the way through. What matters is making the choice on purpose, for the traffic pattern you actually have, rather than defaulting to "REST" because that's the word on the style guide and then quietly reinventing RPC semantics with POST endpoints anyway. If you're debugging one of these APIs by hand, a quick HTTP status code reference is worth bookmarking — the difference between a 409 and a 422 is usually the difference between "retry with backoff" and "don't retry, fix the request."

#rest-api#rpc#grpc#trpc#json-rpc#api-design

Related reading

API Design
Designing Sane Pagination: Why Offset Breaks Under Writes and Keyset Fixes It
API Design
OpenAPI Spec-Driven Development: The Contract That Keeps Your API and Docs From Diverging
API Design
Idempotency Keys: Why 'Just Retry the Request' Breaks in Production