In 2026 there are five honest answers to “what API layer should I use?”: REST, GraphQL, tRPC, gRPC, and Connect. Each is the right call for a specific shape of system. This post is the decision guide.
The trade space
| REST | GraphQL | tRPC | gRPC | Connect | |
|---|---|---|---|---|---|
| Schema-first | Optional (OpenAPI) | Yes | Inferred from TS | Yes (proto) | Yes (proto) |
| End-to-end types | Via OpenAPI codegen | Via codegen | Native | Via codegen | Via codegen |
| Browser native | Yes | Yes | Yes | No (needs proxy) | Yes (HTTP/JSON) |
| Streaming | SSE / WebSockets | Subscriptions | Yes (subscriptions) | First-class | First-class |
| Protocol | HTTP/1.1, 2 | HTTP/1.1, 2 | HTTP/1.1, 2 | HTTP/2 binary | HTTP/1.1, 2, gRPC |
| Tooling maturity | Highest | High | High | High | Mid (newer) |
| Best for | Public APIs, webhooks | Multi-client orgs | TS fullstack | Backend-to-backend | Both |
REST + OpenAPI
The lingua franca. Use REST when:
- The API is public.
- Webhooks send events to your service.
- You want curl-able endpoints.
- Multiple clients (mobile, web, third-party) need a stable contract.
OpenAPI 3.1 is the schema. Generate clients in any language. Use Pydantic v2 for typed request/response. Use FastAPI or Hono — both auto-generate OpenAPI from typed handlers.
What REST does poorly: fine-grained client-driven queries. A mobile client wanting just name and email from a user gets the whole User object. Solutions: sparse fieldsets (?fields=name,email), HAL/JSON:API, or… GraphQL.
GraphQL
GraphQL solves the “I want exactly these fields” problem. The client sends a query; the server returns matching shape:
query {
user(id: 42) {
id
name
posts(first: 5) {
title
publishedAt
}
}
}
When GraphQL wins:
- Many clients with different needs. Mobile wants compact; web wants rich. One endpoint, two queries.
- Many backend services. GraphQL Federation (Apollo Federation, Wundergraph) joins schemas across teams.
- Deeply graph-shaped data. Social networks, marketplaces.
When GraphQL hurts:
- Caching is hard. HTTP caching is GET-based; GraphQL is mostly POST.
- N+1 problem. Without DataLoader, naive resolvers issue one DB query per nested field.
- Operational complexity. Schema versioning, query budgeting, persisted queries — all real work.
- Public surface area. Exposing arbitrary GraphQL queries lets clients construct expensive shapes. Mitigate with depth limits, query budgets, persisted queries.
In 2026 GraphQL is mature but not the default. Reach for it when its strengths solve a problem you have. Otherwise REST or tRPC.
tRPC
tRPC is the default for TypeScript fullstack in 2026. Server defines functions; client calls them with full type inference. No schema file, no codegen.
// server
export const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => db.users.findFirst({ where: eq(users.id, input.id) })),
createUser: publicProcedure
.input(z.object({ email: z.string().email(), name: z.string() }))
.mutation(async ({ input }) => db.insert(users).values(input).returning()),
});
// client
import { trpc } from "./trpc";
const user = await trpc.getUser.query({ id: 42 });
// ^^^^ fully typed end-to-end
What tRPC excels at:
- Same codebase. Works best in a monorepo where server and client share types.
- Refactor safety. Rename a server field; the TS compiler flags every client usage.
- Zero codegen. No
npm run gen. No OpenAPI parsing. Just imports.
When tRPC stops fitting:
- Multiple language clients (mobile native, third-party). Use REST/OpenAPI or gRPC instead.
- Public API. tRPC isn’t standard; non-TS clients have a harder time.
For TS-only fullstack apps, tRPC is the path of least resistance.
gRPC
gRPC is the right call for service-to-service communication with strict typed contracts:
- Binary protocol over HTTP/2.
- Code generation across every language.
- First-class streaming (server, client, bidi).
- Massive ecosystem — Kubernetes, Envoy, every cloud-native tool speaks it.
What gRPC does poorly:
- Browsers. Plain gRPC isn’t browser-callable; you need a proxy (gRPC-Web) or to use Connect.
- Curl debugging. Binary payloads aren’t human-readable.
- One-off endpoints — overkill for “give me the time.”
For backend-to-backend in 2026, gRPC remains the right choice. See Go + gRPC + Protocol Buffers for production patterns.
Connect
Connect
is the modern hybrid. The same .proto generates a server that speaks gRPC, gRPC-Web, and Connect’s own JSON-over-HTTP protocol. One server, three protocols.
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (Order);
rpc StreamOrders (StreamOrdersRequest) returns (stream Order);
}
A browser calls it as JSON over HTTP/1.1; a service calls it as gRPC. The handler is identical.
When to pick Connect:
- New project, both backend and frontend under your control.
- Want typed contracts, browser support, and gRPC ecosystem fit.
- Fine with adopting
buffor proto + codegen.
This is what I reach for in 2026 for greenfield projects that need a single API surface for browser + services. The story used to be “gRPC for backend, REST for browser” — Connect collapses that.
Decision matrix
| Scenario | Best choice |
|---|---|
| Public API consumed by anonymous clients | REST + OpenAPI |
| Webhooks | REST |
| TS monorepo, fullstack web app | tRPC |
| Mobile + web with shared backend | REST or GraphQL |
| Many backend microservices | gRPC or Connect |
| Backend services + browser, same project | Connect |
| Federated graph across many teams | GraphQL with Federation |
| Real-time streaming, server-to-client | SSE (with REST endpoint to start) |
| Bidirectional real-time | WebSockets or gRPC bidi |
| Internal admin tool | tRPC if TS, REST otherwise |
For streaming specifically, see SSE vs WebSockets in 2026 .
Common stack patterns
TypeScript fullstack (Hono / Next.js + your DB)
→ tRPC for the app’s own API; REST + OpenAPI for the public API. Consider Drizzle ORM for type-safe data.
Python backend, multiple clients
→ REST + OpenAPI with FastAPI auto-generation. See FastAPI + Pydantic v2 + SQLAlchemy 2.0 .
Microservices on Kubernetes
→ gRPC or Connect for service-to-service. REST at the edge. mTLS via Cilium and eBPF .
Mobile-first product
→ GraphQL if mobile teams ask for it. Otherwise REST.
Common mistakes
1. GraphQL because “it’s modern”
GraphQL’s complexity is real. If you don’t have multiple clients with different needs, you’re paying for flexibility you won’t use.
2. tRPC for non-TS clients
tRPC types are TypeScript types. They don’t exist in Swift. Don’t pick it if you have native mobile.
3. gRPC for the browser without Connect
Plain gRPC needs a gRPC-Web proxy in the path. Easier to use Connect from day one.
4. REST without OpenAPI
If the contract isn’t documented machine-readably, your clients will hand-roll types and they’ll drift. Generate OpenAPI; generate clients.
5. Multiple API layers in the same product
Pick one for the app’s own API. Add more only when the second has a clear, narrow purpose (e.g., REST for public consumers + gRPC for service-to-service).
What I’d ship today
For a new product in 2026:
- Public API → REST + OpenAPI (FastAPI or Hono with
@hono/zod-openapi). - Internal app API → tRPC if TS-only, else REST.
- Service-to-service → Connect or gRPC.
- Streaming → SSE with a sibling cancel endpoint (most cases).
Predictable, debuggable, replaceable.
Read this next
- Go + gRPC + Protocol Buffers — Production Microservice Patterns
- FastAPI + Pydantic v2 + SQLAlchemy 2.0 Production Patterns
- Modern TypeScript Backend with Hono on Bun
- Designing REST APIs That Don’t Suck
- SSE vs WebSockets in 2026
If you want a Hono + tRPC + Drizzle starter with auth and OTel tracing, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .