Katman
Migrations

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:

  1. Remove all fromTRPC() calls
  2. Uninstall @trpc/server and @trpc/client
  3. 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

tRPCKatmanNotes
t.procedurek.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
TRPCErrorKatmanErrorSame code names
createTRPCProxyClient()createClient()Same proxy pattern
httpBatchLinkcreateLink()Batching via separate plugin
@trpc/react-querykatman/tanstack-querycreateQueryUtils() 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 errorsfail() with compile-time checked error codes
  • 15+ framework adapters — vs tRPC's 4
  • Built-in React server actionscreateAction(), useServerAction()
  • Built-in AI SDKrouterToTools() with zero config

What's next?

On this page