From 9e1b699a32233e39f9983fa889af083d86820294 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 17:24:54 +0700 Subject: [PATCH 1/7] feat(contract): add deprecated ContractRouterClient alias for RouterContractClient --- packages/contract/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index c730668d3..cf7baf709 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -35,6 +35,12 @@ export type { InferRouterContractOutputs as InferContractRouterOutputs, } from './router' export * from './router-client' +export type { + /** + * @deprecated Use `RouterContractClient` instead. + */ + RouterContractClient as ContractRouterClient, +} from './router-client' export * from './router-utils' export { /** From b7e2cc91dbd4fee3f35dadfc00c986beacdf86d8 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 17:25:00 +0700 Subject: [PATCH 2/7] docs: add v1 to v2 migration guide --- apps/content/.vitepress/config.ts | 1 + apps/content/docs/migrations/from-v1.md | 1153 +++++++++++++++++++++++ 2 files changed, 1154 insertions(+) create mode 100644 apps/content/docs/migrations/from-v1.md diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index d3e0a264e..5bacf3605 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -243,6 +243,7 @@ export default withMermaid(defineConfig({ text: 'Migrations', collapsed: true, items: [ + { text: 'Migrating from v1', link: '/docs/migrations/from-v1' }, { text: 'Migrating from tRPC', link: '/docs/migrations/from-trpc' }, ], }, diff --git a/apps/content/docs/migrations/from-v1.md b/apps/content/docs/migrations/from-v1.md new file mode 100644 index 000000000..17bc3774c --- /dev/null +++ b/apps/content/docs/migrations/from-v1.md @@ -0,0 +1,1153 @@ +# 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. + +::: warning Read these first + +- The [RPC Protocol](/docs/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](#middleware). +- The Batch Plugin replaced `exclude` with `filter`, which has the opposite meaning. See [Client Plugins](#client-plugins). + +::: + +## Update Packages + +Install the v2 versions of the packages you use: + +```sh [v2] +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` | + +The Hey API and Durable Iterator were removed. Use [Hibernation](/docs/integrations/hibernation) or [DurablePublisher](/docs/helpers/publisher#adapters) 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](/docs/metadata) via the `openapi` meta from `@orpc/openapi`. See [OpenAPI Routing](/docs/openapi/routing). + +::: code-group + +```ts [v2] +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 }) +``` + +```ts [v1] +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`): + +```ts +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: + +::: code-group + +```ts [v2] +const router = { + planet: os.meta(openapi({ prefix: '/planets' })).lazy(() => import('./planet')), +} +``` + +```ts [v1] +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](/docs/client/server-side). + +::: code-group + +```ts [v2] +import '@orpc/server/extensions/callable' // once at init time +import { os } from '@orpc/server' + +const getting = os + .handler(async () => 'pong') + .callable({ context: {} }) +``` + +```ts [v1] +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](/docs/integrations/next), and "server actions" are now called "server functions". + +::: code-group + +```ts [v2] +'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' +``` + +```ts [v1] +'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](/docs/procedure). + +```ts +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 + +See [Validation Customization](/docs/advanced/validation-customization). + +::: code-group + +```ts [v2] +const base = os.$config({ + disableInputValidation: true, + disableOutputValidation: true, +}) +``` + +```ts [v1] +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()` was removed. See [Metadata](/docs/metadata). + +::: code-group + +```ts [v2] +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 () => {}) +``` + +```ts [v1] +import { os } from '@orpc/server' + +interface ORPCMetadata { + cache?: boolean +} + +const base = os + .$meta({}) + .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](/docs/middleware). + +::: code-group + +```ts [v2] +const merged = aMiddleware.use(anotherMiddleware) + +const example = os + .input(z.object({ id: z.number() })) + .use(canUpdate.adaptInput(input => input.id)) + .handler(async () => {}) +``` + +```ts [v1] +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](/docs/middleware). + +::: code-group + +```ts [v2] +const cacheMiddleware = os.middleware(async ({ next }, input, done) => { + if (cache.has(key)) { + return done({ output: cache.get(key) }) + } + + return next() +}) +``` + +```ts [v1] +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](/docs/best-practices/dedupe-middleware): + +```ts +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](/docs/error-handling) and [RPC Handler](/docs/rpc/handler). + +::: code-group + +```ts [v2] +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 }, +}) +``` + +```ts [v1] +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](/docs/client/error-handling). + +::: code-group + +```ts [v2] +import { isInferableError, safe } from '@orpc/client' + +const [error, data, inferableError, isSuccess] = await safe(client.example({ id: 1 })) + +if (inferableError) { + console.log(inferableError.data.retryAfter) +} +``` + +```ts [v1] +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](/docs/error-handling). + +## AsyncIteratorObject (Event Iterator) + +The "Event Iterator" concept was renamed to [AsyncIteratorObject](/docs/async-iterator-object) (see also [AsyncIteratorObject in Client](/docs/client/async-iterator-object)). All old names still work as deprecated aliases: + +| v1 | v2 | +| ------------------------------------ | ------------------------------------ | +| `eventIterator` | `asyncIteratorObject` | +| `consumeEventIterator` | `consumeAsyncIterator` | +| `eventIteratorToStream` | `asyncIteratorToStream` | +| `eventIteratorToUnproxiedDataStream` | `asyncIteratorToUnproxiedDataStream` | +| `streamToEventIterator` | `streamToAsyncIteratorObject` | + +::: code-group + +```ts [v2] +import { asyncIteratorObject } from '@orpc/server' + +const streaming = os + .output(asyncIteratorObject(z.object({ message: z.string() }))) + .handler(async function* () { + yield { message: 'Hello' } + }) +``` + +```ts [v1] +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](/docs/helpers/publisher) instead. Note that `publish` is now async. + +::: code-group + +```ts [v2] +import { MemoryPublisher } from '@orpc/publisher/memory' + +const publisher = new MemoryPublisher<{ 'something-updated': { id: string } }>() + +await publisher.publish('something-updated', { id: '1' }) +``` + +```ts [v1] +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](/docs/rpc/handler). + +::: code-group + +```ts [v2] +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()], +}) +``` + +```ts [v1] +// 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](/docs/plugins/simple-csrf-protection) 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](/docs/rpc/handler). + +::: code-group + +```ts [v2] +const handler = new RPCHandler(router, { + routingInterceptors: [/* ... */], + fetchInterceptors: [/* ... */], +}) +``` + +```ts [v1] +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](/docs/rpc/handler) and [OpenAPI Specification](/docs/openapi/specification). + +::: code-group + +```ts [v2] +const handler = new RPCHandler(router, { + filter: (contract, path) => !path.includes('internal'), +}) +``` + +```ts [v1] +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](/docs/rpc/serializer). + +::: code-group + +```ts [v2] +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 }) +``` + +```ts [v1] +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](/docs/rpc/handler). + +::: code-group + +```ts [v2] +const handler = new RPCHandler(router, { + toFetchResponse: { // fetch adapter; node uses sendStandardResponse + eventStream: { + keepAlive: { enabled: true, interval: 15000, comment: '' }, + }, + }, +}) +``` + +```ts [v1] +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](/docs/adapters/websocket). + +::: code-group + +```ts [v2] +import { RPCHandler } from '@orpc/server/websocket' + +wss.on('connection', (ws) => { + handler.upgrade(ws, { context: {} }) +}) +``` + +```ts [v1] +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`](/docs/plugins/cors) | +| `RequestHeadersPlugin` | [`RequestHeadersHandlerPlugin`](/docs/plugins/request-headers) | +| `ResponseHeadersPlugin` | [`ResponseHeadersHandlerPlugin`](/docs/plugins/response-headers) | +| `BodyLimitPlugin` (from adapter subpaths) | [`RequestLimitHandlerPlugin`](/docs/plugins/request-limit) (from `@orpc/server/plugins`) | +| `CompressionPlugin` (no alias) | [`RequestCompressionHandlerPlugin`](/docs/plugins/request-compression) + [`ResponseCompressionHandlerPlugin`](/docs/plugins/response-compression) | +| `experimental_RethrowHandlerPlugin` | [`RethrowHandlerPlugin`](/docs/plugins/rethrow) | +| `StrictGetMethodPlugin` (no alias) | removed, use `allowMethods` | + +::: code-group + +```ts [v2] +import { + RequestLimitHandlerPlugin, + ResponseCompressionHandlerPlugin, +} from '@orpc/server/plugins' + +const handler = new RPCHandler(router, { + plugins: [ + new RequestLimitHandlerPlugin({ maxBodySize: 1024 * 1024 }), + new ResponseCompressionHandlerPlugin(), + ], +}) +``` + +```ts [v1] +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](/docs/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](/docs/rpc/link). + +::: code-group + +```ts [v2] +import { RPCLink } from '@orpc/client/fetch' + +const link = new RPCLink({ + origin: 'http://localhost:3000', + url: '/rpc', +}) +``` + +```ts [v1] +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](/docs/rpc/link). + +::: code-group + +```ts [v2] +const link = new RPCLink({ + url: '/rpc', + fetch: (url, init, { context }) => globalThis.fetch(url, { + ...init, + credentials: 'include', + }), +}) +``` + +```ts [v1] +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](/docs/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](/docs/client/client-side). + +::: code-group + +```ts [v2] +import type { RouterContractClient } from '@orpc/contract' + +const client: RouterContractClient = createORPCClient(link) +``` + +```ts [v1] +import type { ContractRouterClient } from '@orpc/contract' + +const client: ContractRouterClient = 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](/docs/adapters/websocket). + +::: code-group + +```ts [v2] +import { RPCLink } from '@orpc/client/websocket' + +const link = new RPCLink({ + connect: () => new WebSocket('ws://localhost:3000'), + reconnect: { enabled: true }, +}) +``` + +```ts [v1] +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`](/docs/plugins/retry) | +| `DedupeRequestsPlugin` | [`DedupeLinkPlugin`](/docs/plugins/dedupe) | +| `RetryAfterPlugin` | [`RetryAfterLinkPlugin`](/docs/plugins/retry-after) | + +v2 also adds new plugins: [Timeout](/docs/plugins/timeout), [Request Compression](/docs/plugins/request-compression), and [Response Compression](/docs/plugins/response-compression). + +::: warning +The [Batch Plugin](/docs/plugins/batch) 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. +::: + +::: code-group + +```ts [v2] +const batchPlugin = new BatchLinkPlugin({ + groups: [{ condition: () => true, context: {} }], + filter: ({ path }) => !path.includes('upload'), // false = not batched +}) +``` + +```ts [v1] +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](/docs/contract/procedure) and [Router Contract](/docs/contract/router). + +| 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](/docs/contract/implementation). + +::: code-group + +```ts [v2] +import { implement } from '@orpc/server' + +const os = implement(contract) + .$context<{ db: Database }>() + .use(loggingMiddleware) +``` + +```ts [v1] +import { implement } from '@orpc/server' + +const os = implement(contract) // context and middleware via separate builders +``` + +::: + +Contract routing uses `openapi()` metadata now, as described in [Routing Moved to OpenAPI Metadata](#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](/docs/openapi/link). + +::: code-group + +```ts [v2] +import { OpenAPILink } from '@orpc/openapi/fetch' + +const link = new OpenAPILink(contract, { + origin: 'http://localhost:3000', + url: '/api', +}) +``` + +```ts [v1] +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](/docs/openapi/specification). + +::: code-group + +```ts [v2] +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' }, + }, +}) +``` + +```ts [v1] +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](/docs/plugins/openapi-reference). + +::: code-group + +```ts [v2] +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' } }, + }), + }), + ], +}) +``` + +```ts [v1] +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](/docs/integrations/zod). + +`ZodSmartCoercionPlugin` was also removed. Use the schema-agnostic [Smart Coercion Plugin](/docs/plugins/smart-coercion) instead, whose option is now named `converters`: + +::: code-group + +```ts [v2] +import { SmartCoercionHandlerPlugin } from '@orpc/json-schema' +import { ZodToJsonSchemaConverter } from '@orpc/zod' + +const handler = new OpenAPIHandler(router, { + plugins: [ + new SmartCoercionHandlerPlugin({ + converters: [new ZodToJsonSchemaConverter()], + }), + ], +}) +``` + +```ts [v1] +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](/docs/integrations/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](/docs/integrations/tanstack-query). + +::: code-group + +```ts [v2] +import { createTanstackQueryUtils } from '@orpc/tanstack-query' + +const orpc = createTanstackQueryUtils(client, { prefix: 'user' }) + +orpc.streamed.streamedOptions({ input: {} }) +orpc.live.liveOptions({ input: {} }) +``` + +```ts [v1] +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](#custom-serializers-use-a-serializer-instance)). + +### SWR and Pinia Colada + +`@orpc/experimental-react-swr` is now `@orpc/swr` (see [SWR Integration](/docs/integrations/swr)), and `@orpc/vue-colada` is now `@orpc/pinia-colada` with `createORPCVueColadaUtils` renamed to `createPiniaColadaUtils` (see [Pinia Colada Integration](/docs/integrations/pinia-colada)). 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](/docs/integrations/nest). + +::: code-group + +```ts [v2] +import { Implement } from '@orpc/nest' +import { implement, ORPCError } from '@orpc/server' + +declare module '@orpc/server' { + interface DefaultInitialContext { + request: Request + } +} +``` + +```ts [v1] +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](/docs/integrations/ai-sdk). + +::: code-group + +```ts [v2] +import { createToolFactory } from '@orpc/ai-sdk' + +const createTool = createToolFactory({ context: {} }) +const tool = createTool(someProcedure) +``` + +```ts [v1] +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](/docs/integrations/hibernation). + +### Logging and tracing + +`@orpc/experimental-pino` is now `@orpc/pino`, with `LoggingHandlerPlugin` renamed to `PinoHandlerPlugin` (see [Pino Integration](/docs/integrations/pino)). `@orpc/otel` is now `@orpc/opentelemetry`, and context propagation works out of the box (see [OpenTelemetry Integration](/docs/integrations/opentelemetry)). + +## 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](/docs/helpers/publisher). + +::: code-group + +```ts [v2] +import { RedisPublisher } from '@orpc/publisher/redis' + +const publisher = new RedisPublisher(client, { + subscriber, + resume: { enabled: true, seconds: 300 }, +}) +``` + +```ts [v1] +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](/docs/helpers/ratelimit). + +::: code-group + +```ts [v2] +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 () => {}) +``` + +```ts [v1] +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](https://github.com/middleapi/orpc/issues). From 8d554c4ac448cb58e43aebedf2dfdef4bae721f5 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 17:59:20 +0700 Subject: [PATCH 3/7] chore(scripts): skip [v1] code fences in docs jsdoc verification --- scripts/verify-docs-jsdoc.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/verify-docs-jsdoc.ts b/scripts/verify-docs-jsdoc.ts index 1517404ed..982825ea1 100644 --- a/scripts/verify-docs-jsdoc.ts +++ b/scripts/verify-docs-jsdoc.ts @@ -101,7 +101,10 @@ function extractCodeFences(markdown: string): { code: string[], outside: string[ if (open) { const lang = open[2]!.split(/[\s[{:]/)[0] ?? '' - fence = { marker: open[1]!, lines: [], isCode: CODE_LANGS.has(lang) } + // Fences labeled `[v1]` (migration guides) show previous-major code whose + // APIs no longer exist, so their imports must not be verified. + const isV1 = /\[v1\]\s*$/.test(line) + fence = { marker: open[1]!, lines: [], isCode: CODE_LANGS.has(lang) && !isV1 } continue } From 36d4da442ba8720349a770cac4c3df32646c857d Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 19:23:54 +0700 Subject: [PATCH 4/7] docs: improve v1 migration guide writing and recommend v2-first approaches --- apps/content/docs/migrations/from-v1.md | 131 ++++++++++++++++-------- 1 file changed, 91 insertions(+), 40 deletions(-) diff --git a/apps/content/docs/migrations/from-v1.md b/apps/content/docs/migrations/from-v1.md index 17bc3774c..fe17126f7 100644 --- a/apps/content/docs/migrations/from-v1.md +++ b/apps/content/docs/migrations/from-v1.md @@ -34,11 +34,13 @@ Some packages were renamed, merged, or promoted from experimental status: | `@orpc/otel` | `@orpc/opentelemetry` | | `@orpc/server/hibernation` (subpath) | `@orpc/hibernation` | -The Hey API and Durable Iterator were removed. Use [Hibernation](/docs/integrations/hibernation) or [DurablePublisher](/docs/helpers/publisher#adapters) in place of Durable Iterator. +::: warning +The Hey API and Durable Iterator integrations no longer exist in v2. In place of Durable Iterator, use [Hibernation](/docs/integrations/hibernation) or [DurablePublisher](/docs/helpers/publisher#adapters). +::: ## 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](/docs/metadata) via the `openapi` meta from `@orpc/openapi`. See [OpenAPI Routing](/docs/openapi/routing). +The biggest change: `.route`, `.prefix`, `.tag`, and `.$route` no longer exist on the builder. OpenAPI routing now lives in [metadata](/docs/metadata), set with the `openapi` helper from `@orpc/openapi`. See [OpenAPI Routing](/docs/openapi/routing). ::: code-group @@ -67,7 +69,8 @@ const router = os ::: -If you prefer the old `.route` style, a compatibility extension restores it (but not `.prefix`, `.tag`, or `.$route`): +::: tip +If you prefer the old style, the [`.route` extension](/docs/openapi/routing#route-extension) brings `.route` back (but not `.prefix`, `.tag`, or `.$route`): ```ts import '@orpc/openapi/extensions/route' // once at init time @@ -77,6 +80,8 @@ const listPlanet = os .handler(async () => []) ``` +::: + The same applies to lazy routers with a prefix: ::: code-group @@ -97,23 +102,35 @@ const router = { ## Procedure Builder -### `.callable` requires an extension +### `.callable` is no longer built in -`.callable` moved behind a side effect import. See [Server-Side Clients](/docs/client/server-side). +In v2, prefer [`call` or `createRouterClient`](/docs/client/server-side) to call procedures on the server: ::: code-group ```ts [v2] -import '@orpc/server/extensions/callable' // once at init time -import { os } from '@orpc/server' +import { call } from '@orpc/server' + +const getting = os.handler(async () => 'pong') +const result = await call(getting, undefined, { context: {} }) +``` + +```ts [v1] const getting = os .handler(async () => 'pong') .callable({ context: {} }) + +const result = await getting() ``` -```ts [v1] -import { os } from '@orpc/server' +::: + +::: tip +If you prefer the v1 style, the [`.callable` extension](/docs/client/server-side#callable-extension) brings `.callable` back: + +```ts +import '@orpc/server/extensions/callable' // once at init time const getting = os .handler(async () => 'pong') @@ -124,23 +141,28 @@ const getting = os ### `.actionable` moved to `@orpc/next` -Server actions now live in the [Next.js Integration](/docs/integrations/next), and "server actions" are now called "server functions". +Server actions are now called [server functions](/docs/integrations/next#server-functions) and live in the [Next.js Integration](/docs/integrations/next). In v2, prefer `createServerFunctionable` (or `createServerFunction`): it works exactly like `.actionable`, returning a value that is both a server function and a regular procedure. ::: code-group ```ts [v2] 'use server' -import { createServerFunction } from '@orpc/next' -export const getting = createServerFunction(gettingProcedure, { context: {} }) +import { createServerFunctionable } from '@orpc/next' +import { os } from '@orpc/server' + +const functionable = createServerFunctionable({ context: {} }) -// or restore .actionable with a side effect import: -// import '@orpc/next/extensions/actionable' +export const getting = functionable( + os.handler(async () => 'pong'), +) ``` ```ts [v1] 'use server' +import { os } from '@orpc/server' + export const getting = os .handler(async () => 'pong') .actionable({ context: {} }) @@ -148,11 +170,20 @@ export const getting = os ::: -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). +::: tip +If you prefer the v1 style, the [`.actionable` extension](/docs/integrations/next#actionable-extension) brings `.actionable` back: + +```ts +import '@orpc/next/extensions/actionable' // once at init time +``` + +::: + +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`](/docs/integrations/next#server-form-functions). ### `.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](/docs/procedure). +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](/docs/procedure#multiple-schemas). ```ts const example = os @@ -161,11 +192,13 @@ const example = os .handler(async ({ input }) => {}) // input: { name: string } & { id: number } ``` -`.$input` was removed along with this change. +::: warning +Use loose object schemas (like `z.looseObject`) when stacking, so one schema does not strip the keys another schema needs. `.$input` was removed along with this change. +::: ### `.$config` options changed -See [Validation Customization](/docs/advanced/validation-customization). +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](/docs/advanced/validation-customization). ::: code-group @@ -294,7 +327,13 @@ const cacheMiddleware = os.middleware(async ({ next }, input, output) => { ### 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](/docs/best-practices/dedupe-middleware): +v1 automatically skipped router-level middleware that was already applied to a procedure. v2 no longer does this. + +::: warning +Middleware applied at both the router and the procedure level now runs twice. Nothing warns you about it: an auth or logging middleware simply executes two times per request. +::: + +Guard shared middleware yourself with the context flag pattern from [Dedupe Middleware](/docs/best-practices/dedupe-middleware): ```ts const authMiddleware = os @@ -378,7 +417,9 @@ if (error && isDefined) { ::: +::: tip v2 also introduces `error` factories for defining reusable typed errors outside `.errors`. See [Error Handling](/docs/error-handling). +::: ## AsyncIteratorObject (Event Iterator) @@ -466,6 +507,10 @@ const handler = new RPCHandler(router) ::: +::: tip +The simplest migration is to stop sending GET instead of allowing it: remove the `method` option from your [RPC Link](/docs/rpc/link) so every call uses POST (the default), and keep the handler's default `allowMethods`. Only allow GET when you really need it, for example for HTTP caching. +::: + The v2 [Simple CSRF Protection Plugin](/docs/plugins/simple-csrf-protection) 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 @@ -512,7 +557,7 @@ const handler = new RPCHandler(router, { ### 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](/docs/rpc/serializer). +The `customJsonSerializers` option with numeric types was replaced by a `serializer` instance with string-keyed handlers, shared between [RPC Handler](/docs/rpc/handler) and [RPC Link](/docs/rpc/link). To override a built-in type, reuse its key (for example `date`) instead of matching a magic number. See [RPC Serializer](/docs/rpc/serializer). ::: code-group @@ -769,6 +814,8 @@ The [Batch Plugin](/docs/plugins/batch) replaced `exclude` with `filter`, and th ::: code-group ```ts [v2] +import { BatchLinkPlugin } from '@orpc/client/plugins' + const batchPlugin = new BatchLinkPlugin({ groups: [{ condition: () => true, context: {} }], filter: ({ path }) => !path.includes('upload'), // false = not batched @@ -776,6 +823,8 @@ const batchPlugin = new BatchLinkPlugin({ ``` ```ts [v1] +import { BatchLinkPlugin } from '@orpc/client/plugins' + const batchPlugin = new BatchLinkPlugin({ groups: [{ condition: () => true, context: {} }], exclude: ({ path }) => path.includes('upload'), // true = not batched @@ -1062,7 +1111,7 @@ Base64Url, Cookie, Encryption, and Signing helpers are unchanged in `@orpc/serve ### Publisher -Now `@orpc/publisher`. The resume option was restructured, and the Redis adapter switched from `ioredis` to `node-redis`. See [Publisher Helpers](/docs/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](/docs/helpers/publisher). ::: code-group @@ -1091,7 +1140,7 @@ The Durable Object adapter moved to `@orpc/cloudflare`, with `PublisherDurableOb ### Rate Limit -Now `@orpc/ratelimit`. Note the casing change from `Ratelimiter` to `RateLimiter`, and the middleware helper rename. See [Rate Limit Helpers](/docs/helpers/ratelimit). +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](/docs/helpers/ratelimit). ::: code-group @@ -1131,23 +1180,25 @@ The Cloudflare rate limiter moved to `@orpc/cloudflare`. 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` | +| 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](https://github.com/middleapi/orpc/issues). From bde4313729df1761e2be137653c40e6a4074184b Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 19:30:38 +0700 Subject: [PATCH 5/7] docs: batch plugin exclude is usually removable in v2 --- apps/content/docs/migrations/from-v1.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/content/docs/migrations/from-v1.md b/apps/content/docs/migrations/from-v1.md index fe17126f7..f2adb1597 100644 --- a/apps/content/docs/migrations/from-v1.md +++ b/apps/content/docs/migrations/from-v1.md @@ -6,7 +6,7 @@ This guide walks you through upgrading an oRPC v1 app to v2. Most of your code k - The [RPC Protocol](/docs/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](#middleware). -- The Batch Plugin replaced `exclude` with `filter`, which has the opposite meaning. See [Client Plugins](#client-plugins). +- The Batch Plugin replaced `exclude` with `filter`, which has the opposite meaning. In most cases you can simply remove `exclude`. See [Batch Plugin](#batch-plugin). ::: @@ -807,9 +807,9 @@ Link plugins were renamed with a `LinkPlugin` suffix. Deprecated aliases exist f v2 also adds new plugins: [Timeout](/docs/plugins/timeout), [Request Compression](/docs/plugins/request-compression), and [Response Compression](/docs/plugins/response-compression). -::: warning -The [Batch Plugin](/docs/plugins/batch) 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. -::: +### Batch Plugin + +The v2 [Batch Plugin](/docs/plugins/batch) supports every response type, including [AsyncIteratorObject](/docs/async-iterator-object) and [File/Blob](/docs/binary-data). In v1, `exclude` existed mainly to skip those unsupported responses, so in most cases you can simply remove it: ::: code-group @@ -818,7 +818,6 @@ import { BatchLinkPlugin } from '@orpc/client/plugins' const batchPlugin = new BatchLinkPlugin({ groups: [{ condition: () => true, context: {} }], - filter: ({ path }) => !path.includes('upload'), // false = not batched }) ``` @@ -827,7 +826,21 @@ import { BatchLinkPlugin } from '@orpc/client/plugins' const batchPlugin = new BatchLinkPlugin({ groups: [{ condition: () => true, context: {} }], - exclude: ({ path }) => path.includes('upload'), // true = not batched + exclude: ({ path }) => { + return ['planets/getImage', 'planets/subscribe'].includes(path.join('/')) + }, +}) +``` + +::: + +::: warning +Some cases still need to skip batching, for example procedures that rely on [Hibernation](/docs/integrations/hibernation), which cannot work through batched responses. For those, use `filter`. Its meaning is inverted compared to `exclude`: `exclude` returned `true` to skip batching, while `filter` returns `false` to skip batching. Negate your predicate when migrating. + +```ts +const batchPlugin = new BatchLinkPlugin({ + groups: [{ condition: () => true, context: {} }], + filter: ({ path }) => path.join('/') !== 'chat/subscribe', // false = not batched }) ``` From 8416a47ed41c536dd391eeea4a84cb06d3aefcf8 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 19:38:29 +0700 Subject: [PATCH 6/7] docs: fix implementer claim, v1 already supported context and middleware --- apps/content/docs/migrations/from-v1.md | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/apps/content/docs/migrations/from-v1.md b/apps/content/docs/migrations/from-v1.md index f2adb1597..a38241653 100644 --- a/apps/content/docs/migrations/from-v1.md +++ b/apps/content/docs/migrations/from-v1.md @@ -862,26 +862,14 @@ Contract types and utilities changed word order from `ContractRouter*` to `Route | `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](/docs/contract/implementation). +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](/docs/advanced/validation-customization), either as a second argument or through `.$config`. See [Contract Implementation](/docs/contract/implementation). -::: code-group - -```ts [v2] -import { implement } from '@orpc/server' - -const os = implement(contract) - .$context<{ db: Database }>() - .use(loggingMiddleware) -``` - -```ts [v1] +```ts import { implement } from '@orpc/server' -const os = implement(contract) // context and middleware via separate builders +const os = implement(contract, { disableOutputValidation: true }) ``` -::: - Contract routing uses `openapi()` metadata now, as described in [Routing Moved to OpenAPI Metadata](#routing-moved-to-openapi-metadata). ## OpenAPI From b95999fc709d44c48f7cf59b9c17799142443e96 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Tue, 4 Aug 2026 19:40:08 +0700 Subject: [PATCH 7/7] docs: clarify oo removal and customComponentName with inline links --- apps/content/docs/migrations/from-v1.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/content/docs/migrations/from-v1.md b/apps/content/docs/migrations/from-v1.md index a38241653..4ce1caf05 100644 --- a/apps/content/docs/migrations/from-v1.md +++ b/apps/content/docs/migrations/from-v1.md @@ -937,7 +937,10 @@ const spec = await generator.generate(router, { ::: -The `oo` helper (`oo.spec`) was removed. Attach specs with `openapi({ spec })` metadata instead. The `shouldHoistDef` option was replaced by `customComponentName`. +Two smaller changes in the same area: + +- The `oo` helper (`oo.spec`) was removed. To customize the operation object, attach [`openapi({ spec })` metadata](/docs/openapi/specification#customizing-the-operation-object) directly on the procedure or router. +- The `shouldHoistDef` option was replaced by [`customComponentName`](/docs/openapi/specification#hoisting-defs). Root `$defs` are now always hoisted into `components.schemas`; this option only renames them. ### `OpenAPIReferencePlugin` renamed and reshaped