Migrating from tRPC
Step-by-step guide to migrate from tRPC to Silgi.
Migrate incrementally. Start serving your existing tRPC router through Silgi 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 'silgi'
import { } from 'silgi/trpc'
import { } from './trpc-router'
const = ({
: () => ({
: .(.),
}),
})
// Convert the entire tRPC router
const = ()
// Serve with Silgi
.(, { : 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 Silgi guards/wraps. This is intentional: nothing breaks.
Phase 2: Incremental rewrite
Convert procedures one by one. Mix tRPC-converted and native Silgi 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
.$input(.({ : .().() }))
.$resolve(({ , }) => .db.users.findMany({ : .limit }))Convert middleware to guards
tRPC middleware becomes Silgi 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 = s.guard(async () => {
if (!.session) throw new SilgiError("UNAUTHORIZED")
return { : .session.user }
})
const = k
.$use()
.$input(z.object({ : z.string() }))
.$resolve(({ , }) => {
return .db.users.create({ ..., : .user.id })
})Convert error handling
tRPC uses TRPCError. Silgi uses SilgiError with the same code names, plus typed errors via fail().
import { } from "@trpc/server"
throw new ({
: "NOT_FOUND",
: "User not found",
})import { } from "silgi"
// Option 1: Same pattern as tRPC
throw new ("NOT_FOUND", { : "User not found" })
// Option 2: Typed errors with fail()
const = k
.$errors({ : 404, : 403 })
.$resolve(({ , , }) => {
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 Silgi ones.
const = {
: listUsers, // native Silgi
: createUser, // native Silgi
: fromTRPC(trpcRouter).users.delete, // still tRPC
}
const = s.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 Silgi's native client
// Before (tRPC client)
import { , } from '@trpc/client'
const = ({
: [({ : 'http://localhost:3000' })],
})
const = await ..list.query({ : 10 })// After (Silgi client)
import { } from 'silgi/client'
import { } from 'silgi/client/ofetch'
const = <>(({ : 'http://localhost:3000' }))
const = await .users.list({ : 10 })Concept mapping
| tRPC | Silgi | Notes |
|---|---|---|
t.procedure | s.$resolve() / s.$input().$resolve() | Silgi uses builder methods |
t.router() | s.router() | Same nesting |
t.middleware() | s.guard() or s.wrap() | Guards for auth, wraps for before+after |
TRPCError | SilgiError | Same code names |
createTRPCProxyClient() | createClient() | Same proxy pattern |
httpBatchLink | createLink() | Batching via separate plugin |
@trpc/react-query | silgi/tanstack-query | createQueryUtils() instead of createTRPCReact() |
ctx via createContext() | context: in silgi() | 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 — Silgi from scratch
- Middleware — guards and wraps in depth
- tRPC Interop —
fromTRPC()reference - Comparison — full feature matrix