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.
Read these first
- The RPC Protocol changed, so a v1 client cannot talk to a v2 server. Deploy the upgraded server and client together.
- Automatic middleware deduplication was removed. Middleware applied at both router and procedure level now runs twice. See Middleware.
- The Batch Plugin replaced
excludewithfilter, which has the opposite meaning. See Client Plugins.
Update Packages
Install the v2 versions of the packages you use:
npm install @orpc/server@beta @orpc/client@betaSome 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 |
The Hey API and Durable Iterator were removed. Use Hibernation or DurablePublisher in place of Durable Iterator.
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 via the openapi meta 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 })If you prefer the old .route style, a compatibility extension restores it (but not .prefix, .tag, or .$route):
import '@orpc/openapi/extensions/route' // once at init time
const listPlanet = os
.route({ method: 'GET', path: '/planets' }) // works again
.handler(async () => [])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 requires an extension
.callable moved behind a side effect import. See Server-Side Clients.
import '@orpc/server/extensions/callable' // once at init time
import { os } from '@orpc/server'
const getting = os
.handler(async () => 'pong')
.callable({ context: {} })import { os } from '@orpc/server'
const getting = os
.handler(async () => 'pong')
.callable({ context: {} }).actionable moved to @orpc/next
Server actions now live in the Next.js Integration, and "server actions" are now called "server functions".
'use server'
import { createServerFunction } from '@orpc/next'
export const getting = createServerFunction(gettingProcedure, { context: {} })
// or restore .actionable with a side effect import:
// import '@orpc/next/extensions/actionable''use 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).
.input and .output now stack
Calling .input or .output again adds another schema instead of not allowing in v1. Use loose object schemas when stacking. See Procedure.
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 }.$input was removed along with this change.
.$config options changed
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, so shared middleware runs twice unless you guard it yourself. Use 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)
}v2 also introduces error factories for defining reusable typed errors outside .errors. See Error Handling.
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 handler and link. 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(),
],
})INFO
When serving binary data cross-origin, allow and expose both the Content-Disposition and the new Standard-Server headers in your CORS configuration. See Binary Data.
Client
RPCLink splits url into origin and url
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',
}),
})Link interceptors renamed
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)WebSocket link uses a connect factory
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.
WARNING
The Batch Plugin replaced exclude with filter, and the meaning is inverted: exclude returned true to skip batching, filter returns false to skip batching. Negate your predicate when migrating.
const batchPlugin = new BatchLinkPlugin({
groups: [{ condition: () => true, context: {} }],
filter: ({ path }) => !path.includes('upload'), // false = not batched
})const batchPlugin = new BatchLinkPlugin({
groups: [{ condition: () => true, context: {} }],
exclude: ({ path }) => path.includes('upload'), // true = not batched
})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 also gained .$context, .use, and .middleware, so you can set up context and shared middleware directly. See Contract Implementation.
import { implement } from '@orpc/server'
const os = implement(contract)
.$context<{ db: Database }>()
.use(loggingMiddleware)import { implement } from '@orpc/server'
const os = implement(contract) // context and middleware via separate buildersContract routing uses openapi() metadata now, as described in Routing Moved to OpenAPI Metadata.
OpenAPI
OpenAPILink moved into @orpc/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' },
})The oo helper (oo.spec) was removed. Attach specs with openapi({ spec }) metadata instead. The shouldHoistDef option was replaced by customComponentName.
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
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
Now @orpc/ratelimit. Note the casing change from Ratelimiter to RateLimiter, 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 |
createFormAction | createServerFormFunction | @orpc/next |
If anything is missing from this guide, check the corresponding page in the v2 docs, or open an issue on GitHub.

