Silgi
Migrations

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:

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

tRPCSilgiNotes
t.procedures.$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
TRPCErrorSilgiErrorSame code names
createTRPCProxyClient()createClient()Same proxy pattern
httpBatchLinkcreateLink()Batching via separate plugin
@trpc/react-querysilgi/tanstack-querycreateQueryUtils() 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 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