TypeScript earns its keep at the edges. Inside a single file, the compiler proves your function arguments match their callers. Inside one process, it proves a backend module and the component that imports it agree. The guarantees are real, and they are what let a large team rename a field across a hundred files and sleep at night. Then your app calls fetch("/api/users/123"), writes as User next to the .json(), and the type system goes silent. From that point on, the type annotation is a claim, not a guarantee. The database changed a column name, the server shipped it, the client still compiles, and the mismatch surfaces as undefined in a render three days after deploy.1

This article sits on top of two earlier ones. Native TypeScript 7 is about how fast the compiler runs, which is a real win but not the same problem as this one. And the Server Actions mutation model is one of the three architectures below, treated here as one option among several rather than the whole story. This piece is the layer neither covered: how verified types actually cross the network boundary at all.

This is the network boundary, and it is the one place where the entire promise of TypeScript quietly unplugs. Everything above the wire is verified. The wire itself is a raw JSON conversation between two processes that cannot see each other's types. In 2020 the only fix was discipline: hand-write an interface on each side, match the fields by eye, and hope the backend author updated theirs when they changed the schema. That is not type safety. That is a to-do list that drifts.1

2026 is the year the honest choices arrive. Three distinct architectures now carry real, compile-time-checked types across that boundary, and they do not overlap much. The craft is no longer "add types to your API." It is choosing which of the three gaps you are actually closing, and paying only for that one.

The boundary is the unit of failure

Before comparing tools, it is worth being precise about what "full-stack type safety" actually buys. The useful definition is the one the ecosystem converged on: a single change to a data shape propagates automatically from the database schema, through the API layer, to the component that renders it, without a human re-typing the shape at any stop along the way.21 When you add a phoneNumber column, the ORM's types update, the API's return type updates, the client hook's data type updates, and TypeScript paints red over every component that now mishandles an optional field. Nothing compiles until the whole chain agrees.

The inverse is where real incidents come from. Consider an account ledger where the backend ships a new field as an integer but the client was told to expect a string. No compile error fires, because the two sides never shared a type. No runtime exception fires, because the value is present. The app renders "2" where a human expects "Checking", and some downstream consumer acts on the wrong meaning for hours before anyone notices. A widely shared account of a type mismatch across three layers in a payment flow puts the cost of that class of bug in the millions for a single outage.2 Whether or not the exact figure holds, the shape of the failure is real: the most expensive bugs are the ones that pass tests, compile clean, and disagree about meaning at a boundary nobody typed.

That is the target. The type chain, not the type checker. And the chain has a structural requirement: one source of truth, and every layer derives from it rather than declaring its own copy. The moment two files define the "same" shape independently, they will drift, and drift is the bug.2

Where TypeScript stops: inside one process the compiler verifies types end to end, but a raw fetch call with an as-cast breaks the chain because the client hand-writes a shape the server never checked, so a renamed or retyped field compiles clean and fails at runtime.
Where TypeScript stops: inside one process the compiler verifies types end to end, but a raw fetch call with an as-cast breaks the chain because the client hand-writes a shape the server never checked, so a renamed or retyped field compiles clean and fails at runtime.

Where the types originate

The chain has to start somewhere, and in a modern stack that is the database schema. Two ORMs dominate the choice of how that schema becomes TypeScript, and they draw the boundary at different places.

Prisma is schema-first. You declare your models in a .prisma file using Prisma Schema Language, then run a code generation step that produces a fully typed client. The generated client is the source of truth for every query and relation, and its types are excellent: rename a model field and the client re-emits with the new shape. The cost is the generation step itself. A prisma migrate does not regenerate the client automatically, and teams have shipped the migration without running generate after it. The symptom is nasty precisely because it is silent: the old client inserts a null into a newly required column, the foreign key chain complains hours later, and nothing flagged the mismatch at build time because the client was simply stale.3

Drizzle is schema-as-code. You define tables in TypeScript with a SQL-like builder, and the schema definition itself is the type. There is no separate generation step to forget, because editing the schema file immediately changes the inferred $inferSelect type that flows into every query. It is light, roughly 7 KB minified and gzipped with no runtime dependencies, which makes it comfortable in edge and serverless runtimes. The trade is that it stays closer to SQL, so a team expecting an ORM that hides the database gets a query builder that assumes you know Postgres well enough to stay out of its way.4

Both feed the same downstream chain. The DB layer choice is really a decision about how many hand-tended steps sit between "I changed the schema" and "the types updated." Prisma interposes a codegen command that must run and be kept current. Drizzle removes that step at the cost of a thinner abstraction. Neither is wrong; the durable principle is that the schema, in whichever form, is the single authority, and you never hand-write an interface that mirrors your tables. A manual mirror is a second source of truth, and a second source of truth is drift waiting to deploy.23

Where DB types come from: Prisma keeps the schema in a .prisma file and runs a generate step to produce the typed client, so a missed generate after a migration leaves the client stale; Drizzle keeps the schema in TypeScript itself, so the inferred row type updates the moment the schema file changes, with no codegen step in between.
Where DB types come from: Prisma keeps the schema in a .prisma file and runs a generate step to produce the typed client, so a missed generate after a migration leaves the client stale; Drizzle keeps the schema in TypeScript itself, so the inferred row type updates the moment the schema file changes, with no codegen step in between.

The three ways across the wire

Here is the real 2026 decision, and it is sharper than the old "REST or GraphQL" debate because it is about the type contract rather than the transport. When your server and client are both TypeScript and both owned by one team, you have three honest options for getting verified types across the network.

Three ways to carry types across the wire: tRPC derives the client contract from a server router with no codegen but only serves your own TypeScript client; Server Actions make a server function the contract, lowest ceremony for forms, but need your own validation and auth on every action; contract-first tools keep the contract as a plain object and emit an OpenAPI document any consumer can hold, at the cost of ceremony.
Three ways to carry types across the wire: tRPC derives the client contract from a server router with no codegen but only serves your own TypeScript client; Server Actions make a server function the contract, lowest ceremony for forms, but need your own validation and auth on every action; contract-first tools keep the contract as a plain object and emit an OpenAPI document any consumer can hold, at the cost of ceremony.

tRPC: inference as the contract

tRPC takes the position that if server and client live in one TypeScript codebase, there is no reason the type system should stop at the network at all. You define a router of procedures on the server, and the client imports the router's type and gets exact knowledge of every procedure's input, output, and error codes, derived automatically with no schema file and no code generation. Calling a procedure on the client is a typed function call that happens to go over the wire, so when the server changes a return type, the client stops compiling anywhere it used the old shape. This was the philosophical shift the T3 stack encoded, and it is the closest a JavaScript full-stack team gets to the refactoring safety a Java developer takes for granted.15

The current major, v11, went GA on March 21, 2025, and the latest stable sits at v11.18.0 (released June 17, 2026). It requires TypeScript 5.7.2 or newer, which matters only in that it signals v11 leans on the stricter strict-mode behaviors. It integrates natively with TanStack Query v5, so reads, mutations, and cache invalidation ride on the ecosystem's standard server-state layer, and it supports subscriptions over WebSocket or Server-Sent Events for live updates.5

The honest cost is confinement. tRPC shares types by importing a TypeScript type from the server router, which only works when both sides are in the same TypeScript project and the client is your own. It is the wrong tool the moment you need to serve a mobile app, a third party, or any non-TypeScript consumer, because there is no OpenAPI document to hand them and no codegen path to a foreign-language SDK. Public API support is a grafted-on add-on, not a first-class surface. If the only client is your own web app, this is the lowest-ceremony way to close the boundary. If you will ever publish an API others consume, it is not your boundary tool.65

Server Actions: the framework-boundary mutation

Next.js Server Actions, finalized in React 19, take a different route. A function marked "use server" lives next to your components, and calling it from a client compiles into a typed POST under the hood. Because you import the function, TypeScript checks its arguments the way it checks any local call, so the function signature becomes the API contract and the boundary vanishes in the same codebase sense that tRPC achieves.7

A production account from a platform serving four million monthly users is worth reading closely, because it is unusually honest about where Actions win and where they stop. The team replaced a hybrid of API routes, tRPC procedures, and mutation hooks with Server Actions and deleted more than twelve thousand lines of scaffolding in the process. Form code shrank, progressive enhancement came back, and the client bundle for the home route dropped roughly 38 KB gzipped because the tRPC client and its hooks left the page. Median time to a successful mutation improved, and incidents tagged to the client/server contract fell by over a third over the following year. For the ordinary case of a form on a page writing to a database, Actions are the least ceremony of the three.7

The caveats are the point of reading the full account. Server Actions do nothing for validation, so the team standardized on a Zod-wrapped action factory from day one rather than letting each form hand-roll its checks. And critically, a Server Action is a public POST endpoint. The team saw a curious user trigger an admin-only action by replaying it from the network tab because the handler had no permission check, which is exactly the failure mode of an unguarded REST route wearing a friendlier API. Every action must carry an explicit auth policy, and the type signature alone enforces nothing at runtime.7

Actions are also framework-bound and transport-limited. They are a Next.js concept tightly coupled to React's rendering model, built for mutations and form submissions rather than general data fetching, and they do not give you a typed client you can reuse outside the app or a versioned API surface. The production account's own verdict is the sane one: default to Server Actions for mutations when your only client is your own Next.js app, and reach for tRPC or a real API the moment you need to serve a second consumer or keep a reusable typed client.78

Contract-first: OpenAPI as the boundary

When the boundary is genuinely public, polyglot, or contract-versioned, neither tRPC nor Server Actions fits, because both assume one TypeScript owner. The third option keeps the type contract as its own artifact and generates both sides from it. Contract-first TypeScript tools like oRPC and ts-rest define the API as a plain typed object, validate server handlers and client call sites against it, and emit an OpenAPI document for anyone who does not share your types. The contract object, not a router or a function, is the source of truth, and the OpenAPI surface is first-class rather than bolted on.6

The cost is ceremony. A contract that spells out every path, parameter, and response is more verbose than a tRPC router for ordinary CRUD, and the generation and validation tooling sits between you and a bare endpoint. This is the right price when the API outlives your repo, because a third party, a mobile team, or a Python service cannot import your TypeScript type but can absolutely consume your OpenAPI spec. For an internal SaaS with only your own TypeScript client, tRPC or Server Actions gives the same safety with less overhead. The decision table the ecosystem converged on is: own TS client and no public API means tRPC, a public REST or mobile surface means a contract-first tool, and a simple form on a page means Server Actions.68

Choosing the boundary tool: if your only consumer is your own TypeScript client, tRPC inference or framework Server Actions carry types across with no codegen; if you will publish a public REST, mobile, or polyglot API, a contract-first tool emits an OpenAPI document anyone can consume; validation and auth are still your job in every case.
Choosing the boundary tool: if your only consumer is your own TypeScript client, tRPC inference or framework Server Actions carry types across with no codegen; if you will publish a public REST, mobile, or polyglot API, a contract-first tool emits an OpenAPI document anyone can consume; validation and auth are still your job in every case.

The runtime is where types become opinions

Every one of these architectures shares a limitation that no amount of tooling removes, and ignoring it is how the chain still breaks. TypeScript types are erased at runtime. They prove at compile time that two pieces of code agree on a shape, but the wire carries plain JSON, and nothing on the other side of the network is obligated to respect your types. A malicious client, a stale client, or a buggy third party can send anything, and your input: z.object({...}) is not a nice-to-have, it is the actual security and correctness boundary.37

This is why validation libraries matter as much as the RPC layer. tRPC takes a validator on every procedure's .input(), and the canonical choice is a Zod schema, with tree-shakable Valibot or the TS-syntax ArkType as lighter alternatives. The same Zod schema that validates on the server can be the schema that types the client form and the schema your contract-first tool emits into OpenAPI. One definition, used for runtime parsing and compile-time inference, is the discipline that keeps validation and types from drifting apart. Server Actions, because the framework does nothing for them, demand that discipline most aggressively: type the input as unknown and parse it, or the function will happily accept whatever a caller POSTs.57

The corollary is that the fancy type-flow machinery is only as sound as its weakest validator. A chain that types every API boundary but leaves an untyped database query or an unvalidated Server Action is a chain that looks safe and is not. The production wisdom is blunt about it: if your frontend types are not inferred from a real source and parsed at a real boundary, they are fiction.27

Choosing honestly

The practical route is not to pick the most powerful tool but to pick the one that matches who consumes your data, then pay for validation everywhere regardless. For a greenfield Next.js app where one team owns both sides and there is no public API, tRPC v11 is the default that the ecosystem landed on, and it gives you the tightest read/mutation/cache loop with no codegen step to keep honest. If the app is mostly forms writing to a database and your only client is your own app, Server Actions are the least ceremony, provided you wrap every one in a validated, auth-gated factory from the first day rather than discovering the need after an incident. The moment a mobile app, a third party, or a second team in another language needs your data, stop hand-rolling and move the contract to a contract-first tool that can emit an OpenAPI document, because that is the only one of the three a foreign consumer can actually hold.678

Beneath all three, the same two rules hold. One schema is the single source of truth, and every layer derives from it; you never maintain a second hand-written copy of a shape. And at every network boundary, a runtime validator parses what actually arrives, because compile-time types describe intent and the wire does not care about your intent.

The value of TypeScript is not that it catches typos, though it does. It is that a single change to a truth propagates everywhere that truth is used, and the compiler refuses to ship a lie. The job of the layer above the database is to make sure the network does not reintroduce a place where a lie can quietly compile. Pick the tool that matches your consumers, validate at the boundary, and keep one source of truth. Do that, and the type system stops lying at the wire for good.

Sources

  1. PkgPulse, "The Rise of Full-Stack TypeScript: 2020 to 2026." Documents the era when the type system stopped at the module boundary, the as User cast, and the shared-types/type-chain shift that closed it. pkgpulse.com 2 3 4

  2. The Code Forge, "Full-Stack Type Safety: The $2.3M Type Mismatch." Full-stack type safety as a single change propagating from schema to UI; the four-layer model; the shared-package rule; "if your frontend types are not inferred from the API, they are fiction." thecodeforge.io 2 3 4 5

  3. The Code Forge, "The New T3 Stack in 2026." The Prisma type chain (schema to client to UI); the stale-Prisma-client-after-migration failure where a missing prisma generate writes silently against an old schema. thecodeforge.io 2 3

  4. Bytebase, "Drizzle ORM vs Prisma: Which TypeScript ORM Should You Use in 2026." Drizzle schema-in-TypeScript at roughly 7.4 KB gzipped with no runtime dependencies; Prisma schema-first with a generated client and its Prisma 7 rewrite. bytebase.com

  5. HiveWiki, "tRPC v11: End-to-End Typesafe APIs for TypeScript." v11 GA date and current version; the TypeScript 5.7.2 requirement; the client-imports-a-type mechanism; the validator choices (Zod, Valibot, ArkType) and no-codegen inference. hivebook.wiki 2 3 4

  6. StarterPick, "tRPC v11 vs oRPC vs ts-rest: Type-Safe RPC for SaaS Boilerplates 2026." The decision table: tRPC for a TS-only client and no public API, oRPC/ts-rest contract-first when you publish a public REST API or serve mobile/polyglot consumers. starterpick.com 2 3 4

  7. The Stack Stories, "React 19 Server Actions in Production: A Year of Lessons From a 4M-User App." Server Actions finalized in React 19; the 12,000-line API-routes deletion; validation-is-your-problem; the public-endpoint admin-action incident; the tRPC-vs-Actions-vs-REST comparison and its verdict. thestackstories.com 2 3 4 5 6 7 8

  8. PkgPulse, "tRPC v11: What Changed, Should You Upgrade?" The 2026 guidance that Server Actions cover simple forms while tRPC fits complex client orchestration and richer routers; the alternatives are better than they were. pkgpulse.com 2 3