GraphQL vs tRPC
GraphQL and tRPC solve the same core problem differently: how do you let a client request exactly the data it needs from a server? GraphQL does it with a runtime query language. The client writes a query like { user(id: 1) { name email } }, the server resolves it against a schema, and returns exactly those fields. The schema is the contract. It is language-agnostic, supports federation (multiple subgraphs), and has a rich ecosystem of tools: Apollo Studio, GraphiQL, Relay, persisted queries, code generation. tRPC does it with TypeScript's type system. There is no schema language. The server defines procedures (queries, mutations, subscriptions) as TypeScript functions. The client imports the router's TypeScript type and gets full IntelliSense, autocomplete, and type checking without any code generation step. The contract IS the TypeScript type. If you change a procedure's return type on the server, the client gets a compile error immediately. The tradeoff is language lock-in. tRPC only works if both client and server are TypeScript. The moment you add a mobile app in Swift, a data pipeline in Python, or a partner consuming your API, you need a language-agnostic contract. GraphQL (or REST + OpenAPI) scales to polyglot environments. In 2026, a common pattern is: tRPC for internal frontend-to-backend communication in a Next.js monorepo, GraphQL for the public or partner-facing API that needs introspection, playground, and multi-client support.
GraphQL is a language-agnostic query protocol with a schema, introspection, and a client-driven query model. tRPC is a TypeScript-only RPC framework that uses Zod schemas and TypeScript inference to provide end-to-end type safety with zero schema duplication. GraphQL fits multi-team, multi-client, or public API scenarios. tRPC fits TypeScript monorepos where the frontend and backend are co-located or in the same repo.
| Feature | GraphQL | tRPC |
|---|---|---|
| Language support | Any language (JS, Python, Go, Java, Rust...) | TypeScript only (client and server) |
| Schema | Explicit SDL schema (schema-first or code-first) | No schema – TypeScript types ARE the contract |
| Type safety | Code generation required (graphql-codegen) | Zero-config – TypeScript inference from server router |
| Code generation | Required for typed clients (graphql-codegen, gql.tada) | Not needed – import the router type directly |
| Query flexibility | Client specifies exact fields (over-fetching prevention) | Server defines the output shape – no field selection |
| Network protocol | HTTP POST (queries can use GET) + WebSocket subscriptions | HTTP (queries/mutations) + WebSocket (subscriptions) |
| Subscriptions | graphql-ws (WebSocket), SSE (newer) | WebSocket subscriptions via tRPC subscriptions |
| Federation / splitting | Apollo Federation 2, WunderGraph Cosmo, Hive (multi-team) | Not supported – single monolithic router |
| File uploads | graphql-upload (multipart), workaround required | Native via FormData, standard Fetch |
| Caching | Normalized client cache (Apollo Client, Relay) | React Query (TanStack Query) cache by procedure key |
| Introspection | Built-in introspection query, GraphiQL playground | No introspection – types visible only in TypeScript |
| Error handling | Errors array in response alongside data | TRPCClientError with code (BAD_REQUEST, UNAUTHORIZED...) |
| Middleware / plugins | Apollo plugins, directives, persisted queries | tRPC middleware (.use()), context, input validation (Zod) |
| Learning curve | Medium – SDL, resolvers, N+1, DataLoader pattern | Low if you know TypeScript – procedures are just functions |
| Best for | Public APIs, multi-client, federation, large orgs | TypeScript monorepos, Next.js apps, single-team projects |
When to use GraphQL
GraphQL is the right choice when: your API will be consumed by multiple clients (web, iOS, Android, third parties), you need a public API with introspection and a playground, you have multiple teams owning different parts of the schema (Federation), your clients are built in different languages, or you need normalized client-side caching with Apollo Client or Relay. GraphQL's explicit schema is also better for documentation and API governance.
When to use tRPC
tRPC is the right choice when: your entire stack is TypeScript, your frontend and backend live in the same repo (monorepo) or share a package, you want zero code generation overhead, you want compile-time end-to-end type safety without thinking about it, and your API is not consumed by external parties. The T3 Stack (Next.js + tRPC + Zod + Prisma) is the canonical tRPC use case.
Common Mistakes
- Using tRPC for a public API – tRPC exposes TypeScript procedure names and signatures as URL paths (/api/trpc/user.getById). This is not a stable, documented, versioned API contract. External consumers cannot easily introspect or generate clients.
- Using GraphQL for a simple TypeScript monorepo – GraphQL adds real complexity: SDL definition, resolver wiring, N+1 DataLoader pattern, code generation pipeline. For a single-team TypeScript app, tRPC delivers type safety with far less ceremony.
- Ignoring tRPC's lack of field selection – tRPC always returns the full procedure output. If a procedure returns a large object and the client needs only 2 fields, you are over-fetching. In GraphQL, the client selects exactly the fields it needs.
- Not using Zod with tRPC input validation – tRPC input parsers (typically Zod schemas) validate inputs at runtime. Skipping input validation means type errors are caught only at compile time, not at runtime against untrusted client data.
- Mixing tRPC and REST in the same Next.js app without a plan – tRPC procedure calls and REST endpoints coexist fine, but be intentional. Use tRPC for internal UI data fetching. Use REST endpoints (or a separate API) for webhooks, OAuth callbacks, file uploads, or any non-TypeScript consumer.
FAQ
Can I use tRPC with React Native?
Yes. tRPC works with any TypeScript client including React Native. You share the router type from your server package and use @trpc/react-query on the client. The network transport is plain HTTP, so React Native's fetch works without modification.
Can tRPC replace GraphQL in a large organization?
Generally not. Once you have multiple teams, multiple clients, or non-TypeScript consumers, tRPC's language lock-in and lack of federation become significant limitations. Large organizations typically use GraphQL Federation to let teams own their own subgraphs independently. tRPC has no equivalent.
Does tRPC support file uploads?
Not natively via the standard tRPC procedure mechanism, but you can handle file uploads through a standard Next.js API route (or Route Handler) alongside tRPC, or use FormData with a custom link. The tRPC team recommends keeping file upload endpoints separate from tRPC procedures.