Skip to content

Choosing an API style, REST vs GraphQL vs tRPC vs gRPC

Each style moves a cost: REST pays in round trips, GraphQL in server work and caching, tRPC in coupling, gRPC in browser reach.

Predict first

Before you read: predict

ExamHall's proctor console has two builds of the candidate card. The REST build sends three GETs (candidate, attempt, flags); the GraphQL build sends one POST query for the same fields. Every API answer carries Cache-Control: private, max-age=600. A proctor opens Ada's card 4 times in 5 minutes, and nothing changes on the server.

How many API requests reach the server in each build?

Pick one

The 30-second version

REST gives resources URLs and fixed shapes: a view may need several GETs and receive unused fields, but HTTP caches reuse GETs. GraphQL POSTs one query whose selection is the reply's shape; caches skip POSTs, and resolvers run per field, so batching and cost limits become server work. tRPC shares types within one codebase. gRPC sends numbered fields.

Each style moves a cost: REST pays in round trips, GraphQL in server work and caching, tRPC in coupling, gRPC in browser reach.

How it works

Step 1 of 11

REST names each resource with a URL. The server fixes each representation's fields; the client chooses which URLs to fetch.
In the kitchen
  • pre-cut bundles on hooks, each a fixed mix of pieces → REST resources (step 1)

Analogy: A fabric shop's cutting counter

A dressmaker can buy cloth in four ways.

At the fabric shop In the system
pre-cut bundles on hooks, each a fixed mix of pieces REST resources (step 1)
a dress needing three bundles, each with pieces she will never sew under- and over-fetching (step 2)
a cutting slip naming exact cloths and lengths, handed over once a GraphQL selection (step 3)
the cutter unrolls and measures every line; a 500-line slip ties up the counter resolvers per field, cost limits (steps 4–5)
bundles hang ready on hooks; a one-off slip is cut afresh each time, unless it is a numbered house kit GETs reused, POSTed queries, persisted ids (steps 6–7)
the workroom's order pad, printed from the shop's own stock list tRPC's shared type (step 8)
trade orders by cloth number; a renamed cloth keeps its number protobuf field numbers (step 10)

In real terms: pick the style by who shapes the data, where it can be cached, who shares the contract, and whether a browser must call it.

Breaks down when: you expect the cutter to refuse an absurd slip on sight. A GraphQL server refuses only what a validation rule you wrote checks (step 5).

Contrasting case: one dressmaker buying one bundle. Every style costs one request then; the differences show only with several resources, repeats, renames or streams.

Examples, run for real

4.1 ExamHall: the room card four ways

EXH-1390: the proctor console's room card sent 10 REST requests per open and carried candidate fields it never showed. MR !1590 moves the console's nested views to one GraphQL query and keeps the exam paper on REST.

4.1 proctor console room card (3 candidates): HTTP requests · response bytes · data-layer calls
  REST, resource URLs          10 requests  1782 B  10 data-layer calls
  GraphQL, one POST query      1 request    256 B   10 data-layer calls
  tRPC, room.card procedure    1 request    192 B   10 data-layer calls (tRPC sent a GET)
  gRPC, RoomCardOf unary call  1 request    81 B    10 data-layer calls
  all four render the same card: true

REST pays in requests and bytes (step 2). GraphQL's one request still makes ten data-layer calls (step 4). The tRPC and gRPC procedures were written for this view. gRPC's figure is the protobuf message; the others are JSON bodies.

4.2 The exam paper and the browser cache

4.2 candidate app loads the ECON-201 paper 3 times; every answer carries Cache-Control: private, max-age=600
  REST GET /api/exams/ECON-201/paper   requests reaching the server: 1 (GET)
  GraphQL POST /graphql                requests reaching the server: 3 (POST)
  GraphQL GET, persisted query id      requests reaching the server: 1 (GET)
  tRPC query paper.get                 requests reaching the server: 1 (GET)
  gRPC puts each call's status in HTTP trailers; can a page's fetch() read trailers? false

Same header, different reuse: the method decides (steps 6, 7, 9). A page cannot read trailers, one reason gRPC needs a proxy (step 11).

4.3 A renamed field

4.3 the server renames candidate.seat to seatLabel; each client is unchanged, then built and run
  REST, hand-written type            tsc: 0 errors | runs: Ada Okafor · seat undefined
  tRPC, imports the router type      tsc: trpc-client.ts:7 TS2339 Property 'seat' does not exist on type '{ id: string; name: string; seatLabel: string; }'. | runs: not run: the build failed
  GraphQL, query in a string         tsc: 0 errors | runs: HTTP 200, errors: Cannot query field "seat" on type "Candidate".
  gRPC, old .proto (3 = seat)        server: seat_label = 3 | runs: Ada Okafor · seat B7

tRPC fails at build (step 8); REST silently shows undefined; GraphQL fails at run time, validated against the schema. gRPC keeps working: only field 3 travels (step 10).

Picture it

three work stations side by side. On the left, two grey-haired workers in green and blue stand at a long table laid out with fixed parts: gears, spanners, bolts and brackets. In the middle, a woman in a blue suit picks pieces from a rack of trays into a small box. On the right, two men join the two halves of a thick, tightly fitted pipe connector.
An illustration to help it stick. The mechanism is in the sections above.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 5

A shop's storefront and its tRPC server live in one monorepo, and the storefront imports the server's type AppRouter. Yesterday's storefront build passed tsc and is live. Today the server renames the output field priceCents to unitPriceCents; the server's CI checks only the server package, and it deploys. The storefront is rebuilt tomorrow.

What does the live storefront show for the Tea tin, priced 1250?

Pick one

Question 2 of 5

A streaming service's players fetch episodes over gRPC with version 1 of the .proto: message Episode { string id = 1; string title = 2; int32 runtime_s = 3; }. Version 2 adds each episode's season. Three drafts were reviewed and one shipped. Since the deploy, players still on version 1 show "Harbour Lights" (2700 s, season 2) as 2 s long.

Which draft shipped?

Pick one

Question 3 of 5

A payouts screen shows three merchants, each with its name and latest payout. The client sends one GraphQL query, { m1: merchant(id: "m-1") { name payouts { amountCents } } m2: … m3: … }, with the same selection for m-2 and m-3. The merchants have 5, 3 and 4 payouts.

What comes back?

Pick one

Question 4 of 5

A chat app's public GraphQL API has user(id) { name friends { … } }; friends can nest to any depth, and every user has 6 friends. Some clients now nest friends 5 and 6 levels deep. The team's budget is 300 runs of the friends resolver per request, whatever a client sends.

Which change keeps every request within it?

Pick one

Question 5 of 5

A box office's kiosks are web pages in Chromium. Each must show seat holds live: a hold placed after the page connects should be pushed to it over one open connection. The hold service is one Node process with a plain HTTP port and a @grpc/grpc-js server whose WatchHolds streams holds; the team will not run another process.

How should the kiosk page receive the holds?

Pick one

Go deeper

Pro tips
  1. Cache a GraphQL read by moving it into the URL. Serve hot, shared queries as persisted-query GETs, and keep POST for mutations. When: a GraphQL read is the same for many users (steps 6–7).
  2. Count a GraphQL query's cost in a test; never time it. Wrap each resolver with a counter and assert a ceiling for the deepest query a client may send. When: a field returns a list of the same type, such as a user's friends (steps 4–5). Verify: the counts repeat exactly: 259 friends runs at depth 4, 9331 at depth 6. graphql.org also recommends query complexity analysis and trusted documents; not run here.
Source
  • graphql.org, Serving over HTTP: https://graphql.org/learn/serving-over-http/ · fetched 2026-09-28. A server "must handle the HTTP POST method for query and mutation operations, and may also accept the GET method for query operations"; GET "may only be used for query operations"; with persisted documents "the client [can] send a short document identifier instead of the full query text" (steps 3, 7). Valid as of 2026-09-28.
  • graphql.org, Caching: https://graphql.org/learn/caching/ · fetched 2026-09-28. In an endpoint-based API, "The URL in these APIs is a globally unique identifier that the client can leverage to build a cache"; "In GraphQL, there's no URL-like primitive" of that kind (step 6).
  • graphql.org, Security: https://graphql.org/learn/security/ · fetched 2026-09-28. "it's a good idea to limit the maximum depth of fields that a single operation can have"; rate limits by request count need "a different approach" for GraphQL.
  • RFC 9111 §2: https://www.rfc-editor.org/rfc/rfc9111#section-2 · the cache key "is composed from, at a minimum, the request method and target URI". RFC 9110 §9.3.3: https://www.rfc-editor.org/rfc/rfc9110#section-9.3.3 · "a POST request cannot be satisfied by a cached POST response because POST is potentially unsafe" (step 6).
  • tRPC, Response caching: https://trpc.io/docs/server/caching · fetched 2026-09-28. "Since all tRPC queries are normal HTTP GET requests, you can use standard HTTP cache headers"; batching is on by default, so cache headers need care with personal data (step 9). tRPC introduction: https://trpc.io/docs/ · types come from "TypeScript's type inference directly, with no code generation step" (step 8).
  • Protocol Buffers, proto3 guide: https://protobuf.dev/programming-guides/proto3/ · fetched 2026-09-28. The field number "identifies the field in the message wire format" and "cannot be changed once your message type is in use"; a missing field returns "the default value for that field"; names are serialized only "when using TextProto or JSON encodings" (step 10).
  • gRPC, Core concepts: https://grpc.io/docs/what-is-grpc/core-concepts/ · unary, server-streaming, client-streaming and bidirectional RPCs (step 11). gRPC blog, The state of gRPC in the browser: https://grpc.io/blog/state-of-grpc-web/ · re-fetched 2026-09-28: "there is simply no browser API with enough fine-grained control over the requests"; the design is to "have a small proxy in front of the gRPC server to translate the requests and responses to something the browser can use". Valid as of 2026-09-28.
  • MDN, EventSource: https://developer.mozilla.org/en-US/docs/Web/API/EventSource · fetched 2026-09-28. "An EventSource instance opens a persistent connection to an HTTP server, which sends events in text/event-stream format. The connection remains open until closed by calling EventSource.close().". Valid as of 2026-09-28.

Checked against the tools and docs as of 2026-09-28.