Back to blog

Leaving PHP, Part 5: frontend-backend type sharing is a different sport now

Part 1 looked at hiring. Part 2 looked at the runtime. Part 3 looked at the PHP type system in isolation. Part 4 looked at the ecosystem of packages on top. Part 5 looks at the seam between the backend and the frontend, and the productivity gap that opens when those two halves of your application cannot share a type system.

1
Number of type systems in a TypeScript-everywhere stack. Server and client read the same source of truth.
2
Number of type systems in PHP plus a TypeScript frontend, with a codegen step in the middle that may or may not be re-run.
~10 yrs
Of compounded tooling around end-to-end type safety on the JS side. Discriminated unions, inference, refactor moves, all of it.

The TypeScript-everywhere shape

In a TypeScript-on-both-sides stack, the type of an API response is defined once and consumed by both the server that produces it and the client that calls it. tRPC does this by exposing the server router as a typed client object that the frontend imports. Hono does it through its RPC mode, where the route definitions become a typed fetch wrapper for the client. Hand-rolled Zod schemas defined in a shared package serve the same purpose: parse on the server, infer on the client, one schema, two consumers.

The result is what people now call end-to-end type safety. A backend change to an API surface breaks the frontend build immediately, at the right line, with the right error message. There is no codegen step. There is no out-of-band schema document. The refactor moves the editor offers on the frontend, "rename this field", "add a parameter", "narrow this union", are aware of every call site on both sides of the wire.

This is not a marginal improvement on the previous decade's tooling. It is one of the largest productivity force multipliers in web development of the last ten years, and teams that have it for the first time say so loudly and repeatedly.

The PHP-plus-JavaScript shape

A PHP backend with a TypeScript frontend cannot do any of that. Not because anyone is doing anything wrong, but because there is no shared source of truth to derive types from. The backend's types, such as they are, live in PHP files that the frontend's TypeScript compiler will never read. The frontend's types live in .ts files that the backend's PHP runtime will never enforce. The two sides have to be reconciled by a third artifact, which is where the cost comes from.

The reconciliation strategies in 2026 look roughly like this:

Strategy What you write What can go wrong
OpenAPI + codegen PHP annotations or YAML, generated TS client Regeneration not enforced; client drifts from server
Hand-written DTO classes PHP DTOs and matching TS interfaces by hand Manual sync; renames forget the other side
JSON Schema + codegen Schema docs as source of truth Less ergonomic than native types in either language
Nothing any on the frontend, tests on the backend Mismatches discovered in production

Every one of these strategies works. None of them gives you what tRPC, Hono RPC, or shared Zod schemas give a TypeScript-everywhere team for free.

Where the bills come due

The cost of two type systems that do not talk is rarely catastrophic. It is steady. It shows up in three places.

1. Refactor confidence

Renaming a field on a server-side DTO, adding a new required parameter to a request, narrowing a union on a response: in a TypeScript-everywhere stack, the compiler tells you every call site that needs to change. In the PHP-plus-JS shape, the PHP side tells you about its callers, the TypeScript side tells you nothing until you regenerate the client, and the regeneration is a step somebody has to remember to run. The same refactor takes more time and lands less safely.

2. Onboarding speed

A new engineer in a TypeScript-everywhere codebase opens any file, follows the types into the server, and learns the shape of the system by reading the editor's tooltips. In a two-language codebase, the same path stops at the seam: the engineer has to learn the OpenAPI generator, or the DTO convention, or the JSON Schema layout, before they can follow a request from frontend to backend. The seam is a third thing to learn that is not part of either language.

3. Error budget

Mismatched types between the server and the client are bugs that the type system cannot prevent. They land in production. Frequency depends on team discipline, but the floor is not zero, and the floor in a single-language stack is much closer to zero. Over a year, the budget difference is real.

A scorecard for the seam

No shared source of truth
PHP types and TS types are two artifacts reconciled by a third. The third always lags.
Codegen is opt-in
Nothing in the build forces OpenAPI regeneration. The first signal of drift is a runtime error.
Refactor across the seam is manual
A field rename on the server is a server-only operation until somebody updates the frontend separately.
Onboarding now includes a third thing
The seam is its own concept to learn before any feature work can start.
=
The tooling on the JS side keeps compounding
tRPC, Hono RPC, Zod, ts-rest, Effect Schema. None of it can be reached from PHP without leaving PHP.

This is the gap. It is not a deal-breaker on day one. It is a tax on every feature you ship, and the rate goes up every year that the TypeScript-everywhere world adds another piece of tooling on top of the shared-type assumption that PHP cannot participate in.

Up next in the series

The talent argument was about who. The runtime argument was about what it runs on. The type argument was about the artifact. The ecosystem argument was about what the artifact can reach. The type-sharing argument is about how cleanly the artifact talks to its other half. Part 6 moves to deployment: container shape, cold starts, sidecars, and the operational profile of a modern application.

If you want to start the migration before Part 10 lands, book a demo and we will walk you through what Pext does to your codebase.