orpc-migrate

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrating to oRPC

迁移到 oRPC

Playbook for two migrations: tRPC to oRPC, and oRPC v1 to v2. Work in small mechanical steps and run the project's typecheck and test suite after each one, so any failure points at the last step. Pretrained knowledge of oRPC describes v1 and is often wrong for v2: derive every import path, builder method, and option name from the docs pages listed at the end, never from memory. For core v2 concepts while rewriting (the
os
builder, routers, middleware, clients), load the
orpc
skill. v2 currently ships under the
beta
npm dist-tag and plain installs get v1; drop the
@beta
suffix once
npm view @orpc/server dist-tags
shows
latest
at 2.x.
本指南涵盖两类迁移:tRPC 到 oRPC,以及 oRPC v1 到 v2。以小而机械化的步骤推进,并在每一步后运行项目的 typecheck 和 test suite,以便让问题在最后一步之前就暴露出来。预训练知识中关于 oRPC 的描述针对的是 v1,通常不适用于 v2:每个 import path、builder method 和 option name 都应从末尾列出的文档页面推导,绝不能凭记忆。重写时涉及 v2 的核心概念(
os
builder、routers、middleware、clients),请加载
orpc
skill。v2 目前通过
beta
npm dist-tag 发布,普通安装会获得 v1;待
npm view @orpc/server dist-tags
显示
latest
为 2.x 后,去掉
@beta
后缀。

tRPC to oRPC

tRPC 到 oRPC

Two paths. Pick incremental when the app must keep shipping or the tRPC router is large; pick the full rewrite when the router is small enough to convert in one pass.
有两条迁移路径。当应用必须继续发布,或 tRPC router 规模较大时,选择增量方式;当 router 足够小、可以一次完成转换时,选择完整重写。

Incremental: wrap the existing tRPC router

增量方式:封装现有 tRPC router

Install
@orpc/trpc@beta
and convert. The result is a regular oRPC router: expose it through an RPC or OpenAPI handler, or call it with a server-side client, while the tRPC code keeps working untouched.
ts
import { toORPCRouter } from '@orpc/trpc'

const orpcRouter = toORPCRouter(trpcRouter)
  • tRPC error formatting is not supported: tRPC errors arrive wrapped in
    ORPCError
    with the
    TRPCError
    as
    cause
    (and a
    ZodError
    below that for validation failures). Reshape them in a handler interceptor if consumers need structured errors.
  • toTRPCMeta
    bridges oRPC
    openapi()
    metadata into tRPC
    .meta()
    so converted procedures get OpenAPI routing. Chained tRPC
    .meta()
    calls merge shallowly, so keep all oRPC metadata inside a single
    toTRPCMeta
    call.
Then rewrite leaf routers to native oRPC one at a time, mounting each next to the converted router in one plain object.
安装
@orpc/trpc@beta
并进行转换。转换结果是一个常规的 oRPC router:可以通过 RPC 或 OpenAPI handler 暴露,也可以使用 server-side client 调用,而 tRPC 代码无需改动即可继续工作。
ts
import { toORPCRouter } from '@orpc/trpc'

const orpcRouter = toORPCRouter(trpcRouter)
  • 不支持 tRPC error formatting:tRPC errors 到达时会被包装在
    ORPCError
    中,
    TRPCError
    作为
    cause
    (对于 validation failures,其下还会有一个
    ZodError
    )。如果使用方需要结构化 errors,可在 handler interceptor 中调整其结构。
  • toTRPCMeta
    将 oRPC
    openapi()
    metadata 桥接到 tRPC
    .meta()
    ,让转换后的 procedures 获得 OpenAPI routing。链式调用 tRPC
    .meta()
    时只会进行浅合并,因此应把所有 oRPC metadata 放在同一次
    toTRPCMeta
    调用中。
随后一次只重写一个叶子 router,将每个重写后的 router 与已转换的 router 并列挂载到同一个普通对象中。

Full rewrite: concept map

完整重写:概念映射

ConcepttRPCoRPC
Router
t.router({...})
plain object
Procedure builder
t.procedure
os
Context
initTRPC.context<T>()
os.$context<T>()
Create middleware
t.middleware(fn)
os.middleware(fn)
Use middleware
.use(mw)
.use(mw)
Validation
.input(schema)
/
.output(schema)
same names
Implementation
.query()
/
.mutation()
/
.subscription()
.handler()
for all three
Errors
new TRPCError({ code, ... })
new ORPCError(code, { ... })
Serializer
superjson
transformer
built in, remove
superjson
Steps, in order, verifying after each:
  1. Packages. Remove
    @trpc/server
    ,
    @trpc/client
    ,
    @trpc/tanstack-react-query
    ; install
    @orpc/server@beta
    ,
    @orpc/client@beta
    ,
    @orpc/tanstack-query@beta
    .
  2. Base file. Port the context factory unchanged, then rebuild the shared procedures. In handlers and middleware,
    ctx
    becomes
    context
    :
    ts
    import { ORPCError, os } from '@orpc/server'
    
    const o = os.$context<Awaited<ReturnType<typeof createContext>>>()
    
    export const publicProcedure = o.use(timingMiddleware)
    export const protectedProcedure = publicProcedure.use(({ context, next }) => {
      if (!context.session?.user)
        throw new ORPCError('UNAUTHORIZED')
      return next({ context: { session: context.session } })
    })
  3. Procedures. Replace
    .query
    /
    .mutation
    /
    .subscription
    with
    .handler
    ;
    .input
    and
    .output
    carry over as is.
  4. App router. Delete every
    createTRPCRouter()
    wrapper; nested plain objects are the router.
  5. Server. Replace the tRPC adapter with an oRPC handler for the runtime (fetch shown; other adapters exist for Node, Fastify, AWS Lambda, WebSocket):
    ts
    import { RPCHandler } from '@orpc/server/fetch'
    
    const handler = new RPCHandler(appRouter)
    
    const { response } = await handler.handle(request, {
      prefix: '/api/orpc',
      context: await createContext({ headers: request.headers }),
    })
  6. Client.
    RPCLink
    plus
    createORPCClient
    , typed by
    RouterClient
    . Call sites drop the
    .query()
    /
    .mutate()
    suffixes:
    ts
    import type { RouterClient } from '@orpc/server'
    import { createORPCClient } from '@orpc/client'
    import { RPCLink } from '@orpc/client/fetch'
    
    const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc' })
    export const client: RouterClient<typeof appRouter> = createORPCClient(link)
    
    const { planets } = await client.planet.list({ cursor: 0 })
  7. TanStack Query.
    createTanstackQueryUtils(client)
    replaces the provider and
    useTRPC
    hook entirely; use the utils object directly. Input moves inside an
    input
    key:
    orpc.planet.list.queryOptions({ input: { cursor: 0 } })
    ,
    orpc.planet.create.mutationOptions()
    . For infinite queries,
    infiniteOptions
    takes
    input
    as a function of the page param.
概念tRPCoRPC
Router
t.router({...})
普通对象
Procedure builder
t.procedure
os
Context
initTRPC.context<T>()
os.$context<T>()
创建 middleware
t.middleware(fn)
os.middleware(fn)
使用 middleware
.use(mw)
.use(mw)
验证
.input(schema)
/
.output(schema)
同名
实现
.query()
/
.mutation()
/
.subscription()
三者均使用
.handler()
错误
new TRPCError({ code, ... })
new ORPCError(code, { ... })
序列化器
superjson
transformer
内置,移除
superjson
按以下顺序执行,并在每一步后进行验证:
  1. 包。 移除
    @trpc/server
    、
    @trpc/client
    、
    @trpc/tanstack-react-query
    ;安装
    @orpc/server@beta
    、
    @orpc/client@beta
    、
    @orpc/tanstack-query@beta
    。
  2. 基础文件。 保持不变地移植 context factory,然后重建 shared procedures。在 handlers 和 middleware 中,
    ctx
    变为
    context
    :
    ts
    import { ORPCError, os } from '@orpc/server'
    
    const o = os.$context<Awaited<ReturnType<typeof createContext>>>()
    
    export const publicProcedure = o.use(timingMiddleware)
    export const protectedProcedure = publicProcedure.use(({ context, next }) => {
      if (!context.session?.user)
        throw new ORPCError('UNAUTHORIZED')
      return next({ context: { session: context.session } })
    })
  3. Procedure。 将
    .query
    /
    .mutation
    /
    .subscription
    替换为
    .handler
    ;
    .input
    和
    .output
    原样保留。
  4. App router。 删除所有
    createTRPCRouter()
    wrapper;嵌套的普通对象就是 router。
  5. Server。 将 tRPC adapter 替换为适用于当前 runtime 的 oRPC handler(图中为 fetch;另有适用于 Node、Fastify、AWS Lambda、WebSocket 的 adapters):
    ts
    import { RPCHandler } from '@orpc/server/fetch'
    
    const handler = new RPCHandler(appRouter)
    
    const { response } = await handler.handle(request, {
      prefix: '/api/orpc',
      context: await createContext({ headers: request.headers }),
    })
  6. Client。
    RPCLink
    与
    createORPCClient
    配合使用,并通过
    RouterClient
    提供类型。调用处去掉
    .query()
    /
    .mutate()
    后缀:
    ts
    import type { RouterClient } from '@orpc/server'
    import { createORPCClient } from '@orpc/client'
    import { RPCLink } from '@orpc/client/fetch'
    
    const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc' })
    export const client: RouterClient<typeof appRouter> = createORPCClient(link)
    
    const { planets } = await client.planet.list({ cursor: 0 })
  7. TanStack Query。
    createTanstackQueryUtils(client)
    完全替代 provider 和
    useTRPC
    hook;直接使用 utils 对象。input 移入
    input
    key:
    orpc.planet.list.queryOptions({ input: { cursor: 0 } })
    、
    orpc.planet.create.mutationOptions()
    。对于 infinite queries,
    infiniteOptions
    接收一个以 page param 为参数的
    input
    函数。

oRPC v1 to v2

oRPC v1 到 v2

Most v1 names still compile through deprecated aliases (strike-through hints, not errors), so migrate in passes. Order of operations:
  1. Update packages. Install every
    @orpc/*
    package from the
    beta
    dist-tag (
    npm install @orpc/server@beta @orpc/client@beta
    , and so on). Swap renamed ones first:
    @orpc/react-query
    /
    @orpc/vue-query
    /
    @orpc/solid-query
    /
    @orpc/svelte-query
    all became
    @orpc/tanstack-query
    ;
    @orpc/openapi-client
    merged into
    @orpc/openapi
    ;
    @orpc/react
    became
    @orpc/next
    ;
    @orpc/otel
    became
    @orpc/opentelemetry
    ; the
    experimental-
    packages were promoted (
    @orpc/publisher
    ,
    @orpc/ratelimit
    ,
    @orpc/pino
    ,
    @orpc/swr
    );
    @orpc/vue-colada
    became
    @orpc/pinia-colada
    . Typecheck: the remaining errors are the hard breaks.
  2. Fix the hard breaks (no aliases):
    • Routing:
      .route
      ,
      .prefix
      ,
      .tag
      ,
      .$route
      are gone from the builder. Use
      .meta(openapi({ method, path, prefix, tags }))
      from
      @orpc/openapi
      , or restore
      .route
      with
      import '@orpc/openapi/extensions/route'
      .
    • .callable
      and
      .actionable
      : use
      call
      /
      createRouterClient
      from
      @orpc/server
      and
      createServerFunctionable
      from
      @orpc/next
      , or the corresponding extension imports.
    • RPCLink
      : the single
      url
      split into
      origin
      plus a path-only
      url
      .
    • Errors:
      status
      was removed from
      ORPCError
      and
      .errors
      definitions; map codes to HTTP status with
      errorStatusMap
      on the handler.
    • safe()
      : the third tuple element is now the typed error itself (or
      null
      ) and a fourth
      isSuccess
      element was added.
    • Option renames, scoped: handler
      rootInterceptors
      to
      routingInterceptors
      (handler
      clientInterceptors
      still exists, unchanged); link
      clientInterceptors
      to
      transportInterceptors
      . Flat
      eventIterator*
      options moved under the adapter's request/response mapping:
      toFetchResponse.eventStream
      on the fetch handler,
      sendStandardResponse.eventStream
      on Node,
      toFetchRequest.eventStream
      on the link.
    • adapterInterceptors
      was removed from handlers and links, because regular interceptors can now customize body parsing behavior.
  3. Audit silent behavior changes (compile fine, behave differently):
    • Wire format changed: a v1 link cannot talk to a v2 server, in either direction. Deploy the upgraded server and clients together.
    • Automatic middleware deduplication removed: middleware applied at both router and procedure level now runs twice, with no warning. Guard shared middleware with the context-flag pattern from the dedupe-middleware recipe.
    • Batch Plugin
      exclude
      became
      filter
      with the opposite meaning.
      Usually delete
      exclude
      ; if skipping is still needed, negate the predicate.
    • RPCHandler
      rejects GET by default
      (
      allowMethods
      defaults to POST/PUT/PATCH/DELETE). Simplest fix: stop sending GET from the link; only allow GET deliberately, with CSRF protection.
    • Handler
      filter
      takes positional arguments now;
      the v1 destructured form still type-checks but reads wrong values.
    • .input
      /
      .output
      now stack:
      a repeated call adds a schema instead of replacing the previous one.
  4. Sweep deprecated aliases last:
    eventIterator
    to
    asyncIteratorObject
    , handler plugins gained a
    HandlerPlugin
    suffix and link plugins a
    LinkPlugin
    suffix,
    ContractRouter*
    types became
    RouterContract*
    . The from-v1 guide ends with the full alias cheat sheet.
Verification: typecheck and unit tests after steps 1, 2, and 4; step 3 needs integration or e2e tests, since those changes never surface at compile time. Before finishing, grep for old package names and remaining deprecation strike-throughs.
大多数 v1 名称仍可通过 deprecated aliases 编译(删除线提示,而不是报错),因此应分批迁移。操作顺序如下:
  1. 更新包。 从
    beta
    dist-tag 安装所有
    @orpc/*
    包(
    npm install @orpc/server@beta @orpc/client@beta
    ,依此类推)。先替换重命名的包:
    @orpc/react-query
    /
    @orpc/vue-query
    /
    @orpc/solid-query
    /
    @orpc/svelte-query
    均已更名为
    @orpc/tanstack-query
    ;
    @orpc/openapi-client
    已合并为
    @orpc/openapi
    ;
    @orpc/react
    已更名为
    @orpc/next
    ;
    @orpc/otel
    已更名为
    @orpc/opentelemetry
    ;带
    experimental-
    前缀的包已正式发布(
    @orpc/publisher
    、
    @orpc/ratelimit
    、
    @orpc/pino
    、
    @orpc/swr
    );
    @orpc/vue-colada
    已更名为
    @orpc/pinia-colada
    。执行 typecheck:剩余错误才是真正的破坏性变更。
  2. 修复硬性破坏(hard breaks)(没有 aliases):
    • Routing: builder 中的
      .route
      、
      .prefix
      、
      .tag
      、
      .$route
      已移除。使用
      @orpc/openapi
      中的
      .meta(openapi({ method, path, prefix, tags }))
      ,或通过
      import '@orpc/openapi/extensions/route'
      恢复
      .route
      。
    • .callable
      和
      .actionable
      :
      使用
      @orpc/server
      中的
      call
      /
      createRouterClient
      和
      @orpc/next
      中的
      createServerFunctionable
      ,或使用对应的 extension imports。
    • RPCLink
      :
      原来的单个
      url
      已拆分为
      origin
      和仅包含路径的
      url
      。
    • Errors:
      ORPCError
      和
      .errors
      definitions 中已移除
      status
      ;在 handler 上使用
      errorStatusMap
      将 codes 映射到 HTTP status。
    • safe()
      :
      第三个 tuple element 现在是 typed error 本身(或
      null
      ),并新增了第四个
      isSuccess
      element。
    • 选项重命名(按范围区分): handler 的
      rootInterceptors
      改为
      routingInterceptors
      (handler 的
      clientInterceptors
      仍存在且保持不变);link 的
      clientInterceptors
      改为
      transportInterceptors
      。扁平的
      eventIterator*
      选项已移至 adapter 的 request/response mapping 下:fetch handler 使用
      toFetchResponse.eventStream
      ,Node 使用
      sendStandardResponse.eventStream
      ,link 使用
      toFetchRequest.eventStream
      。
    • adapterInterceptors
      已从 handlers 和 links 中移除,因为普通 interceptors 现在可以自定义 body parsing behavior。
  3. 审计静默行为变更(可以编译通过,但行为不同):
    • Wire format 已变更: v1 link 无法与 v2 server 通信,两个方向均不可行。应将升级后的 server 和 clients 一起部署。
    • 已移除自动 middleware 去重: 同时应用于 router 和 procedure 级别的 middleware 现在会运行两次,且不会发出警告。使用 dedupe-middleware recipe 中的 context-flag pattern 保护 shared middleware。
    • Batch Plugin 的
      exclude
      变成了
      filter
      ,且含义相反。
      通常应删除
      exclude
      ;如果仍需跳过,则对 predicate 取反。
    • RPCHandler
      默认拒绝 GET
      (
      allowMethods
      默认值为 POST/PUT/PATCH/DELETE)。最简单的修复方法是停止由 link 发送 GET;仅在有 CSRF protection 的情况下明确允许 GET。
    • Handler 的
      filter
      现在接收位置参数;
      v1 的解构形式仍可通过 typecheck,但会读取到错误的值。
    • .input
      /
      .output
      现在可以叠加:
      重复调用会添加一个 schema,而不是替换之前的 schema。
  4. 最后清理 deprecated aliases:
    eventIterator
    改为
    asyncIteratorObject
    ,handler plugins 增加了
    HandlerPlugin
    后缀,link plugins 增加了
    LinkPlugin
    后缀,
    ContractRouter*
    types 改为
    RouterContract*
    。from-v1 guide 最后附有完整的 alias 对照表。
验证:在步骤 1、2 和 4 后运行 typecheck 和 unit tests;步骤 3 需要 integration 或 e2e tests,因为这些变更不会在编译期暴露。完成前,grep 旧包名以及仍存在的 deprecation 删除线提示。

Docs retrieval

文档检索

Fetch pages instead of recalling them, and if this skill and a fetched page disagree, trust the page. The v2 docs live at https://orpc.dev and the v1 docs at https://v1.orpc.dev; slugs look alike across both hosts, so check which host a page came from before copying anything from it. The index of every v2 docs page is at https://orpc.dev/llms.txt, https://orpc.dev/llms-full.txt bundles the entire docs in one large file, and appending
.md
to any page URL returns its exact source markdown.
Authoritative pages to consult during the migration (this skill deliberately omits their full mapping tables):
应获取页面而不是凭记忆回答;如果此 skill 与获取到的页面有冲突,以页面为准。v2 文档位于 https://orpc.dev,v1 文档位于 https://v1.orpc.dev;两个 host 上的 slugs 看起来相似,因此复制内容前应确认页面来自哪个 host。每个 v2 文档页面的索引位于 https://orpc.dev/llms.txt,https://orpc.dev/llms-full.txt 将所有文档打包到一个大型文件中,而给任意页面 URL 追加
.md
即可返回其精确的源 Markdown。
迁移期间应查阅的权威页面(此 skill 有意省略其完整映射表):