Skip to content
oRPC
Esc
navigateopen⌘Jpreview
On this page

Migrating from oRPC v1

This guide walks you through upgrading an oRPC v1 app to v2. Most of your code keeps working: many v1 names still compile through deprecated aliases, so your editor shows a strike-through hint instead of an error. The sections below focus on the changes that need your attention, each with a v2 and v1 comparison.

Update Packages

Install the v2 versions of the packages you use:

npm install @orpc/server@beta @orpc/client@beta

Some packages were renamed, merged, or promoted from experimental status:

v1 package v2 package
@orpc/openapi-client merged into @orpc/openapi
@orpc/react @orpc/next
@orpc/react-query / @orpc/vue-query / @orpc/solid-query / @orpc/svelte-query @orpc/tanstack-query
@orpc/vue-colada @orpc/pinia-colada
@orpc/experimental-react-swr @orpc/swr
@orpc/experimental-publisher @orpc/publisher
@orpc/experimental-publisher-durable-object @orpc/cloudflare
@orpc/experimental-ratelimit @orpc/ratelimit
@orpc/experimental-pino @orpc/pino
@orpc/otel @orpc/opentelemetry
@orpc/server/hibernation (subpath) @orpc/hibernation

Routing Moved to OpenAPI Metadata

The biggest change: .route, .prefix, .tag, and .$route no longer exist on the builder. OpenAPI routing now lives in metadata, set with the openapi helper from @orpc/openapi. See OpenAPI Routing.

import { openapi } from '@orpc/openapi'

const listPlanet = os
  .meta(openapi({ method: 'GET', path: '/planets' }))
  .handler(async () => [])

const router = os
  .meta(openapi({ prefix: '/planets', tags: ['planets'] }))
  .router({ list: listPlanet })
const listPlanet = os
  .route({ method: 'GET', path: '/planets' })
  .handler(async () => [])

const router = os
  .prefix('/planets')
  .tag('planets')
  .router({ list: listPlanet })

The same applies to lazy routers with a prefix:

const router = {
  planet: os.meta(openapi({ prefix: '/planets' })).lazy(() => import('./planet')),
}
const router = {
  planet: os.prefix('/planets').lazy(() => import('./planet')),
}

Procedure Builder

.callable is no longer built in

In v2, prefer call or createRouterClient to call procedures on the server:

import { call } from '@orpc/server'

const getting = os.handler(async () => 'pong')

const result = await call(getting, undefined, { context: {} })
const getting = os
  .handler(async () => 'pong')
  .callable({ context: {} })

const result = await getting()

.actionable moved to @orpc/next

Server actions are now called server functions and live in the Next.js Integration. In v2, prefer createServerFunctionable (or createServerFunction): it works exactly like .actionable, returning a value that is both a server function and a regular procedure.

'use server'

import { createServerFunctionable } from '@orpc/next'
import { os } from '@orpc/server'

const functionable = createServerFunctionable({ context: {} })

export const getting = functionable(
  os.handler(async () => 'pong'),
)
'use server'

import { os } from '@orpc/server'

export const getting = os
  .handler(async () => 'pong')
  .actionable({ context: {} })

The hooks were renamed too: useServerAction is now useServerFunction and useOptimisticServerAction is now useOptimisticServerFunction, both imported from @orpc/next/hooks (old names still work as deprecated aliases). createFormAction is now createServerFormFunction.

.input and .output now stack

In v1, a procedure had at most one input and one output schema. In v2, each .input or .output call adds another schema on top of the previous ones. See Multiple Schemas.

const example = os
  .input(z.looseObject({ name: z.string() }))
  .input(z.looseObject({ id: z.number() })) // adds a second schema
  .handler(async ({ input }) => {}) // input: { name: string } & { id: number }

.$config options changed

The index-based validation options were replaced by two simple flags. v2 tracks the order of middleware and validation automatically, so dedupeLeadingMiddlewares and the index options are gone. See Validation Customization.

const base = os.$config({
  disableInputValidation: true,
  disableOutputValidation: true,
})
const base = os.$config({
  initialInputValidationIndex: Number.NEGATIVE_INFINITY,
  initialOutputValidationIndex: Number.NaN,
})

.$meta replaced by meta plugins

.meta now accepts meta plugins created with defineMeta, and .$meta<T>() was removed. See Metadata.

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

const [cacheMeta, getCacheMeta] = defineMeta(
  'cache',
  (incoming: boolean) => incoming,
)

const base = os.use(async ({ procedure, next }) => {
  if (getCacheMeta(procedure) !== true) {
    return next()
  }
  // ...
  return next()
})

const example = base
  .meta(cacheMeta(true))
  .handler(async () => {})
import { os } from '@orpc/server'

interface ORPCMetadata {
  cache?: boolean
}

const base = os
  .$meta<ORPCMetadata>({})
  .use(async ({ procedure, next }) => {
    if (!procedure['~orpc'].meta.cache) {
      return next()
    }
    // ...
    return next()
  })

const example = base
  .meta({ cache: true })
  .handler(async () => {})

Middleware

Renamed methods

.concat is now .use, and .mapInput is now .adaptInput. The two argument form .use(middleware, mapInput) was removed. See Middleware.

const merged = aMiddleware.use(anotherMiddleware)

const example = os
  .input(z.object({ id: z.number() }))
  .use(canUpdate.adaptInput(input => input.id))
  .handler(async () => {})
const merged = aMiddleware.concat(anotherMiddleware)

const example = os
  .input(z.object({ id: z.number() }))
  .use(canUpdate, input => input.id)
  .handler(async () => {})

output argument replaced by done

The third middleware argument for short-circuiting with an output changed shape. See Middleware.

const cacheMiddleware = os.middleware(async ({ next }, input, done) => {
  if (cache.has(key)) {
    return done({ output: cache.get(key) })
  }

  return next()
})
const cacheMiddleware = os.middleware(async ({ next }, input, output) => {
  if (cache.has(key)) {
    return output(cache.get(key))
  }

  return next()
})

Automatic deduplication removed

v1 automatically skipped router-level middleware that was already applied to a procedure. v2 no longer does this.

Guard shared middleware yourself with the context flag pattern from Dedupe Middleware:

const authMiddleware = os
  .$context<{ user?: User, authLoaded?: boolean }>()
  .middleware(async ({ context, next }) => {
    if (context.authLoaded) {
      return next()
    }

    return next({
      context: { user: await loadUser(), authLoaded: true },
    })
  })

The dedupeLeadingMiddlewares config option was removed together with this behavior.

Error Handling

status removed from errors

ORPCError and .errors definitions no longer accept a status. HTTP status codes are now a handler concern, configured with errorStatusMap. See Error Handling and RPC Handler.

import { COMMON_ERROR_STATUS_MAP } from '@orpc/server'

const example = os
  .errors({
    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },
  })
  .handler(async ({ errors }) => {
    throw errors.RATE_LIMITED({ data: { retryAfter: 60 } })
  })

const handler = new RPCHandler(router, {
  errorStatusMap: { ...COMMON_ERROR_STATUS_MAP, RATE_LIMITED: 429 },
})
const example = os
  .errors({
    RATE_LIMITED: { status: 429, data: z.object({ retryAfter: z.number() }) },
  })
  .handler(async ({ errors }) => {
    throw errors.RATE_LIMITED({ data: { retryAfter: 60 } })
  })

const handler = new RPCHandler(router)

isDefinedError renamed to isInferableError

isDefinedError still works as a deprecated alias. The safe result also changed: the third element is now the typed error itself (or null) instead of a boolean, and a fourth isSuccess element was added. See Client Error Handling.

import { isInferableError, safe } from '@orpc/client'

const [error, data, inferableError, isSuccess] = await safe(client.example({ id: 1 }))

if (inferableError) {
  console.log(inferableError.data.retryAfter)
}
import { isDefinedError, safe } from '@orpc/client'

const [error, data, isDefined] = await safe(client.example({ id: 1 }))

if (error && isDefined) {
  console.log(error.data.retryAfter)
}

AsyncIteratorObject (Event Iterator)

The “Event Iterator” concept was renamed to AsyncIteratorObject (see also AsyncIteratorObject in Client). All old names still work as deprecated aliases:

v1 v2
eventIterator asyncIteratorObject
consumeEventIterator consumeAsyncIterator
eventIteratorToStream asyncIteratorToStream
eventIteratorToUnproxiedDataStream asyncIteratorToUnproxiedDataStream
streamToEventIterator streamToAsyncIteratorObject
import { asyncIteratorObject } from '@orpc/server'

const streaming = os
  .output(asyncIteratorObject(z.object({ message: z.string() })))
  .handler(async function* () {
    yield { message: 'Hello' }
  })
import { eventIterator } from '@orpc/server'

const streaming = os
  .output(eventIterator(z.object({ message: z.string() })))
  .handler(async function* () {
    yield { message: 'Hello' }
  })

EventPublisher replaced by MemoryPublisher

EventPublisher was removed from @orpc/server. Use the Publisher Helpers instead. Note that publish is now async.

import { MemoryPublisher } from '@orpc/publisher/memory'

const publisher = new MemoryPublisher<{ 'something-updated': { id: string } }>()

await publisher.publish('something-updated', { id: '1' })
import { EventPublisher } from '@orpc/server'

const publisher = new EventPublisher<{ 'something-updated': { id: string } }>()

publisher.publish('something-updated', { id: '1' })

RPC Handler

GET requests are rejected by default

v1 shipped StrictGetMethodPlugin enabled by default. v2 removed that plugin (and the strictGetMethodPluginEnabled option) in favor of an allowMethods option that defaults to ['POST', 'PUT', 'PATCH', 'DELETE']. If your client sends GET requests, allow them explicitly and add CSRF protection. See RPC Handler.

import { SimpleCsrfProtectionHandlerPlugin } from '@orpc/server/plugins'
import { RPC_DEFAULT_ALLOW_METHODS } from '@orpc/server/standard'

const handler = new RPCHandler(router, {
  allowMethods: ['GET', ...RPC_DEFAULT_ALLOW_METHODS],
  plugins: [new SimpleCsrfProtectionHandlerPlugin()],
})
// GET was accepted when the procedure declared .route({ method: 'GET' }),
// enforced by the default StrictGetMethodPlugin
const handler = new RPCHandler(router)

The v2 Simple CSRF Protection Plugin checks the Sec-Fetch-Mode header, so it no longer needs a matching link plugin. Remove SimpleCsrfProtectionLinkPlugin from your client; it no longer exists.

Interceptor options renamed

rootInterceptors is now routingInterceptors, and adapterInterceptors is now named after the adapter (for example fetchInterceptors on the fetch adapter). See RPC Handler.

const handler = new RPCHandler(router, {
  routingInterceptors: [/* ... */],
  fetchInterceptors: [/* ... */],
})
const handler = new RPCHandler(router, {
  rootInterceptors: [/* ... */],
  adapterInterceptors: [/* ... */],
})

filter takes positional arguments

The filter option on handlers (and the OpenAPI generator) receives positional arguments now. The v1 destructured form still type-checks but reads the wrong values, so update it carefully. See RPC Handler and OpenAPI Specification.

const handler = new RPCHandler(router, {
  filter: (contract, path) => !path.includes('internal'),
})
const handler = new RPCHandler(router, {
  filter: ({ contract, path }) => !path.includes('internal'),
})

Custom serializers use a serializer instance

The customJsonSerializers option with numeric types was replaced by a serializer instance with string-keyed handlers, shared between RPC Handler and RPC Link. To override a built-in type, reuse its key (for example date) instead of matching a magic number. See RPC Serializer.

import { RPCSerializer } from '@orpc/client'

const serializer = new RPCSerializer({
  handlers: {
    user: {
      condition: data => data instanceof User,
      serialize: data => data.toJSON(),
      deserialize: data => new User(data.id, data.name),
    },
  },
})

const handler = new RPCHandler(router, { serializer })
const link = new RPCLink({ serializer })
import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard'

const userSerializer: StandardRPCCustomJsonSerializer = {
  type: 21, // unique number > 20
  condition: data => data instanceof User,
  serialize: data => data.toJSON(),
  deserialize: data => new User(data.id, data.name),
}

const handler = new RPCHandler(router, { customJsonSerializers: [userSerializer] })
const link = new RPCLink({ url: '...', customJsonSerializers: [userSerializer] })

Event stream options nested under the response mapping

The flat eventIterator* handler options moved under the adapter’s response option, and the keep-alive default changed from 5 to 15 seconds. See RPC Handler.

const handler = new RPCHandler(router, {
  toFetchResponse: { // fetch adapter; node uses sendStandardResponse
    eventStream: {
      keepAlive: { enabled: true, interval: 15000, comment: '' },
    },
  },
})
const handler = new RPCHandler(router, {
  eventIteratorKeepAliveEnabled: true,
  eventIteratorKeepAliveInterval: 5000,
  eventIteratorKeepAliveComment: '',
})

WebSocket adapters unified

@orpc/server/ws and @orpc/server/bun-ws were removed. A single @orpc/server/websocket adapter now covers ws, Bun, Deno, Cloudflare, and more. See WebSocket Adapters.

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

wss.on('connection', (ws) => {
  handler.upgrade(ws, { context: {} })
})
import { RPCHandler } from '@orpc/server/ws'

wss.on('connection', (ws) => {
  handler.upgrade(ws, { context: {} })
})

Server Plugins

Handler plugins were renamed with a HandlerPlugin suffix. Deprecated aliases exist unless noted:

v1 v2
CORSPlugin CORSHandlerPlugin
RequestHeadersPlugin RequestHeadersHandlerPlugin
ResponseHeadersPlugin ResponseHeadersHandlerPlugin
BodyLimitPlugin (from adapter subpaths) RequestLimitHandlerPlugin (from @orpc/server/plugins)
CompressionPlugin (no alias) RequestCompressionHandlerPlugin + ResponseCompressionHandlerPlugin
experimental_RethrowHandlerPlugin RethrowHandlerPlugin
StrictGetMethodPlugin (no alias) removed, use allowMethods
import {
  RequestLimitHandlerPlugin,
  ResponseCompressionHandlerPlugin,
} from '@orpc/server/plugins'

const handler = new RPCHandler(router, {
  plugins: [
    new RequestLimitHandlerPlugin({ maxBodySize: 1024 * 1024 }),
    new ResponseCompressionHandlerPlugin(),
  ],
})
import { BodyLimitPlugin, CompressionPlugin } from '@orpc/server/fetch'

const handler = new RPCHandler(router, {
  plugins: [
    new BodyLimitPlugin({ maxBodySize: 1024 * 1024 }),
    new CompressionPlugin(),
  ],
})

Client

url is now a path prefix starting with /, and the origin moves to a separate origin option (omit it in the browser to use the current origin). See RPC Link.

import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  origin: 'http://localhost:3000',
  url: '/rpc',
})
import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  url: 'http://localhost:3000/rpc',
})

Custom fetch receives a URL string

The first argument of a custom fetch is now the URL string instead of a Request object. See RPC Link.

const link = new RPCLink({
  url: '/rpc',
  fetch: (url, init, { context }) => globalThis.fetch(url, {
    ...init,
    credentials: 'include',
  }),
})
const link = new RPCLink({
  url: 'http://localhost:3000/rpc',
  fetch: (request, init, { context }) => globalThis.fetch(request, {
    ...init,
    credentials: 'include',
  }),
})

clientInterceptors is now transportInterceptors, and adapterInterceptors is now fetchInterceptors on the fetch adapter. Event stream options moved under toFetchRequest.eventStream, mirroring the handler-side change. See RPC Link.

Typed clients for contracts

ContractRouterClient was renamed to RouterContractClient (the old name still works as a deprecated alias). RouterClient from @orpc/server is unchanged. See Client-Side Clients.

import type { RouterContractClient } from '@orpc/contract'

const client: RouterContractClient<typeof contract> = createORPCClient(link)
import type { ContractRouterClient } from '@orpc/contract'

const client: ContractRouterClient<typeof contract> = createORPCClient(link)

Pass a factory instead of a WebSocket instance. Reconnection is now built in, so you no longer need partysocket. See WebSocket Adapters.

import { RPCLink } from '@orpc/client/websocket'

const link = new RPCLink({
  connect: () => new WebSocket('ws://localhost:3000'),
  reconnect: { enabled: true },
})
import { RPCLink } from '@orpc/client/websocket'

const websocket = new WebSocket('ws://localhost:3000')

const link = new RPCLink({ websocket })

Client Plugins

Link plugins were renamed with a LinkPlugin suffix. Deprecated aliases exist for all of them:

v1 v2
ClientRetryPlugin RetryLinkPlugin
DedupeRequestsPlugin DedupeLinkPlugin
RetryAfterPlugin RetryAfterLinkPlugin

v2 also adds new plugins: Timeout, Request Compression, and Response Compression.

Batch Plugin

The v2 Batch Plugin supports every response type, including AsyncIteratorObject and File/Blob. In v1, exclude existed mainly to skip those unsupported responses, so in most cases you can simply remove it:

import { BatchLinkPlugin } from '@orpc/client/plugins'

const batchPlugin = new BatchLinkPlugin({
  groups: [{ condition: () => true, context: {} }],
})
import { BatchLinkPlugin } from '@orpc/client/plugins'

const batchPlugin = new BatchLinkPlugin({
  groups: [{ condition: () => true, context: {} }],
  exclude: ({ path }) => {
    return ['planets/getImage', 'planets/subscribe'].includes(path.join('/'))
  },
})

Contract-First

Contract types and utilities changed word order from ContractRouter* to RouterContract*. Deprecated aliases exist for all of them. See Procedure Contract and Router Contract.

v1 v2
ContractRouterClient RouterContractClient
AnyContractRouter RouterContract
AnyContractProcedure AnyProcedureContract
InferContractRouterInputs InferRouterContractInputs
InferContractRouterOutputs InferRouterContractOutputs
minifyContractRouter minifyRouterContract
populateContractRouterPaths (@orpc/contract) populateRouterContractOpenAPIPaths (@orpc/openapi)
RequestValidationPlugin RequestValidationLinkPlugin
ResponseValidationPlugin ResponseValidationLinkPlugin

The implementer works the same as in v1: implement(contract) still supports .$context, .use, and .middleware. New in v2, it also accepts a procedure config, either as a second argument or through .$config. See Contract Implementation.

import { implement } from '@orpc/server'

const os = implement(contract, { disableOutputValidation: true })

Contract routing uses openapi() metadata now, as described in Routing Moved to OpenAPI Metadata.

OpenAPI

The @orpc/openapi-client package was merged into @orpc/openapi. Its options follow the same changes as RPCLink (origin + url, transportInterceptors, and so on). See OpenAPI Link.

import { OpenAPILink } from '@orpc/openapi/fetch'

const link = new OpenAPILink(contract, {
  origin: 'http://localhost:3000',
  url: '/api',
})
import { OpenAPILink } from '@orpc/openapi-client/fetch'

const link = new OpenAPILink(contract, {
  url: 'http://localhost:3000/api',
})

The form data helpers moved with it, from @orpc/openapi-client/helpers to @orpc/openapi/helpers.

OpenAPIGenerator options restructured

schemaConverters is now converters, and document fields moved under base. commonSchemas was removed: define reusable schemas natively in your schema library instead (for example .meta({ id: 'Planet' }) in Zod), and they are hoisted into components.schemas automatically. See OpenAPI Specification.

import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod'

const generator = new OpenAPIGenerator({
  converters: [new ZodToJsonSchemaConverter()],
})

const spec = await generator.generate(router, {
  base: {
    info: { title: 'My App', version: '0.0.0' },
  },
})
import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod'

const generator = new OpenAPIGenerator({
  schemaConverters: [new ZodToJsonSchemaConverter()],
})

const spec = await generator.generate(router, {
  info: { title: 'My App', version: '0.0.0' },
})

Two smaller changes in the same area:

  • The oo helper (oo.spec) was removed. To customize the operation object, attach openapi({ spec }) metadata directly on the procedure or router.
  • The shouldHoistDef option was replaced by customComponentName. Root $defs are now always hoisted into components.schemas; this option only renames them.

OpenAPIReferencePlugin renamed and reshaped

The plugin is now OpenAPIReferenceHandlerPlugin, and you provide the spec yourself instead of passing converters and generate options. See OpenAPI Reference Plugin.

import { OpenAPIGenerator } from '@orpc/openapi'
import { OpenAPIReferenceHandlerPlugin } from '@orpc/openapi/plugins'

const generator = new OpenAPIGenerator({
  converters: [new ZodToJsonSchemaConverter()],
})

const handler = new OpenAPIHandler(router, {
  plugins: [
    new OpenAPIReferenceHandlerPlugin({
      provider: 'scalar',
      spec: () => generator.generate(router, {
        base: { info: { title: 'My App', version: '0.0.0' } },
      }),
    }),
  ],
})
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'

const handler = new OpenAPIHandler(router, {
  plugins: [
    new OpenAPIReferencePlugin({
      docsProvider: 'scalar',
      schemaConverters: [new ZodToJsonSchemaConverter()],
      specGenerateOptions: { info: { title: 'My App', version: '0.0.0' } },
    }),
  ],
})

Zod integration requires Zod v4

@orpc/zod now supports Zod v4 only. The @orpc/zod/zod4 subpath and the oz helper (oz.file(), oz.openapi(), …) were removed. See Zod Integration.

ZodSmartCoercionPlugin was also removed. Use the schema-agnostic Smart Coercion Plugin instead, whose option is now named converters:

import { SmartCoercionHandlerPlugin } from '@orpc/json-schema'
import { ZodToJsonSchemaConverter } from '@orpc/zod'

const handler = new OpenAPIHandler(router, {
  plugins: [
    new SmartCoercionHandlerPlugin({
      converters: [new ZodToJsonSchemaConverter()],
    }),
  ],
})
import { ZodSmartCoercionPlugin } from '@orpc/zod'

const handler = new OpenAPIHandler(router, {
  plugins: [new ZodSmartCoercionPlugin()],
})

The Valibot and ArkType converters dropped their experimental_ prefixes: ValibotToJsonSchemaConverter and ArkTypeToJsonSchemaConverter. When no converter matches, v2 falls back to Standard Schema JSON conversion instead of producing an unknown schema.

Integrations

TanStack Query

The per-framework packages were removed in favor of @orpc/tanstack-query, and a few options changed. See TanStack Query Integration.

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

const orpc = createTanstackQueryUtils(client, { prefix: 'user' })

orpc.streamed.streamedOptions({ input: {} })
orpc.live.liveOptions({ input: {} })
import { createTanstackQueryUtils } from '@orpc/tanstack-query'

const orpc = createTanstackQueryUtils(client, { path: ['user'] })

orpc.streamed.experimental_streamedOptions({ input: {} })
orpc.live.experimental_liveOptions({ input: {} })

experimental_defaults became scoped, and the hydration serializer changed from StandardRPCJsonSerializer to RPCSerializer (see Custom serializers).

SWR and Pinia Colada

@orpc/experimental-react-swr is now @orpc/swr (see SWR Integration), and @orpc/vue-colada is now @orpc/pinia-colada with createORPCVueColadaUtils renamed to createPiniaColadaUtils (see Pinia Colada Integration). Both switched from path to prefix, same as TanStack Query.

NestJS

Import implement, ORPCError, and onError from @orpc/server instead of @orpc/nest, and augment DefaultInitialContext instead of ORPCGlobalContext. See NestJS Integration.

import { Implement } from '@orpc/nest'
import { implement, ORPCError } from '@orpc/server'

declare module '@orpc/server' {
  interface DefaultInitialContext {
    request: Request
  }
}
import { Implement, implement, ORPCError } from '@orpc/nest'

declare module '@orpc/nest' {
  interface ORPCGlobalContext {
    request: Request
  }
}

AI SDK

@orpc/ai-sdk now targets AI SDK v7+. implementTool and createTool became factories, and tool metadata uses the aiSdkTool() meta plugin. See AI SDK Integration.

import { createToolFactory } from '@orpc/ai-sdk'

const createTool = createToolFactory({ context: {} })
const tool = createTool(someProcedure)
import { createTool } from '@orpc/ai-sdk'

const tool = createTool(someProcedure, { context: {} })

Hibernation

The @orpc/server/hibernation subpath became the @orpc/hibernation package. HibernationPlugin is now HibernationHandlerPlugin, HibernationEventIterator is now HibernationAsyncIteratorClass (aliases kept), encodeHibernationRPCEvent is now async, and the 'done' event was renamed to 'close'. See Hibernation Integration.

Logging and tracing

@orpc/experimental-pino is now @orpc/pino, with LoggingHandlerPlugin renamed to PinoHandlerPlugin (see Pino Integration). @orpc/otel is now @orpc/opentelemetry, and context propagation works out of the box (see OpenTelemetry Integration).

Helpers

Base64Url, Cookie, Encryption, and Signing helpers are unchanged in @orpc/server/helpers.

Publisher

The package is now @orpc/publisher. The resume option was restructured, and the Redis adapter switched from ioredis to node-redis. See Publisher Helpers.

import { RedisPublisher } from '@orpc/publisher/redis'

const publisher = new RedisPublisher(client, {
  subscriber,
  resume: { enabled: true, seconds: 300 },
})
import { IORedisPublisher } from '@orpc/experimental-publisher/ioredis'

const publisher = new IORedisPublisher({
  commander,
  listener,
  resumeRetentionSeconds: 300,
})

The Durable Object adapter moved to @orpc/cloudflare, with PublisherDurableObject renamed to DurablePublisherObject.

Rate Limit

The package is now @orpc/ratelimit. Watch the casing change from Ratelimiter to RateLimiter in every class name, and the middleware helper rename. See Rate Limit Helpers.

import { ratelimit } from '@orpc/ratelimit'
import { MemoryRateLimiter } from '@orpc/ratelimit/memory'

const limiter = new MemoryRateLimiter({ maxRequests: 10, window: 60_000 })

const example = os
  .use(ratelimit({
    limiter: () => limiter,
    key: ({ context }) => `user:${context.user.id}`,
  }))
  .handler(async () => {})
import { createRatelimitMiddleware } from '@orpc/experimental-ratelimit'
import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory'

const limiter = new MemoryRatelimiter({ maxRequests: 10, window: 60_000 })

const example = os
  .use(createRatelimitMiddleware({
    limiter: () => limiter,
    key: ({ context }) => `user:${context.user.id}`,
  }))
  .handler(async () => {})

The Cloudflare rate limiter moved to @orpc/cloudflare.

Deprecated Alias Cheat Sheet

These renames still compile through deprecated aliases, so you can migrate them gradually:

v1 name v2 name Package
isDefinedError isInferableError @orpc/client
InferClientErrorUnion InferClientError @orpc/client
ClientPromiseResult PromiseWithError @orpc/client
eventIterator asyncIteratorObject @orpc/server, @orpc/contract
consumeEventIterator consumeAsyncIterator @orpc/client
InferRouterCurrentContexts InferRouterFinalContexts @orpc/server
CORSPlugin CORSHandlerPlugin @orpc/server/plugins
BodyLimitPlugin RequestLimitHandlerPlugin @orpc/server/plugins
ClientRetryPlugin RetryLinkPlugin @orpc/client/plugins
DedupeRequestsPlugin DedupeLinkPlugin @orpc/client/plugins
RetryAfterPlugin RetryAfterLinkPlugin @orpc/client/plugins
AnyContractRouter RouterContract @orpc/contract
ContractRouterClient RouterContractClient @orpc/contract
minifyContractRouter minifyRouterContract @orpc/contract
useServerAction useServerFunction @orpc/next/hooks
useOptimisticServerAction useOptimisticServerFunction @orpc/next/hooks
createFormAction createServerFormFunction @orpc/next
createORPCVueColadaUtils createPiniaColadaUtils @orpc/pinia-colada

If anything is missing from this guide, check the corresponding page in the v2 docs, or open an issue on GitHub.

Last updated on August 6, 2026

Was this page helpful?