Migrating from tRPC
Step-by-step guide to migrate from tRPC to Katman.
Migrate incrementally. Start serving your existing tRPC router through Katman today, then rewrite procedures at your own pace.
Phase 1: Zero-rewrite interop
Use fromTRPC() to convert your entire tRPC router. Everything keeps working — same procedures, same input schemas, same behavior.
import { } from "katman"
import { } from "katman/trpc"
import { } from "./trpc-router"
const = ({
: () => ({
: .(.),
}),
})
// Convert the entire tRPC router
const = ()
// Serve with Katman
.(, { : 3000 })Your existing tRPC client still works. Point it at the new server and everything connects.
fromTRPC() wraps each tRPC procedure resolver. tRPC middleware still runs inside the procedure — it is not converted to Katman guards/wraps. This is intentional: nothing breaks.
Phase 2: Incremental rewrite
Convert procedures one by one. Mix tRPC-converted and native Katman procedures in the same router.
Pick a procedure to convert
Start with a simple query. Here's the tRPC version:
import { } from "zod"
import { } from "./trpc"
export const = .procedure
.input(.({ : .().() }))
.query(({ , }) => {
return .db.users.findMany({ : .limit })
})import { } from "zod"
const = k.query(
.({ : .().() }),
({ , }) => .db.users.findMany({ : .limit }),
)Convert middleware to guards
tRPC middleware becomes Katman guards (for auth/context) or wraps (for before+after logic).
const = t.middleware(async ({ , }) => {
if (!.session) throw new TRPCError({ : "UNAUTHORIZED" })
return ({ : { : .session.user } })
})
const = t.procedure.use()
export const =
.input(z.object({ : z.string() }))
.mutation(({ , }) => {
return .db.users.create({ ..., : .user.id })
})const = k.guard(async () => {
if (!.session) throw new KatmanError("UNAUTHORIZED")
return { : .session.user }
})
const = k.mutation({
: [],
: z.object({ : z.string() }),
: ({ , }) => {
return .db.users.create({ ..., : .user.id })
},
})Convert error handling
tRPC uses TRPCError. Katman uses KatmanError with the same code names, plus typed errors via fail().
import { } from "@trpc/server"
throw new ({
: "NOT_FOUND",
: "User not found",
})import { } from "katman"
// Option 1: Same pattern as tRPC
throw new ("NOT_FOUND", { : "User not found" })
// Option 2: Typed errors with fail()
const = k.mutation({
: { : 404, : 403 },
: ({ , , }) => {
const = .db.users.find(.id)
if (!) ("NOT_FOUND") // typed — only these codes allowed
if (.ownerId !== .user.id) ("FORBIDDEN")
return .db.users.delete(.id)
},
})Merge into the router
Replace the converted procedure in your router. Unconverted tRPC procedures sit right next to native Katman ones.
const = {
: listUsers, // native Katman
: createUser, // native Katman
: fromTRPC(trpcRouter).users.delete, // still tRPC
}
const = k.router({
: ,
// Other routes still running through fromTRPC
: fromTRPC(trpcRouter).posts,
})Phase 3: Full migration
Once all procedures are rewritten:
- Remove all
fromTRPC()calls - Uninstall
@trpc/serverand@trpc/client - Switch the client to Katman's native client
// Before (tRPC client)
import { , } from "@trpc/client"
const = ({
: [({ : "http://localhost:3000" })],
})
const = await ..list.query({ : 10 })// After (Katman client)
import { } from "katman/client"
import { } from "katman/client/ofetch"
const = <>(
({ : "http://localhost:3000" })
)
const = await .users.list({ : 10 })Concept mapping
| tRPC | Katman | Notes |
|---|---|---|
t.procedure | k.query() / k.mutation() | Katman uses separate methods |
t.router() | k.router() | Same nesting |
t.middleware() | k.guard() or k.wrap() | Guards for auth, wraps for before+after |
TRPCError | KatmanError | Same code names |
createTRPCProxyClient() | createClient() | Same proxy pattern |
httpBatchLink | createLink() | Batching via separate plugin |
@trpc/react-query | katman/tanstack-query | createQueryUtils() instead of createTRPCReact() |
ctx via createContext() | context: in katman() | Factory runs per request |
What you gain
After migrating, you get access to everything tRPC does not have:
- Single package — uninstall 4+ tRPC packages, install one
- Compiled pipeline — faster execution, no per-request middleware iteration
- Content negotiation — JSON, MessagePack, devalue automatically
- Guard/wrap model — cleaner separation of concerns
- Typed errors —
fail()with compile-time checked error codes - 15+ framework adapters — vs tRPC's 4
- Built-in React server actions —
createAction(),useServerAction() - Built-in AI SDK —
routerToTools()with zero config
What's next?
- Getting Started — Katman from scratch
- Middleware — guards and wraps in depth
- tRPC Interop —
fromTRPC()reference - Comparison — full feature matrix