Full-stack TypeScript in 2025: the good, the meh, the bad

Monitor mostrando código TypeScript con sintaxis resaltada sobre escritorio de programador

For several years I resisted the idea of full-stack TypeScript. I came from architectures with Go or Python on the backend and TypeScript only on the frontend, and the idea of unifying felt like a convenience concession. But over the last two years I’ve worked on several projects where the stack is entirely TypeScript, and my opinions are more nuanced than they were. Time for a balance.

The point of this post isn’t to convince anyone to adopt full-stack TypeScript, or to dissuade them, but to offer an honest review of what I’ve seen work, what’s still a problem, and in what kind of project the choice makes sense.

The good

The most immediate advantage is the shared data model. When the same User or Product type lives on frontend and backend, and you don’t have to write it twice or sync it with an external generator, cognitive cost drops and misalignment errors vanish. It’s one of those things you don’t appreciate until you’ve lived it, because the alternative (OpenAPI, Protobuf, manual duplication) works but adds constant maintenance.

Tools like tRPC have turned this into much more than shared types. With tRPC, a procedure in the backend is automatically a typed function on the frontend, with full autocompletion, serialization and deserialization validation, and no external code-generation layer. It’s the kind of development that feels truly agile, with no direct analog outside the TypeScript world.

The second real advantage is the framework ecosystem that assumes full-stack from the start. Next.js with App Router, Remix (now merged with React Router), SvelteKit, Nuxt: all are designed to make writing a server function and consuming it from the client trivial. This integration has become so good that the line between frontend and backend has blurred, and in small-to-medium projects that simplifies a lot.

Third, typing quality has improved a lot. TypeScript 5.x has introduced significant improvements in inference, conditional types, and const types. The type derived from a Zod schema, for example, is often more expressive than what you’d write by hand, and runtime validation and compile-time checking use the same source of truth.

The meh

There are areas where the balance is more ambiguous and depends on the problem you’re solving.

Node.js backend performance is reasonable but not brilliant. For IO-bound services, it’s more than fine. For CPU-bound services or those with strong predictable-latency requirements, more appropriate alternatives remain (Go, Rust, even modern C#). The arrival of Bun and Deno has put healthy pressure on, and Bun especially has very interesting features (high startup performance, lighter ecosystem), but most real projects are still on Node for npm ecosystem compatibility and enterprise support.

Dependency management remains annoying. npm and pnpm have stabilized a lot, but having 1,200 packages in node_modules in a modest project is still normal. Most are transitive and you didn’t pick them. Each supply-chain attack on popular packages reminds you this is a big surface.

Frontend bundle size also deserves vigilance. With recent libraries (React Server Components, lighter runtime, better tree-shaking) it has improved, but it’s very easy for a project to bloat without noticing until load time makes it obvious. This is a structural problem of the full-stack TypeScript model and requires permanent discipline.

The bad

Some things remain frankly problematic.

The testing ecosystem is more fragmented than the rest of the language. Vitest has stabilized as a serious Jest alternative and is clearly better for new projects, but migrating large projects remains painful. Mocking libraries are many and none has become a clear standard. Integration with frameworks like Next.js for integration tests with real routes is still not first-class.

Frontend-backend friction in truly large services reappears. When the project grows enough, what looked like one codebase splits into two or three, and the benefit of sharing types fades. At very large scale, the advantage over a backend in another language with generated types can almost disappear.

The experience with TypeScript in third-party libraries remains uneven. Some libraries are excellently typed; others publish incomplete or incorrect types. When you hit a bad definition, fixing it means either forking, writing local type declarations, or living with any. No option is pleasant.

And compilation still hurts. A large TypeScript project compiles slower than its Go or Rust equivalent, sometimes by a lot. TypeScript 5.x improvements (project references, incremental, go-to-definition without compiling) have eased but not eliminated the problem. For those coming from fast-compiled languages, it’s an adjustment.

When it makes sense

After trying several setups, my current recommendation is:

Full-stack TypeScript is an excellent choice for small-to-medium product projects with small teams, fast iteration, and value in frontend-backend proximity. B2B SaaS, internal dashboards, admin tools, early-stage products where business rules change a lot and benefiting from frictionless shared types is a real advantage.

It’s a less clear choice for systems with heterogeneous components, where the backend does heavy work (data processing, complex calculations, real-time streaming), or where multiple independent teams work on the same system. In those cases, language uniformity stops making up for its limitations.

And it’s a bad choice when the real requirement is sustained high performance, low memory usage, or deployment in very constrained environments. There are languages designed for that, and forcing Node.js is an exercise in stubbornness.

What’s coming

In 2025 I expect consolidation rather than revolution. Bun will keep pressuring Node, which is good. Frameworks like Next.js will keep blurring the client/server line. Automatic typing from database schemas (à la Drizzle ORM) will spread. And tools like tRPC will see mature alternatives on other platforms (there’s work on OpenAPI + tRPC bridges).

What I don’t expect is a cycle reversal. Full-stack TypeScript has earned a legitimate slot and will probably keep it for years. It’s not the universal solution, but for a real segment of the market it’s a reasonable choice that, well implemented, delivers very good results. That’s what I’ve learned after two years, and what I wish I’d known when starting.

Entradas relacionadas