Skip to content
result-rpc
Esc
navigateopen⌘Jpreview

Migrating from tRPC

Per-router coexistence, a mechanical concept mapping, and the auth-layer-first first slice.

Migration is per-router, not big-bang. result-rpc is a separate endpoint with a separate client — it shares nothing with tRPC at runtime, so both stacks run side by side for as long as the migration takes:

// server: two handlers, two routes
app.all("/api/trpc/*", trpcHandler)     // existing routers stay
app.post("/rpc", resultRpcHandler)      // migrated routers move here

// client: two clients during the transition
export const trpc = createTRPCReact<LegacyRouter>()
export const client = createClient({ contract, transport: batchFetchTransport({ url: "/rpc" }) })

The recommended first slice is the auth layer plus one feature router — small enough to finish in days, and it exercises the part tRPC cannot express (a shell owning session expiry) so the migration proves its value immediately instead of at the end.

The concept mapping is mechanical:

tRPC result-rpc
initTRPC.context<Ctx>().create() rpc.context<Ctx>()
t.procedure.input(z...).query(fn) app.procedure().input(wire...).output(wire...).errors({...}).query(fn)
throw new TRPCError({ code }) return err(errors.SomeError({...}))
t.middleware + ctx spread app.middleware<Added>().errors({...}).use(...)
protectedProcedure app.procedure().use(authenticated) — same pattern
httpBatchLink batchFetchTransport
@trpc/react-query hooks useResultQuery / shell hooks
errorFormatter gone — error data is a wire codec, not a formatted shape
adapter onError onError + onInternalError on createFetchHandler
createCaller createServerClient (parity mode)
queryClient.setDefaultOptions({ onError }) a shell

Two things have no tRPC equivalent and are the actual work: every procedure declares its error union (this is where the two-failure-channel debt gets paid down, one procedure at a time), and interceptor logic moves into shells. There is no codemod; each procedure is a five-minute mechanical rewrite.

During coexistence the two stacks keep separate caches — a result-rpc mutation does not invalidate tRPC queries or vice versa. Migrate whole features, not halves of one screen, and the seam stays invisible.

Was this page helpful?