oRPC Migrate from tRPC

A comprehensive guide to migrate your tRPC application to oRPC.

SKILL.md

GitHub

Migrating from tRPC

oRPC draws significant inspiration from tRPC, making migration straightforward.

For quick enhancement without full migration, see 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

Step-by-Step Migration

1. Installation

npm uninstall @trpc/server @trpc/client @trpc/tanstack-react-query
npm install @orpc/server@latest @orpc/client@latest @orpc/tanstack-query@latest

2. Initialize

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`)
  }
})

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 } }
  })
})

3. Procedures

In oRPC there are no separate .query/.mutation/.subscription methods. Use .handler for all procedure types.

export const planetRouter = {
  list: publicProcedure
    .input(z.object({ cursor: z.number().int().default(0) }))
    .handler(({ input }) => {
      return { planets: [], nextCursor: input.cursor + 1 }
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1),
      distanceFromSun: z.number().positive()
    }))
    .handler(async ({ context, input }) => {}),
}

4. App Router

In oRPC you don't need to wrap routers — plain objects are enough:

export const appRouter = {
  planet: planetRouter,
}

5. Error Handling

throw new ORPCError('BAD_REQUEST', {
  message: 'Invalid input',
  data: 'some data',
  cause: validationError
})

6. Server Setup (Next.js)

// app/api/orpc/[[...rest]]/route.ts
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

7. Client Setup

import { createORPCClient, onError } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  url: 'http://localhost:3000/api/orpc',
  interceptors: [onError(e => console.error(e))],
})

export const client: RouterClient<typeof appRouter> = createORPCClient(link)

// Usage:
const { planets } = await client.planet.list({ cursor: 0 })

8. TanStack Query Integration

import { createTanstackQueryUtils } from '@orpc/tanstack-query'

export const orpc = createTanstackQueryUtils(client)

// Usage:
const query = useQuery(orpc.planet.list.queryOptions({ input: { cursor: 0 } }))
const mutation = useMutation(orpc.planet.create.mutationOptions())