Migrating from tRPC
This guide will help you migrate your existing tRPC application to oRPC. Since oRPC draws significant inspiration from tRPC, the migration process should feel familiar and straightforward.
INFO
For a quick way to enhance your existing tRPC app with oRPC features without fully migrating, refer to the tRPC Integration.
Core Concepts Comparison
Concept | tRPC | oRPC |
---|---|---|
Router | t.router() | an object |
Procedure | t.procedure | os |
Context | t.context() | os.$context() |
Create Middleware | t.middleware() | os.middleware() |
Use Middleware | t.procedure.use() | os.use() |
Input Validation | t.procedure.input(schema) | os.input(schema) |
Output Validation | t.procedure.output(schema) | os.output(schema) |
Error Handling | TRPCError | ORPCError |
Serializer | superjson | built-in |
INFO
Learn more about oRPC vs tRPC Comparison
Step-by-Step Migration
1. Installation
First, install oRPC and remove tRPC dependencies:
npm uninstall @trpc/server @trpc/client @trpc/tanstack-react-query
npm install @orpc/server@latest @orpc/client@latest @orpc/tanstack-query@latest
yarn remove @trpc/server @trpc/client @trpc/tanstack-react-query
yarn add @orpc/server@latest @orpc/client@latest @orpc/tanstack-query@latest
pnpm remove @trpc/server @trpc/client @trpc/tanstack-react-query
pnpm add @orpc/server@latest @orpc/client@latest @orpc/tanstack-query@latest
bun remove @trpc/server @trpc/client @trpc/tanstack-react-query
bun add @orpc/server@latest @orpc/client@latest @orpc/tanstack-query@latest
deno remove npm:@trpc/server npm:@trpc/client npm:@trpc/tanstack-react-query
deno add npm:@orpc/server@latest npm:@orpc/client@latest npm:@orpc/tanstack-query@latest
2. Initialize
Initialization is an optional step in oRPC. You can use os
directly without initialization, but for reusability and better code organization, it's recommended to initialize your base procedures.
import { ORPCError, os } from '@orpc/server'
export async function createRPCContext(opts: { headers: Headers }) {
const session = await auth()
return {
headers: opts.headers,
session,
}
}
const o = os.$context<Awaited<ReturnType<typeof createRPCContext>>>()
const timingMiddleware = o.middleware(async ({ next, path }) => {
const start = Date.now()
try {
return await next()
}
finally {
console.log(`[oRPC] ${path} took ${Date.now() - start}ms to execute`)
}
})
export const publicProcedure = o.use(timingMiddleware)
export const protectedProcedure = publicProcedure.use(({ context, next }) => {
if (!context.session?.user) {
throw new ORPCError('UNAUTHORIZED')
}
return next({
context: {
session: { ...context.session, user: context.session.user }
}
})
})
import { initTRPC, TRPCError } from '@trpc/server'
import superjson from 'superjson'
export async function createRPCContext(opts: { headers: Headers }) {
const session = await auth()
return {
headers: opts.headers,
session,
}
}
const t = initTRPC.context<typeof createRPCContext>().create({
transformer: superjson,
})
export const createTRPCRouter = t.router
const timingMiddleware = t.middleware(async ({ next, path }) => {
const start = Date.now()
const result = await next()
const end = Date.now()
console.log(`[tRPC] ${path} took ${end - start}ms to execute`)
return result
})
export const publicProcedure = t.procedure.use(timingMiddleware)
export const protectedProcedure = t.procedure
.use(timingMiddleware)
.use(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
})
})
INFO
Learn more about oRPC Context, and Middleware.
3. Procedures
In oRPC, there are no separate .query
, .mutation
, or .subscription
methods. Instead, use .handler
for all procedure types.
export const planetRouter = {
list: publicProcedure
.input(z.object({ cursor: z.number().int().default(0) }))
.handler(({ input }) => {
// Logic here
return {
planets: [
{
name: 'Earth',
distanceFromSun: 149.6,
}
],
nextCursor: input.cursor + 1,
}
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1),
distanceFromSun: z.number().positive()
}))
.handler(async ({ context, input }) => {
// Logic here
}),
}
export const planetRouter = createTRPCRouter({
list: publicProcedure
.input(z.object({ cursor: z.number().int().default(0) }))
.query(({ input }) => {
// Logic here
return {
planets: [
{
name: 'Earth',
distanceFromSun: 149.6,
}
],
nextCursor: input.cursor + 1,
}
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1),
distanceFromSun: z.number().positive()
}))
.mutation(async ({ ctx, input }) => {
// Logic here
}),
})
INFO
Learn more about oRPC Procedures.
4. App Router
The main router structure is similar between tRPC and oRPC, except in oRPC you don't need to wrap routers in a .router
call - plain objects is enough.
import { planetRouter } from './planet'
export const appRouter = {
planet: planetRouter,
}
import { planetRouter } from './planet'
export const appRouter = createTRPCRouter({
planet: planetRouter,
})
INFO
Learn more about oRPC Router.
5. Error Handling
throw new ORPCError('BAD_REQUEST', {
message: 'Invalid input',
data: 'some data',
cause: validationError
})
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Invalid input',
data: 'some data',
cause: validationError
})
INFO
Learn more about oRPC Error Handling.
6. Server Setup
This example assumes you're using Next.js. If you're using a different framework, check the oRPC HTTP Adapters documentation.
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(appRouter, {
interceptors: [
async ({ next }) => {
try {
return await next()
}
catch (error) {
console.error(error)
throw error
}
}
]
})
async function handleRequest(request: Request) {
const { response } = await handler.handle(request, {
prefix: '/api/orpc',
context: await createORPCContext(request)
})
return response ?? new Response('Not found', { status: 404 })
}
export const GET = handleRequest
export const POST = handleRequest
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
function handler(req: Request) {
return fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext(req),
onError: ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`
)
}
})
}
export { handler as GET, handler as POST }
7. Client Setup
import { createORPCClient, onError } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { RouterClient } from '@orpc/server'
const link = new RPCLink({
url: 'http://localhost:3000/api/orpc',
interceptors: [
onError((error) => {
console.error(error)
})
],
})
export const client: RouterClient<typeof appRouter> = createORPCClient(link)
// ---------------- Usage ----------------
const { planets } = await client.planet.list({ cursor: 0 })
import { createTRPCProxyClient, httpLink } from '@trpc/client'
export const client = createTRPCProxyClient<typeof appRouter>({
links: [
httpLink({
url: 'http://localhost:3000/api/trpc'
})
]
})
// ---------------- Usage ----------------
const { planets } = await client.planet.list.query({ cursor: 0 })
INFO
Learn more about oRPC Client-Side Clients, Batch Requests Plugin, and Dedupe Requests Plugin.
8. TanStack Query (React) Integration
The oRPC TanStack Query integration is similar to tRPC, but simpler - you can use the orpc
utilities directly without React providers or special hooks.
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
export const orpc = createTanstackQueryUtils(client)
// ---------------- Usage in React Components ----------------
const query = useQuery(orpc.planet.list.queryOptions({
input: { cursor: 0 },
}))
const infinite = useInfiniteQuery(orpc.planet.list.infiniteOptions({
input: (page: number) => ({ cursor: page }),
initialPageParam: 0,
getNextPageParam: lastPage => lastPage.nextCursor,
}))
const mutation = useMutation(orpc.planet.create.mutationOptions())
import { createTRPCContext } from '@trpc/tanstack-react-query'
export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<typeof appRouter>()
// ---------------- Usage in React Components ----------------
const trpc = useTRPC()
const query = useQuery(trpc.planet.list.queryOptions({ cursor: 0 }))
const infinite = useInfiniteQuery(trpc.planet.list.infiniteQueryOptions(
{},
{
initialCursor: 0,
getNextPageParam: lastPage => lastPage.nextCursor,
}
))
const mutation = useMutation(trpc.planet.create.mutationOptions())
INFO
Learn more about oRPC TanStack Query Integration.