GraphQL, RPC, tRPC
Trade-offs across API styles. The decision tree.
REST is one way to design an API. There are at least four others you'll meet in real codebases: GraphQL, gRPC, tRPC, and Server Actions. They aren't replacements for each other; they're different tools for different jobs. This lesson is the side-by-side comparison so you can pick on purpose instead of by default.
The five styles, in one sentence each
- REST - verbs + nouns over HTTP. Universal, cacheable, slightly verbose for nested data.
- GraphQL - one endpoint, one query language. The client asks for exactly the fields it needs. Backed by a schema.
- gRPC - binary RPC over HTTP/2 using Protocol Buffers. Tiny, fast, strongly typed across many languages.
- tRPC- TypeScript-only RPC where the client imports the server's types directly. Zero codegen.
- Server Actions (Next.js, Remix, SvelteKit) - framework-built RPC where you call a server function from a component like a normal function.
Same feature, five ways
Let's look at the same operation across the five styles: "get article 42 with its author's name."
REST
GET /articles/42?include=author HTTP/1.1
# response
{
"id": 42,
"title": "Hello",
"author": { "id": 7, "name": "Ada" }
}Or you might do two requests: GET /articles/42 then GET /users/7. Server decides what to include.
GraphQL
query {
article(id: 42) {
title
author { name }
}
}{
"data": {
"article": { "title": "Hello", "author": { "name": "Ada" } }
}
}The client picks exactly the fields it wants. Mobile clients get small payloads. The web gets richer ones. Same endpoint, no custom routes per use case.
gRPC
service Articles {
rpc GetArticle(GetArticleRequest) returns (Article);
}
message GetArticleRequest {
int64 id = 1;
}
message Article {
int64 id = 1;
string title = 2;
User author = 3;
}
message User {
int64 id = 1;
string name = 2;
}const article = await articlesClient.getArticle({ id: 42 });
console.log(article.title, article.author?.name);Protobuf is compiled to typed code in any language. Wire format is binary, way smaller and faster than JSON. Browsers can't speak raw gRPC, so for browser clients you usually go through gRPC-Web or Connect-RPC.
tRPC
export const appRouter = router({
articles: router({
getById: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ input }) => db.articles.findOne(input.id, { include: { author: true } })),
}),
});
export type AppRouter = typeof appRouter;const article = await trpc.articles.getById.query({ id: 42 });
// 'article' is fully typed end-to-end. No codegen step.Both sides are TypeScript. The client imports the AppRouter type from the server. Autocomplete crosses the network boundary. Best dev experience when your whole stack is TS.
Server Actions (Next.js / React)
async function getArticle(id: number) {
"use server";
return db.articles.findOne(id, { include: { author: true } });
}
export default async function Page() {
const article = await getArticle(42);
return (
<article>
<h1>{article.title}</h1>
<p>By {article.author.name}</p>
</article>
);
}No fetch, no endpoint, no client library. You write a function with "use server" and the framework handles the RPC plumbing. For mutations, the function call becomes a POST under the hood.
The decision tree
- Public API with many third-party consumers? → REST + OpenAPI. Boring, universal, every language has a client.
- One frontend (web/mobile) talking to one backend, and you control both?
- All TypeScript? → tRPC (best DX) or Server Actionsif you're on Next.js or similar.
- Mixed languages? → GraphQL (single endpoint, multi-language clients, great for many features sharing data).
- Service-to-service inside a polyglot backend? → gRPC. Tiny payloads, generated clients in every language, streaming built in.
- Mobile app that needs to ship one query and get everything for a screen? → GraphQL. That was literally why Facebook built it.
- Quick internal admin tool? → REST or Server Actions, whichever your framework defaults to.
?include= params to a REST API, then think about GraphQL.Trade-offs nobody mentions
- Caching: REST gets HTTP caching for free (
Cache-Control, ETags, CDNs). GraphQL POSTs to one endpoint, so the HTTP cache can't see field-level changes. You build it in the client (Apollo, urql, Relay). - Discoverability: GraphQL ships a complete schema you can introspect. REST relies on OpenAPI. tRPC relies on your editor.
- Error stories:GraphQL famously returns 200 OK with errors in the body. REST uses status codes. gRPC uses status codes from a small enum. Pick a stack that doesn't fight your monitoring.
- Performance ceiling: gRPC over HTTP/2 with protobuf is the fastest. JSON over HTTP/1.1 is the slowest. GraphQL adds parsing/resolution overhead per request. Usually irrelevant. Sometimes load-bearing.
Quiz
Your team builds a public REST API consumed by 50+ third-party developers in many languages. You also need internal service-to-service traffic that's as fast as possible. What's a sensible stack?
Recap
- REST = nouns and verbs over HTTP. GraphQL = one endpoint, one query language. gRPC = binary RPC with protobufs. tRPC = TS-only end-to-end types. Server Actions = framework-built RPC.
- Public + polyglot consumers → REST. All-TypeScript app → tRPC or Server Actions. Service-to-service polyglot → gRPC. Many features sharing relational data → GraphQL.
- Caching is REST's superpower. End-to-end types are tRPC's superpower. Tiny binary payloads are gRPC's.
- Pick on purpose. Don't cargo-cult.