· trpc / graphql / typescript
tRPC vs GraphQL in 2026 — when one beats the other
tRPC wins for TypeScript monorepos with no external API consumers. GraphQL wins the moment a non-TypeScript client exists. Here is when each choice pays off.
By Ethan
1,544 words · 8 min read
Use tRPC if your team is TypeScript all the way through, your clients and server live in the same repo, and you never need to expose that API externally. Use GraphQL the moment any of those conditions break — one non-TypeScript client makes tRPC’s value proposition disappear. Everything below is the part that earns that verdict.
Who this is for
TypeScript developers choosing an API layer for a new project, or considering a migration. The reader already knows what both tools are — this is not a tutorial.
If you maintain a public API or a polyglot backend, go straight to the verdict table. tRPC is probably not in play.
How we compared them
Versions under test: tRPC v11.17.0 (released 2026-04-28), graphql-js v16.14.0, GraphQL Spec September 2025, Pothos latest stable, GraphQL Code Generator latest. We used the Echobind/Bison migration as the primary real-world data point (bundle metrics from the Echobind blog post; line counts confirmed via GitHub PR #282), supplemented by maintainer statements in GitHub issues and practitioner experience from four Hacker News threads.
Type safety: closer than it looks
tRPC’s pitch is zero-codegen type safety. The server exports a router type, the client receives it as a TypeScript generic, and inference does the rest — no schema file, no generation step, no types that go stale between runs.
// server/router.ts
export type AppRouter = typeof appRouter;
// client.ts
const client = createTRPCClient<AppRouter>({ links: [...] });
const user = await client.getUser.query('id_bilbo');
// TypeScript knows argument and return types from inference alone
The catch, from the official FAQ: TypeScript ≥ 5.7.2 with "strict": true is required, and client and server must be in the same repo — or server types must be published as a private npm package. Without that constraint: “you will lose guarantees that your client and server works together.”
GraphQL has two typed paths. Pothos (code-first, no codegen) achieves the same guarantee via TypeScript generics and explicit field registration, with zero runtime overhead beyond graphql-js itself. Used in production at Airbnb and Netflix. GraphQL Code Generator generates types from your schema and operation files — but types go stale between runs and require a separate graphql-codegen --watch process.
Verdict on type safety: both can reach compile-time guarantees. Pothos eliminates codegen. The real DX gap is elsewhere.
The field selection gap — tRPC’s hard limit
This is tRPC’s most consequential architectural constraint.
GraphQL clients declare precisely the fields they need per query. Fragment colocation (Relay-style) lets each UI component own its data shape. Three views of a Product entity can request three different field subsets from the same endpoint.
tRPC has no equivalent. A maintainer confirmed in GitHub discussion #2592 that the json-mask workaround doesn’t preserve type inference. The practical alternatives are over-fetching (one procedure returns everything for all consumers) or procedure sprawl (a separate procedure per shape).
For a small API with clear, stable boundaries, this is a non-issue. For a large, deeply-relational data model with many UI permutations, it becomes a real tax.
Schema ownership and multi-team federation
Apollo Federation 2 lets multiple backend teams own separate subgraphs that the router composes into one supergraph. Each subgraph can use a different language or framework. Clients hit a single endpoint; the router coordinates.
# Team A's User subgraph
type User @key(fields: "id") {
id: ID!
name: String!
}
# Team B's Order subgraph
type Order {
id: ID!
buyer: User @external
}
tRPC has no equivalent. Importing TypeScript types from a Go or Python service is not possible. If your architecture is already polyglot microservices, tRPC is not in the conversation.
For a single-service monolith, Federation is overhead with no upside. The comparison only becomes meaningful when you actually have — or are explicitly building toward — multiple services.
Developer experience
Where tRPC genuinely wins against every GraphQL toolchain, including Pothos:
| Dimension | tRPC | GraphQL (Codegen path) |
|---|---|---|
| Autocomplete | Native TypeScript inference, always current | Requires codegen run; stale between runs |
| ”Go To Definition” | Jumps to server procedure source | Jumps to generated type file |
| Rename refactoring | Propagates server→client simultaneously | String-based query fields are opaque to TypeScript |
| Error HTTP status | Standard codes (400, 401, 403, 500) | Always HTTP 200; errors in body errors[] |
| Non-TypeScript clients | Alpha OpenAPI plugin only | Full language-agnostic support |
| Schema introspection | No built-in equivalent | GraphQL Playground / Introspection API |
The HTTP 200 issue is a real operational trap. GraphQL errors arrive as HTTP 200 with an errors field in the response body. Standard APM tools alert on 4xx/5xx — GraphQL errors are invisible without custom configuration. From HN thread 31284846: “GraphQL servers send 200s unless modified… turned into quite the headache for us.”
Bundle size and performance
The most credible performance number in this comparison comes from the Echobind/Bison migration — they moved the open-source Bison starter kit from Apollo/Nexus/GraphQL Codegen to tRPC + React Query:
- Bundle before: 81.2 KB minified + gzip
- Bundle after: 23.7 KB minified + gzip
- Net reduction: 3.5×
- Lines of code: −1,608 (removed 3,373, added 1,765)
The bundle difference is significant in serverless and edge function contexts where it maps directly to cold start time. For server-to-server calls, it’s largely irrelevant.
Raw query latency at realistic volumes is not a meaningful differentiator. Network time and database query shape dominate both. The N+1 footgun exists in GraphQL resolvers (DataLoader is the standard fix) but does not affect tRPC — though tRPC procedures can also return inefficient queries if you write them.
The decisive test
A practitioner on HN put it precisely:
“I use GraphQL because my clients are not all TypeScript based… My stack is therefore Rust with GraphQL with clients in React for web and Flutter for mobile.”
That is the test. One iOS app, one Flutter client, one Python script consuming your API — tRPC’s type safety does not apply to that client. You need a language-agnostic schema anyway. If you need both tRPC (for the TypeScript clients) and GraphQL or REST (for everyone else), you’ve added a layer without reducing complexity.
Someone with deep GraphQL production experience (ex-Twitch GraphQL infrastructure) chose tRPC at their startup: “I still love GraphQL, but man do I not miss all the work maintaining the ‘contract’ between front and back.” That is a DX win against real operational overhead, not a beginner’s impression.
The honest characterization of tRPC’s niche, from the same threads: “tRPC is for a single team working on TypeScript frontend to TypeScript backend — that’s niche.” Not a criticism. A precise description.
Verdict
Pick tRPC if:
- Single TypeScript team, client and server in the same monorepo
- Internal tooling: dashboards, admin panels, internal APIs
- No external API consumers, ever
- Team of 2–8, codegen infrastructure is overhead you can’t justify
- You’re on the T3 stack (tRPC ships as the default)
Pick GraphQL if:
- Any non-TypeScript client exists now or is likely: iOS, Android, Flutter, Python, third-party
- Backend is not Node.js/TypeScript (Go, Rust, Python)
- External developers consume your API
- Multiple frontend teams need independent query flexibility
- Multiple backend services need federation
- You need schema versioning and field-level deprecation
- Your data model is deeply relational and components need different field subsets of the same entities
The one scenario that ends the comparison immediately: a single non-TypeScript client. At that point you need a REST or GraphQL layer regardless — and if you’re maintaining both, tRPC stops simplifying anything.
If tRPC suits your stack, your next call is usually the database layer — Prisma vs Drizzle and Best TypeScript ORM 2026 cover that decision in the same depth-first style.
Related reading
- Deno vs Node.js — has the case finally landed?
- Prisma vs Drizzle 2026 — Full TypeScript ORM showdown
- Go vs TypeScript — backend service language pick for 2026
Caveats
We did not test tRPC’s alpha @trpc/openapi plugin (v11) for REST/non-TypeScript interop — it was not stable at publish time and the migration story may change as it matures.
The Echobind bundle numbers are from a 2022 migration of a specific starter kit. Your numbers will differ based on what you were using before and what your client graph looks like.
tRPC v11 (latest: v11.17.0 as of 2026-04-28) requires TypeScript ≥ 5.7.2 and Node.js 18+. React Query v5 is a peer dependency — the isLoading → isPending rename affects existing v10 code.
No documented “a year later” reflection on the Echobind migration exists as of publish date. The metrics are from the migration commit itself.
References
- tRPC FAQ — monorepo requirement
- tRPC v11 migration guide
- tRPC v11 announcement
- tRPC GitHub discussion #2592 — field selection
- tRPC GitHub releases
- graphql-js releases
- Pothos schema builder
- GraphQL Code Generator
- Apollo Federation 2 docs
- Echobind: Why we ditched GraphQL for tRPC
- Echobind/Bison GitHub PR #282 — verifiable metrics
- HN thread: TRPC — end-to-end typesafe APIs
- HN thread: Why use GraphQL? There is tRPC now
- HN thread: GraphQL Is a Trap?
- HN thread: People ditching GraphQL for tRPC