ts-rest vs tRPC vs OpenAPI codegen: how to choose
ts-rest vs tRPC vs OpenAPI codegen: compare contract strictness, team fit, and glue code so you can choose the API approach that saves time.

The problem teams run into
Teams usually start with a simple plan: ship the backend, connect the frontend, and keep types in sync. That works for the first version. Then a few fast releases go out, one endpoint changes shape, a field becomes optional, and the frontend still calls the old contract.
People patch the gap by hand. One developer writes a mapper in the client. Another copies backend types into the web app. The mobile app adds its own adapter because it cannot reuse the same helper. None of this looks serious at first. A few weeks later, the codebase has small piles of glue code scattered everywhere.
The cost is not just bugs. It is time lost on dull checks and repeated fixes. Someone spends 20 minutes figuring out whether "status" is a string, an enum, or two different things in two repos. Error handling gets messy too. The server returns one shape, the client expects another, and nobody wants to touch the old adapter that "mostly works."
A small team can ignore this for a while. Picture a startup with one backend engineer and two frontend engineers shipping three times a week. They move fast, but they also change payloads fast. Without a shared contract, they spend more time repairing breakages after each release than building new features.
Teams also need different levels of API contract strictness. A public API needs stability, versioning, and clear docs because outside users depend on it. An internal app often needs speed more than ceremony, especially when the same team owns both sides and updates them together.
That is why the ts-rest vs tRPC vs OpenAPI codegen choice is really about ownership and change rate. If one team controls frontend and backend, a tighter contract saves rework. If several teams, outside customers, or long-lived clients depend on the API, stricter boundaries usually save more pain later.
What you're actually choosing
This decision is less about syntax and more about how your team works. You are choosing how tightly frontend and backend move together, how painful a breaking change will be, and how much tooling you want to maintain.
If one team owns both sides, tight coupling often feels fine. The same people can change a route, update the client, run tests, and ship in one pass. In that setup, strong type sharing removes a lot of glue code because nobody waits on another team.
That changes when the API has several consumers. A web app, mobile app, admin panel, and partner integration do not update on the same day. Once multiple clients depend on the same contract, small backend changes stop being small. A field rename turns into support work, versioning, and extra release steps.
Breaking changes are where teams often misjudge the trade. If you can absorb them quickly, a tighter contract is easy to live with. If you cannot, you need a contract that is explicit, stable, and easy to share outside one codebase.
Tooling cost matters too. Some teams are happy to maintain generators, schemas, shared packages, and build steps. Most are not. Be honest about that. A cleaner contract on paper is not better if nobody wants to maintain it six months later.
A simple rule works well:
- One team, one main client, fast releases: tighter contracts usually fit.
- Several clients, slower releases, outside consumers: explicit contracts age better.
- Low patience for tooling: choose the path with fewer moving parts.
That is why different teams pick different tools and all feel right at first. The hard part is matching contract strictness to how you ship, how many clients you support, and how much maintenance work you will actually tolerate.
When tRPC fits best
tRPC fits best when one TypeScript team owns both the frontend and the backend. The same people define procedures, inputs, and return types, then use them on both sides without writing a separate contract by hand. That removes a lot of drag when the product changes every week.
The main advantage is proximity. Server logic and client types stay close together, so a renamed field or changed input usually fails at compile time instead of turning into a late QA bug. For internal dashboards, admin panels, and early SaaS features, that speed is hard to ignore.
A small startup is the clearest fit. Imagine two engineers building a React app and a Node backend. They ship often, change the data model all the time, and do not need a public API yet. In that setup, tRPC feels simple. It keeps momentum high and avoids extra schemas, generators, and wrappers just to pass JSON around.
tRPC also works well during early product work, when the API is still settling and the team needs fast feedback. That is common in startups. Ideas change every week. If the same people touch both sides of the stack, a direct TypeScript-first setup usually feels lighter.
The awkward part shows up when the API stops being just for TypeScript. If a Swift mobile app, a Kotlin service, a Python worker, or a partner integration needs the same contract, tRPC gets less comfortable. Its strength comes from TypeScript talking to TypeScript.
If your stack is mostly TS and the API is mainly internal, tRPC is often the cleanest way to get type-safe API contracts without adding much tooling.
When ts-rest fits best
ts-rest is the middle ground many TypeScript teams actually want. It keeps the API as normal HTTP, with explicit routes, methods, params, and status codes, but it does not force a heavy spec-first workflow.
That matters when your team likes REST and wants the contract to stay visible. A frontend developer can still see that an endpoint is GET /projects/:id with a certain response shape. Nothing feels hidden behind function calls, and you do not need much custom wiring to keep client and server in sync.
ts-rest works well when more than one client uses the same backend. A web app, an admin panel, and a mobile app can all share the same endpoint shapes without guessing how requests should look. That is often a better fit than tRPC when your API needs to behave like an API, not just an internal TypeScript pipe.
A common startup example looks like this: a Next.js app for customers, a mobile app for staff, and maybe a small admin tool. They all need the same orders, auth, and reporting endpoints. With ts-rest, the team keeps one contract in TypeScript and reuses it across both sides while still speaking plain HTTP.
The trade-off is simple. ts-rest asks for more design work up front than tRPC. You need to decide route names, request shapes, status codes, and error responses before you move fast. Some teams find that annoying in the first week. Many end up glad they did it once the second or third client appears.
If you want clear contracts over HTTP without going all the way to full OpenAPI codegen, ts-rest is often the most practical choice.
When OpenAPI codegen fits best
If your API will leave the building, OpenAPI codegen is usually the safer bet. It fits best when more than one app, team, or outside company depends on the same backend. A written spec gives everyone the same source of truth, even if they never open the server code.
This matters most for public APIs and products with many kinds of clients. A React app might be only one consumer. Add an iPhone app, an Android app, an admin tool, and a partner integration, and loose agreements start to break. OpenAPI gives you the strongest external contract of the three because the contract exists on its own, outside your TypeScript code.
It tends to fit when:
- you have web, mobile, and partner clients using the same endpoints
- some consumers are not written in TypeScript
- different teams ship on different schedules
- backward compatibility matters more than moving fast for one sprint
In that setup, generated clients and models cut down on the small, constant mistakes. People stop arguing about field names, nullable values, error shapes, and pagination rules.
The cost is real, though. Someone has to maintain the spec, review changes carefully, and run generation as part of the build or CI flow. Generated code can feel awkward too, so teams still end up writing wrappers around it. OpenAPI codegen does not remove all glue code. It shifts more of the work into contract design and review.
That trade is often worth it when stability matters more than speed. If you are building a public API, supporting several client platforms, or planning long-lived integrations, the extra process is insurance.
How to choose without overthinking it
Most teams make this harder than it needs to be. They start with a favorite tool, then force every API decision around it. That usually creates more work later.
A better approach is to look at who will call the API and how often those callers change. That tells you how much strictness you actually need.
Start by listing every client that touches the API: the main web app, mobile apps, admin tools, background jobs, internal scripts, and outside integrations. Then mark which clients change often and which need to stay stable for months. After that, pick the lightest contract that still protects the stable clients from breakage.
You do not need a giant migration plan to test this. Try one medium-sized feature first. Account settings, billing, search, or an admin workflow is enough. Those features are messy in normal ways, which makes them useful for judging real friction.
A small startup makes the trade-offs easy to see. Say you have one React app for customers, one internal admin panel, and one mobile app. Two engineers own frontend and backend together, so every extra layer costs time.
If most product work lives in React and TypeScript, tRPC often feels best. One engineer changes a procedure, the other updates the UI, and they keep moving.
If the mobile app already matters, or you expect mixed clients soon, ts-rest is usually the safer middle ground. The API stays clear and standard, and the move to more clients hurts less.
If separate teams, partners, or public API users already depend on the backend, wait less and formalize the contract with OpenAPI.
Do not standardize after a toy demo. Build something real. If that feature needs wrappers, duplicate types, or hand-written fixes to keep frontend and backend aligned, your first choice is probably too loose or too strict.
The best tool is usually the one that protects the parts that stay still and stays out of the way for the parts that move.
Where glue code still shows up
Even with shared contracts, a few seams stay manual. These tools cut a lot of waste, but they do not decide how your app handles sessions, edge cases, or transport details.
Auth is the most common example. Your client still needs to attach headers, detect an expired token, refresh it, and retry the request once. A contract can tell you which endpoint needs auth. It will not decide where refresh logic lives or how you stop three failed requests from trying to refresh at the same time.
Small data mismatches also survive every approach. The backend may send a date as an ISO string while the UI wants a Date object for sorting and display. Enums look simple until one side adds "archived" and the other still expects only "active" or "disabled." null and undefined cause the same kind of trouble. The contract says a field is optional, but your form code treats missing and empty as two different states.
Error handling drifts faster than most teams expect. One route returns { code, message }, another adds details, and a third sends plain text because somebody copied old middleware. If you define one error shape early, the frontend can show messages consistently and your logs make more sense.
Uploads and streaming need extra care because they do not behave like plain JSON requests. File uploads need rules for size limits, content type, progress, and failure messages. Streaming routes need decisions about reconnects, partial data, and timeouts.
A short checklist helps:
- define one error format
- decide how dates become UI values
- handle
nullandundefinedon purpose - write auth retry rules once
- test uploads and streams by hand
Teams that sort out these pieces early write much less glue code later.
Common mistakes early on
Many teams choose the strictest setup before the API stops moving. That feels careful, but it often creates busywork. When endpoints, field names, and error shapes still change every few days, a rigid setup makes simple product changes feel expensive.
Another common mistake is mixing patterns across services for no clear reason. A team uses tRPC for internal tools, ts-rest for one app, and OpenAPI for a public service because each developer picked a favorite. The result is more overhead, not less. People have to remember different rules for errors, auth, validation, and client generation.
Generated clients also do not rescue a bad API. If routes have unclear names, list endpoints return different shapes, or one service uses three error formats, codegen will reproduce every rough edge. The frontend still needs adapters, special cases, and defensive checks.
Teams also forget that contracts only help when tests and docs are part of normal work. A startup team might generate a fresh client after every backend change, but if nobody runs contract tests in CI or updates request examples, the typed client creates false confidence. The code compiles, yet the real integration still breaks.
A better habit is boring and effective: pick one pattern per boundary, keep route design consistent, and update docs and tests on the same day as the contract.
Quick checks before you commit
A good API tool should survive a boring Tuesday. Give a new teammate one small task, like adding a route that reads a record and returns two fields. If they spend most of the hour wiring types, adapters, and client helpers, the setup is too heavy for your team.
A few blunt questions help:
- Can someone add one route, call it from the frontend, and handle one error case in under an hour?
- If a backend developer renames a field, does the frontend fail in local dev or CI before release?
- Can a second client, like a mobile app or an internal script, use the same contract without custom wrappers?
- Does the tool fit the team you have now, with your current stack and habits?
That last question matters more than most people admit. Teams often choose for a future org chart that does not exist yet. A small TypeScript team can move very fast with tRPC. A team that needs clearer HTTP contracts, or expects more than one kind of client, usually feels better with ts-rest or OpenAPI codegen.
One simple exercise tells you a lot. Pick one real endpoint and sketch it three ways. Include request validation, auth context, one frontend call, and one breaking change. You will learn more from that than from any feature comparison table.
What to do next
Pick one feature and test your choice on real work for a week or two. Do not start with the whole product. A small but messy feature works best, like auth, billing, or a dashboard page that touches several endpoints.
Watch the friction, not just whether the demo works. Teams usually learn more from one annoying week than from three architecture meetings.
Keep notes on four things: how often frontend and backend drift apart, how much hand-written mapping code you add, how long simple API changes take end to end, and whether a new team member can follow the setup without help.
Set a review date before you start. Two to four weeks is usually enough. Early choices feel permanent when they are not. If the tool fits now but starts to hurt once the app gets more users, more services, or outside consumers, you want a clear moment to reconsider it.
Keep your escape hatch simple. Store schemas close to route logic, avoid clever wrappers, and keep client generation boring. That gives you room to move toward stricter contracts later without a full rewrite.
If you want a second opinion before standardizing on one approach, Oleg Sotnikov at oleg.is reviews API design, infrastructure, and team workflow as part of his Fractional CTO work with startups and small businesses. That kind of review is most useful before one pattern spreads across the whole codebase.
Frequently Asked Questions
How do I choose between tRPC, ts-rest, and OpenAPI codegen?
Start with your clients, not the tool. If one TypeScript team owns frontend and backend and ships fast, use tRPC. If you want clear HTTP routes and expect more than one client, use ts-rest. If outside teams, partner apps, or public users depend on the API, choose OpenAPI codegen.
When does tRPC fit best?
Use tRPC when one TypeScript team controls both sides and changes the product often. It keeps types close to server code and catches many contract mistakes during development. That works well for internal tools, admin panels, and early SaaS features.
Why would I choose ts-rest over tRPC?
Pick ts-rest when you want normal REST endpoints with shared TypeScript contracts. It gives you visible routes, methods, params, and status codes without a full spec-first flow. Teams often like it when a web app, admin panel, and mobile app share one backend.
When should I go straight to OpenAPI codegen?
Choose OpenAPI codegen when the API has to stand on its own. That usually means public APIs, partner integrations, separate teams, or non-TypeScript clients. You spend more time on the contract, but you save time later on breakages and mismatched client code.
What if I already have both web and mobile clients?
A mobile app changes the trade right away. tRPC loses some of its appeal because its best path depends on TypeScript on both sides. If the mobile app matters now, ts-rest usually gives you a cleaner path. If several mobile and partner clients depend on the same API, OpenAPI usually fits better.
Will one of these tools remove all glue code?
No. They cut down a lot of repeated type work, but they do not solve every seam in your app. You still need to handle auth, date parsing, error formatting, uploads, streams, and a few UI-specific transforms by hand.
Where does glue code usually still show up?
Auth logic causes trouble first. Your client still has to attach tokens, refresh them, and avoid three retries at once. Date fields also trip teams up because the server sends strings while the UI wants real date objects. Error shapes, null handling, uploads, and streaming routes need clear rules too.
Is OpenAPI too heavy for an early startup?
Yes, if your API still changes every few days and only one small team uses it. In that stage, a heavy spec and code generation flow can slow simple product work. Start lighter, prove the product shape, then tighten the contract when more clients or teams depend on it.
Should I use different tools across different services?
Usually no. Pick one pattern per API boundary and stick to it unless you have a clear reason not to. If every service uses a different style, your team has to remember different rules for auth, errors, validation, and client setup, and that adds more overhead than it saves.
How can I test my choice before I commit?
Try one real feature for one or two weeks. Choose something messy enough to expose friction, like auth, billing, or a dashboard flow. Watch how often frontend and backend drift, how much mapping code you write, and how long a small API change takes from server code to UI. That tells you more than any comparison table.