Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Migrating from tRPC

This guide shows how to migrate an existing tRPC app to oRPC. Because oRPC is heavily inspired by tRPC, most concepts map directly, so the migration should feel familiar.

Core Concepts Comparison

Concept tRPC oRPC
Router t.router() plain 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

Remove the tRPC packages and install the oRPC replacements:

npm uninstall @trpc/server @trpc/client @trpc/tanstack-react-query
npm install @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
yarn remove @trpc/server @trpc/client @trpc/tanstack-react-query
yarn add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
pnpm remove @trpc/server @trpc/client @trpc/tanstack-react-query
pnpm add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
bun remove @trpc/server @trpc/client @trpc/tanstack-react-query
bun add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
deno remove npm:@trpc/server npm:@trpc/client npm:@trpc/tanstack-react-query
deno add npm:@orpc/server@beta npm:@orpc/client@beta npm:@orpc/tanstack-query@beta

2. Initialize

Initialization is optional in oRPC. You can use os directly, but creating shared base procedures makes context and middleware easier to reuse.

import { ORPCError, os } from '@orpc/server'

export async function createORPCContext(opts: { headers: Headers }) {
  const session = await auth()

  return {
    headers: opts.headers,
    session,
  }
}

const o = os.$context<Awaited<ReturnType<typeof createORPCContext>>>()

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 createTRPCContext(opts: { headers: Headers }) {
  const session = await auth()

  return {
    headers: opts.headers,
    session,
  }
}

const t = initTRPC.context<typeof createTRPCContext>().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 },
      },
    })
  })

3. Procedures

oRPC does not split procedures into .query, .mutation, and .subscription. 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
    }),
})

4. App Router

The overall router structure stays similar. In oRPC, you do not wrap routers in a .router call. A plain object is enough.

import { planetRouter } from './planet'

export const appRouter = {
  planet: planetRouter,
}
import { planetRouter } from './planet'

export const appRouter = createTRPCRouter({
  planet: planetRouter,
})

5. Error Handling

Error handling is similar, but ORPCError takes the error code as its first argument.

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

6. Server Setup

This example uses Next.js. If you use another framework, see oRPC HTTP Adapters.

import { RPCHandler } from '@orpc/server/fetch'

const handler = new RPCHandler(appRouter, {
  interceptors: [
    async ({ next, path }) => {
      try {
        return await next()
      }
      catch (error) {
        console.error(`❌ oRPC failed on ${path.join('.')}: `, error)
        throw error
      }
    }
  ]
})

async function handleRequest(request: Request) {
  const { response } = await handler.handle(request, {
    prefix: '/api/orpc',
    context: await createORPCContext({ headers: request.headers })
  })

  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({ headers: req.headers }),
    onError: ({ path, error }) => {
      console.error(
        `❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`
      )
    }
  })
}

export { handler as GET, handler as POST }

7. Client Setup

Create a transport link, then use it to build a typed client.

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

const link = new RPCLink({
  origin: 'http://localhost:3000',
  url: '/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 })

8. TanStack Query (React) Integration

The TanStack Query integration feels similar to tRPC, but it is lighter. You can use the generated orpc utilities directly without a React provider or custom 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())

Last updated on August 6, 2026

Was this page helpful?