REST vs RPC vs gRPC (and where tRPC and GraphQL fit)
- Tell a resource-shaped API from a procedure-shaped one by its endpoints, and say what each gets from HTTP
- Explain what gRPC adds — a
.protocontract with generated clients, HTTP/2, four call types, deadlines — and why browsers need gRPC-Web and a translating proxy - Explain how tRPC v11 shares a contract without code generation, and what that requires of every client
- Choose REST, gRPC, tRPC or GraphQL from who calls the API, and justify the choice
Read
Nouns or verbs — the same feature, two API shapes
FlagHere is one feature — orders you can list, create, read, change and cancel — designed twice.
Resource-shaped (what people usually mean by "a REST API"): URLs name things, and the HTTP method says what to do to them.
GET /orders?status=open list
POST /orders create -> 201 Created, Location: /orders/43
GET /orders/43 read
PATCH /orders/43 change some fields
DELETE /orders/43 remove
POST /orders/43/cancellation cancel (an action modelled as a thing you create)
Procedure-shaped (RPC — remote procedure call): URLs name functions, and almost every call
is a POST with the arguments in the body.
POST /rpc/listOrders {"status":"open"}
POST /rpc/createOrder {"sku":"TEA-1","qty":2}
POST /rpc/getOrder {"id":43}
POST /rpc/cancelOrder {"id":43,"reason":"duplicate"}
Neither is wrong. They put the meaning in different places, and that decides what the infrastructure around you can do without reading your docs.
What the resource shape gets from HTTP
- Caching. A
GETresponse is cacheable by browsers, CDNs and proxies (RFC 9110, section 9.3.1).POST /rpc/getOrderis aPOST, and a cache never answers aPOSTwith a stored response (section 9.3.3), so every read goes to the server. - Retries. Clients and proxies know
GET,PUTandDELETEare idempotent and can resend them after a dropped connection. Every RPC call looks like a non-idempotentPOST. - Generic tooling. Status codes,
Location,ETag, logs grouped by method and path, curl commands anyone can read — all of it works without knowing your API.
What the procedure shape gets
- Names that match the code.
cancelOrder(id, reason)on the client isPOST /rpc/cancelOrderon the wire. No debate about whether a cancellation is a resource. - One obvious way to add an operation: add a function. Resource APIs need a design conversation for every verb that isn't create/read/update/delete.
That trade-off — HTTP's shared meaning versus the code's own vocabulary — runs through every choice in this unit. gRPC and tRPC are both procedure-shaped; they differ in how the procedure's contract reaches the client.
gRPC on one page — contract, wire, streams, browsers
FlaggRPC is procedure-shaped RPC with a contract first. You describe the service in a .proto
file — Protocol Buffers is gRPC's default interface definition language — and the protobuf
compiler's gRPC plugins generate a typed client and a server skeleton in each language you use.
syntax = "proto3";
package shop.v1;
service Orders {
rpc GetOrder (GetOrderRequest) returns (Order); // unary
rpc WatchOrders (WatchOrdersRequest) returns (stream OrderEvent); // server streaming
}
message GetOrderRequest { int64 id = 1; }
message Order { int64 id = 1; string status = 2; }
message WatchOrdersRequest { string status = 1; }
message OrderEvent { Order order = 1; }
The numbers (= 1, = 2) are the contract on the wire: a field number "cannot be changed once
your message type is in use", and a deleted field's number must be reserved so nobody reuses
it. That's how a Go server and a Java client deployed months apart still agree.
On the wire: always HTTP/2, always POST
Every call is an HTTP/2 request with :method POST, :path /shop.v1.Orders/GetOrder and
content-type: application/grpc. The body is a stream of length-prefixed protobuf messages —
binary, so curl shows you bytes, not JSON. The result travels in trailers after the body:
grpc-status: 0 is OK, anything else is an error. An error response still uses HTTP status
200, which is why gRPC servers answer 415 to clients that don't speak gRPC, so a plain HTTP
client doesn't mistake a gRPC error for success.
What it gives you beyond "JSON over POST"
- Four call types: unary (one request, one response), server streaming, client streaming, and bidirectional streaming — both sides sending on one long-lived call.
- Deadlines. The client says how long it will wait (sent as a
grpc-timeoutheader); past it, the call ends withDEADLINE_EXCEEDED. - Cancellation from either side ends the call at once. It doesn't stop your server code, though: gRPC "does not have a mechanism to interrupt the application-provided server handler", so a long handler has to check for cancellation and stop itself. And "changes made before a cancellation are not rolled back".
- Generated clients in every supported language, from the one
.proto.
The browser problem
A browser can't make a native gRPC call: fetch gives you no control over HTTP/2 framing and no
access to trailers. gRPC-Web is a variant protocol for browsers, and something has to
translate it to real gRPC: usually a proxy in front of the service — Envoy is grpc-web's default,
and its README lists others such as Nginx — or a server framework with gRPC-Web support built in
(the README names Armeria and Tonic). The grpc-web client supports only unary and
server-streaming calls (server streaming only in its grpcwebtext mode); client and
bidirectional streaming are "not currently supported".
So gRPC shines between services you own: many languages, high call volume, streaming, a contract the compiler enforces. For browsers and third-party developers, expose a JSON/HTTP API instead (or in front of it).
Choosing by who calls you — and where GraphQL fits
FlagThe useful question isn't "which style is best" but "who has to understand this contract, and what do they already have?"
| Callers | Reach for | Because |
|---|---|---|
| Third-party developers, partners, any language | REST (resource-shaped JSON, described with OpenAPI) | They have curl and an HTTP library, nothing else of yours. Caching, retries and status codes work without your docs |
| Your own services in several languages, high volume, streaming | gRPC | One .proto generates every client; HTTP/2 streams, deadlines and binary protobuf; a contract the compiler checks across languages |
| One TypeScript app and its own backend, same repo, same deploy | tRPC | The router's type is the contract — no schema file, no generator, compile errors on mismatch |
| Many client screens wanting different slices of a connected graph of data | GraphQL (below) | Clients ask for exactly the fields they need in one round trip |
Mixed answers are normal: gRPC between internal services and a REST edge for the public; tRPC for the web app and a small REST surface for a partner integration.
The three questions that decide it
- Can the caller share your build? If yes (same repo, TypeScript), tRPC's type sharing is
nearly free. If no, the contract must be a document both sides can read: OpenAPI or
.proto. - Does HTTP's shared meaning pay for itself? Public reads that a CDN can cache, links people paste into curl, retries by generic clients — that's resource-shaped HTTP. Service-to-service calls inside one platform rarely need any of it.
- Is streaming part of the feature? gRPC has it in the contract (server, client and bidirectional streams). Plain HTTP APIs reach for SSE or WebSockets — the subject of this track's last unit.
GraphQL in one paragraph
A GraphQL server exposes one endpoint (usually /graphql) and a typed schema; the client sends
a query naming exactly the fields it wants, across related objects, and gets that shape back. It
fixes over- and under-fetching for many varied clients. The costs move to the server: every
operation goes to the same URL, usually as a POST (GET is allowed only for queries, and long
queries outgrow URL limits), so HTTP caching needs extra machinery such as persisted documents;
resolvers can turn one query into N+1 backend calls unless you batch them (DataLoader); and
arbitrary queries need depth and complexity limits. Partial failures come back
as an errors array next to data — with a 2xx status when data is present, since HTTP has
no "partial success" code.
Quiz
An API has POST /api/getInvoice, POST /api/sendInvoice and POST /api/voidInvoice. What shape is it, and what does that cost?
A product catalogue is read millions of times a day through a CDN. Which endpoint can the CDN cache with standard HTTP rules and no extra machinery?
FlagA gRPC call fails with status NOT_FOUND. What HTTP status code does the response carry?
Your React app wants to call an internal gRPC service directly from the browser. What does that take?
FlagYou delete string note = 3; from a protobuf message that deployed services still exchange. What should you do with the number 3?
What does tRPC require that a REST + OpenAPI design doesn't?
FlagA payments company has (a) a public API for merchants in many languages, (b) a dozen internal Go, Java and Python services that stream events to each other, and (c) its own Next.js dashboard backed by a TypeScript server in the same repo. Which mapping fits best?
FlagA GraphQL response arrives with HTTP 200 and a body containing both data and an errors array. What happened?
Fill in the blank
Make WatchOrders a server-streaming call, and retire the deleted note field's number for good.
service Orders { rpc GetOrder (GetOrderRequest) returns (Order); rpc WatchOrders (WatchOrdersRequest) returns ( OrderEvent); } message Order { int64 id = 1; 2; // was: string note = 2; string status = 3; }
Give the client the server's contract without importing any server code.
Flagimport { createTRPCClient, httpBatchLink } from "@trpc/client"; import { AppRouter } from "../server/appRouter"; const trpc = createTRPCClient<>({ links: [httpBatchLink({ url: "/trpc" })], });
One procedure reads, one writes. Pick the procedure types (they decide GET vs POST on the wire).
export const appRouter = t.router({ userById: t.procedure.input(z.string()).(({ input }) => db.users.find(input)), userCreate: t.procedure .input(z.object({ name: z.string() })) .(({ input }) => db.users.create(input)), });
Further reading
- RFC 9110: HTTP Semantics (methods, caching)
- Fielding: REST (dissertation, chapter 5)
- gRPC: Core concepts
- gRPC: Cancellation guide
- gRPC over HTTP/2 (protocol spec)
- grpc-web README (supported call types, proxy)
- gRPC blog: gRPC Load Balancing
- gRPC: Authentication guide
- Protocol Buffers: proto3 language guide
- tRPC v11: Quickstart
- tRPC v11: HTTP RPC specification
- GraphQL: Serving over HTTP
- GraphQL: Performance (N+1, demand control)