Skip to content

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.

INFO

If you want to add oRPC features to an existing tRPC app without a full migration, see tRPC Integration.

Core Concepts Comparison

ConcepttRPCoRPC
Routert.router()plain object
Proceduret.procedureos
Contextt.context()os.$context()
Create Middlewaret.middleware()os.middleware()
Use Middlewaret.procedure.use()os.use()
Input Validationt.procedure.input(schema)os.input(schema)
Output Validationt.procedure.output(schema)os.output(schema)
Error HandlingTRPCErrorORPCError
Serializersuperjsonbuilt-in

INFO

See oRPC vs tRPC Comparison for a broader comparison.

Step-by-Step Migration

1. Installation

Remove the tRPC packages and install the oRPC replacements:

sh
npm uninstall @trpc/server @trpc/client @trpc/tanstack-react-query
npm install @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
sh
yarn remove @trpc/server @trpc/client @trpc/tanstack-react-query
yarn add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
sh
pnpm remove @trpc/server @trpc/client @trpc/tanstack-react-query
pnpm add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
sh
bun remove @trpc/server @trpc/client @trpc/tanstack-react-query
bun add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta
sh
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.

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

INFO

Learn more about oRPC Context and Middleware.

3. Procedures

oRPC does not split procedures into .query, .mutation, and .subscription. Use .handler for all procedure types.

ts
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
    }),
}
ts
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 overall router structure stays similar. In oRPC, you do not wrap routers in a .router call. A plain object is enough.

ts
import { planetRouter } from './planet'

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

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

INFO

Learn more about oRPC Router.

5. Error Handling

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

ts
throw new ORPCError('BAD_REQUEST', {
  message: 'Invalid input',
  data: 'some data',
  cause: validationError
})
ts
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 uses Next.js. If you use another framework, see oRPC HTTP Adapters.

ts
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
ts
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.

ts
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 })
ts
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 Plugin, and Dedupe Plugin.

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.

ts
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())
ts
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.

Released under the MIT License.