For web eng friends who are building TypeScript full-stack applications, chances are you’ve either used tRPC or had it enthusiastically recommended to you. And for very good reasons: tRPC has revolutionized how we build type-safe APIs, eliminating the schema definition/code generation dance that plagued earlier approaches.

I’ve been a happy tRPC user since v9, and I’m not here to convince you to abandon it. The developer experience benefits are real. End-to-end type safety without code generation, excellent editor autocomplete, and runtime validation that just works. Plus, as the latest StackOverflow survey indicates, TypeScript developers continue to command higher salaries than their JavaScript counterparts.

But after adopting tRPC in several projects, from rapid MVPs to more complex applications, I’ve discovered there are legitimate scenarios where reaching for tRPC isn’t the slam-dunk decision that Twitter (or X, or whatever we’re calling it now) threads make it out to be.

When tight coupling becomes a liability

The magic of tRPC stems from the tight integration between your frontend and backend. Your client code directly imports types from your server procedures, creating a seamless developer experience. But this tight coupling can become problematic as your application grows.

A few months ago, I was working on a project where the frontend and backend were maintained by different teams. What started as a monorepo with tRPC quickly became unwieldy when the teams needed to operate on different release cycles. Every minor change to a procedure signature required coordination between teams, and the frontend was constantly chasing the backend’s type definitions.

tRPC could mitigate this with strategies like versioning procedures or maintaining a stable API layer within the monorepo, but these require additional discipline.

In this scenario, a more traditional REST API with OpenAPI specifications would have provided cleaner separation of concerns. The frontend team could have consumed a stable contract rather than being tightly bound to the backend implementation.

The monolith assumption

tRPC works beautifully in the monorepo, monolithic application model. But the moment you need to expose your API to third-party consumers or split your backend into microservices with different technology stacks, things get complicated.

On a recent e-commerce platform rebuild, we started with tRPC for internal services. However, when we needed to expose a public API for partners, we faced a dilemma: our beautifully type-safe procedures weren’t easily consumable by non-TypeScript clients. We ended up maintaining two parallel API layers, one with tRPC for our own frontend and another REST API for partners. The duplication created maintenance headaches and inconsistencies.

The integration complexity tax

When you need to integrate with existing systems or third-party services that don’t speak tRPC, you often end up with a patchwork of approaches.

Last quarter, I worked on a project that needed to communicate with a legacy Java backend, a GraphQL service, and a newer Node.js microservice. Using tRPC for just the parts we controlled meant constantly translating between different API paradigms. We ended up abandoning tRPC in favor of a more universal approach with REST and OpenAPI, which allowed for more consistent patterns across all integrations.

Scale considerations

While tRPC’s performance is generally excellent, there are egde cases where its default behavior isn’t optimal. When working on a data-intensive application with hundreds of concurrent users performing complex operations, we discovered that tRPC’s default batching and caching mechanisms required significant customization.

In comparison, mature REST frameworks had more out-of-the-box solutions for these performance challenges. The trade-off between developer experience and performance optimization became apparent when we started hitting scale.

The learning curve reality

Despite its relatively straightforward API, tRPC still represents a new paradigm for many developers. On teams with varying levels of TypeScript experience, the learning curve can be steeper than anticipated.

A few months bakc, I worked on a project with a team that had extensive REST API experience but limited TypeScript exposure. What I thought would be a productivity boost turned into a source of friction, with team members struggling to understand the error messages and type inference nuances. The developer experience benefits of tRPC are significantly diminished when half your team is fighting with the tooling rather than leveraging it.

Package ecosystem maturity

While the tRPC core is robust, the surrounding ecosystem for common needs like authentication, caching, and monitoring isn’t as mature as what you’ll find in established REST frameworks like Express or Fastify, or even GraphQL with Apollo.

This becomes evident when you need specialized middleware or integrations. Recently, I needed fine-grained request tracing for a performance audit. With Express, I had numerous battle-tested options. With tRPC, I found myself writing custom solutions and adapters.

When consistency trumps type safety

If your organization has standardized on a different API style, introducing tRPC may create inconsistency in your API design patterns and documentation.

I consulted for a company that had invested heavily in GraphQL and built internal tools around its introspection capabilities. Adding tRPC to the mix for new services would have fragmented their API approach and tooling, ultimately reducing overall productivity despite the local optimizations tRPC might have provided.

The LLM toolings consideration

Somewhat ironically, as LLM coding tools have become increasingly competent at generating type definitions and API integrations, some of tRPC’s manual labor savings have become less significant. In projects where I’ve leveraged LLM tools like Cursor extensively, the productivity gap between hand-coding REST API integrations and using tRPC has narrowed.

This isn’t to say that generated code is as elegant or maintainable as tRPC’s approach. But for teams already deeply invested in AI-assisted development workflows, the relative advantage of tRPC may be diminished. Though one must be careful here, as AI-generated code can easily create its own technical debt problems.

Thinking beyond the hype

The intent here isn’t to dissuade folks from using tRPC. For many projects, particularly greenfield TypeScript applications built by experienced TypeScript developers in a monorepo setup, tRPC remains an excellent choice.

However, as with any technology decision, context matters. The best engineers I know don’t blindly apply the same solution to every problem. They carefully consider the specific requirements, team composition, and ecosystem constraints of each project.