REST has dominated API design for fifteen years, but 2026 is the year most teams have at least two API styles in production: REST for public surfaces and something faster (gRPC or tRPC) for internal services. The right choice depends on who calls the API, what language they use, and what matters most — standardization, performance, or developer experience.
What changed in 2026
- tRPC v12 stabilised with first-class support for React Server Components and Next.js App Router, making it the default for new TypeScript full-stack projects.
- gRPC-Web improved enough that browser clients can call gRPC services without a proxy — though tRPC still wins for browser DX.
- Connect (by Buf) gained significant adoption as a gRPC-compatible protocol that works over plain HTTP/1.1 and HTTP/2, solving the browser gRPC problem cleanly.
- OpenAPI 3.1 and tooling like Zod-to-OpenAPI mean REST can regain type safety with less friction than before.
Core concepts
REST (Representational State Transfer) models the API around resources. Operations are HTTP verbs on noun URLs: GET /users/42, POST /orders, DELETE /sessions/abc.
RPC (Remote Procedure Call) models the API around actions. Operations are function calls: getUser(42), createOrder({...}), deleteSession("abc").
Neither is strictly superior — REST wins when resources are the right mental model; RPC wins when actions are.
Comparison table
| Dimension |
REST |
gRPC |
tRPC |
JSON-RPC |
| Transport |
HTTP/1.1 or HTTP/2 |
HTTP/2 |
HTTP/1.1 or HTTP/2 |
HTTP, WS |
| Encoding |
JSON (text) |
Protobuf (binary) |
JSON |
JSON |
| Type safety |
Via OpenAPI codegen |
.proto files |
Native TypeScript |
Manual |
| Browser support |
Native |
gRPC-Web / Connect |
Native |
Native |
| Streaming |
SSE / WS |
Bidirectional |
Subscriptions (WS) |
No |
| Payload size |
Larger |
~5× smaller |
Larger |
Larger |
| Best for |
Public APIs |
Internal microservices |
TS full-stack |
Command APIs |
REST: when it is the right choice
GET /orders?status=pending → list
POST /orders → create
GET /orders/42 → read
PATCH /orders/42 → update
DELETE /orders/42 → delete
REST maps naturally to CRUD resources. HTTP caching (ETags, Cache-Control) is automatic. Every HTTP client, load balancer, and CDN understands it. For public APIs, REST is almost always correct.
gRPC: when it is the right choice
// orders.proto
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (Order);
rpc StreamOrders (StreamRequest) returns (stream Order);
}
gRPC shines when services call other services at high frequency. Protobuf binary encoding is ~5× smaller than JSON equivalents and ~3× faster to serialize/deserialize. Bidirectional streaming is first-class. The cost: no browser support without Connect/gRPC-Web, and .proto files add a build step.
tRPC: when it is the right choice
// server: define a router
const router = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => db.user.findUnique({ where: { id: input.id } })),
});
// client: fully typed, no codegen
const user = await trpc.getUser.query({ id: "42" });
// ^? User | null — inferred from server type
tRPC eliminates the schema-first step entirely. The server types flow directly to the client. No OpenAPI, no .proto, no codegen. The tradeoff: TypeScript only. If you have non-TS consumers, publish a REST or OpenAPI interface too.
How to pick
- Public API consumed by anyone → REST + OpenAPI spec.
- Internal service-to-service at high volume → gRPC (or Connect for HTTP/1.1 compat).
- TypeScript monorepo, full-stack → tRPC. Best DX, lowest boilerplate.
- Simple command API (games, blockchain, protocol) → JSON-RPC.
- Mixed consumers (browser + server) → REST for browser routes, gRPC internally.
- You already have REST and need streaming → Add Server-Sent Events before migrating to gRPC.
Common mistakes
Forcing every action into a REST resource. Not everything is a noun. POST /orders/42/cancel is fine REST; POST /cancelOrder is fine RPC. Pick the model that matches the domain, not a purity ideal.
gRPC without Connect in browser-facing services. Plain gRPC-Web requires an Envoy proxy. Use Connect protocol (Buf) instead — it speaks HTTP/1.1 and HTTP/2 natively.
tRPC without input validation. Always use Zod (or Valibot) schemas on every procedure. Unvalidated inputs are a security risk even when TypeScript types are correct.
Versioning REST APIs in the URL (/v1/, /v2/) when field additions would have sufficed. Version only on breaking changes; additive changes are backwards compatible.
gRPC Any types. They defeat the purpose of Protobuf typing and are as unsafe as untyped JSON. Define concrete message types instead.
What to skip
- GraphQL for simple CRUD. It is powerful but adds complexity (N+1 problem, schema stitching, persisted queries) that most apps do not need.
- SOAP. It is still alive in enterprise, but no new system should choose it in 2026.
- Rolling a custom binary protocol. Use Protobuf or MessagePack. Custom wire formats have no tooling.
FAQ
Can REST and gRPC coexist?
Yes — this is the common production pattern. Expose REST for external consumers; use gRPC internally. A gateway (Kong, Traefik, Envoy) translates between them.
Is tRPC production-ready?
Yes. tRPC v11/v12 is used in production by many large TypeScript shops. The main constraint is TypeScript-only consumers.
Does REST support streaming?
Not natively in the request body. Use Server-Sent Events (one-way server push) or WebSockets (bidirectional). gRPC has cleaner streaming semantics.
What is the performance difference between REST and gRPC?
At the wire level, Protobuf is ~3–5× smaller and faster to parse than equivalent JSON. In practice, DB query time dominates — so the difference matters mainly at very high RPS (>10k/s) or on mobile with constrained bandwidth.
Where to go next