GraphQL vs. tRPC - And why tRPC wins in most use cases.

As a full stack developer building data-driven applications, choosing my API for fetching and managing data is critical for productivity, DX, and my ability to quickly iterate on ideas. In this post, I'll compare my experiences using GraphQL vs. tRPC in depth, including the key pros and cons I encountered with each approach while building an app with Next.js.

Why I Initially Chose GraphQL

I decided to use GraphQL for my Next.js application because of its purported benefits compared to REST:

  • Flexible querying to avoid over/under fetching data
  • GraphQL's query language allows you to specify only the fields needed by the client rather than getting entire objects
  • This prevents wasteful over-fetching and avoids extra network requests
  • Strong typing for reliability and developer confidence
  • The GraphQL schema defines an API contract with strong types that serves as documentation
  • Know exactly what data shape to expect from queries
  • Separates concerns between backend and frontend
  • Backend focuses on resolvers and data while frontend handles UI and consumption
  • Allows specialization instead of full stack work
  • Rich query language to fetch nested, relational data
  • GraphQL allows traversing relationships and nested fields in one query
  • No need for multiple backend requests
  • Caching/performance improvements
  • GraphQL encourages a single endpoint that can be aggressively cached rather than many endpoints
  • Potential for fewer network requests

Why GraphQL Became Problematic

However, as my Next.js application grew in size and complexity, GraphQL began to demonstrate significant downsides from both a technical and development velocity perspective:

  • High overhead of schema synchronization
  • Any backend or frontend changes could break compatibility and required constant schema updates
  • A lot of wasted time re-syncing schemas
  • Brittle client-server contract
  • The frontend and backend were tightly coupled to the schema as the source of truth
  • Made it painful to iterate quickly
  • Slower build times from code generation
  • To ensure correctness, queries were generated from schema rather than written manually
  • Generated files took long time to create
  • Imperative query language less intuitive than calling functions
  • GraphQL involves constructing query strings rather than calling Next.js component functions
  • More cognitive load composing queries vs. calling endpoints
  • Complex caching and performance improvements require Apollo or Relay
  • To realize performance benefits, significant additional tooling is needed beyond bare GraphQL
  • Counterintuitive compared to simple caching with React Query, SWR etc.

Key Advantages of Switching to tRPC

To resolve these issues and streamline development, I switched to tRPC which provided the following improvements:

  • No schema definition required
  • Backend APIs are defined as ordinary TypeScript functions rather than GraphQL types
  • No need to keep schema in sync
  • Changes immediately reflected between frontend and backend
  • Calling a backend function directly means no schema synchronization necessary
  • Faster iteration speed
  • Faster build times
  • No code generation step required with colocated endpoint definitions
  • Avoid slow query generation
  • More intuitive Next.js component APIs
  • Endpoint functions are called directly using Next.js hooks rather than constructing queries
  • Calling functions feels simpler
  • Excellent type safety
  • TypeScript endpoint functions are strongly typed for reliability
  • Retains key GraphQL benefit of type safety
  • Simpler caching
  • Endpoint functions can leverage React Query, SWR etc. for caching rather than complex GraphQL solutions
  • Leverage existing community solutions

Additional Benefits Observed

Beyond those architectural improvements, moving to tRPC delivered significant gains for my Next.js application:

  • Faster development velocity
  • Less time wasted on schema complexity and synchronization
  • Improved iteration speed
  • Backend changes rapidly reflected on frontend without friction
  • More focus on business logic
  • No longer bogged down by schema management and data plumbing
  • Superior developer experience
  • Calling functions more intuitive than constructing GraphQL queries
  • Easier adoption
  • tRPC integrates cleanly with existing code rather than requiring whole new system

Summary

In the end, while GraphQL appeared great on paper, the realities of leveraging it for a non-trivial Next.js app showed significant downsides. tRPC provided all the benefits I initially sought from GraphQL like type safety and flexible data queries while dramatically simplifying full stack development. For my needs, tRPC was overwhelmingly superior for developer experience and productivity.