Skip to main content

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.

FeatureGraphQLtRPC
Language supportAny language (JS, Python, Go, Java, Rust...)TypeScript only (client and server)
SchemaExplicit SDL schema (schema-first or code-first)No schema – TypeScript types ARE the contract
Type safetyCode generation required (graphql-codegen)Zero-config – TypeScript inference from server router
Code generationRequired for typed clients (graphql-codegen, gql.tada)Not needed – import the router type directly
Query flexibilityClient specifies exact fields (over-fetching prevention)Server defines the output shape – no field selection
Network protocolHTTP POST (queries can use GET) + WebSocket subscriptionsHTTP (queries/mutations) + WebSocket (subscriptions)
Subscriptionsgraphql-ws (WebSocket), SSE (newer)WebSocket subscriptions via tRPC subscriptions
Federation / splittingApollo Federation 2, WunderGraph Cosmo, Hive (multi-team)Not supported – single monolithic router
File uploadsgraphql-upload (multipart), workaround requiredNative via FormData, standard Fetch
CachingNormalized client cache (Apollo Client, Relay)React Query (TanStack Query) cache by procedure key
IntrospectionBuilt-in introspection query, GraphiQL playgroundNo introspection – types visible only in TypeScript
Error handlingErrors array in response alongside dataTRPCClientError with code (BAD_REQUEST, UNAUTHORIZED...)
Middleware / pluginsApollo plugins, directives, persisted queriestRPC middleware (.use()), context, input validation (Zod)
Learning curveMedium – SDL, resolvers, N+1, DataLoader patternLow if you know TypeScript – procedures are just functions
Best forPublic APIs, multi-client, federation, large orgsTypeScript 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.