orpc

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

oRPC

oRPC

oRPC is a typesafe API framework: write plain TypeScript functions on the server, call them from clients like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step. The same router can also be served as a REST API with an OpenAPI spec.
This skill targets oRPC v2. Check what is installed before writing code:
npm ls @orpc/server
(or any
@orpc/*
package). A 1.x version means v1, where this skill's guidance does not apply; use the
orpc-migrate
skill to upgrade. v2 currently ships under the
beta
dist-tag (
npm install @orpc/server@beta @orpc/client@beta
; a plain install silently gets v1). If
npm view @orpc/server dist-tags
shows
latest
at 2.x, the beta has ended: install normally.
Pretrained oRPC knowledge describes v1 and is often wrong for v2 (routing moved to
.meta(openapi(...))
,
RPCLink
split
url
into
origin
plus a path, automatic middleware dedupe was removed). Prefer retrieval: the index of every docs page is at https://orpc.dev/llms.txt; see Full documentation for the mechanics.
Package map:
  • @orpc/server
    : the
    os
    builder, routers, middleware,
    RPCHandler
    , server-side clients (
    call
    ,
    createRouterClient
    ),
    implement
    for mocks
  • @orpc/client
    :
    createORPCClient
    ,
    RPCLink
    ,
    safe
    ,
    createSafeClient
    ,
    isInferableError
  • @orpc/contract
    : contract-first API definitions implemented separately from their logic (see the
    orpc-contract
    skill)
  • @orpc/openapi
    :
    OpenAPIHandler
    ,
    OpenAPILink
    , OpenAPI 3.1 spec generation
  • Integration packages such as
    @orpc/tanstack-query
    and
    @orpc/nest
oRPC是一个类型安全的API框架:在服务端编写普通的TypeScript函数,即可像调用本地函数一样从客户端调用它们。输入会在运行时验证,类型信息贯穿整个流程,且无需代码生成步骤。同一个路由还可以作为带有OpenAPI规范的REST API部署。
本技能针对oRPC v2版本。编写代码前请检查已安装的版本:
npm ls @orpc/server
(或任意
@orpc/*
包)。若为1.x版本则是v1,本技能的指导内容不适用;请使用
orpc-migrate
技能进行升级。目前v2版本在
beta
分发标签下发布(执行
npm install @orpc/server@beta @orpc/client@beta
安装;直接执行安装命令会默认获取v1版本)。若
npm view @orpc/server dist-tags
显示
latest
标签对应2.x版本,则测试版已结束,可正常安装。
预训练的oRPC知识针对v1版本,通常不适用于v2(路由功能移至
.meta(openapi(...))
RPCLink
url
拆分为
origin
加路径,自动中间件去重功能已移除)。优先从文档获取信息:所有文档页面的索引位于https://orpc.dev/llms.txt;具体机制请查看[完整文档](#完整文档)。
包映射:
  • @orpc/server
    :包含
    os
    构建器、路由、中间件、
    RPCHandler
    、服务端客户端(
    call
    createRouterClient
    )、用于模拟的
    implement
  • @orpc/client
    :包含
    createORPCClient
    RPCLink
    safe
    createSafeClient
    isInferableError
  • @orpc/contract
    :契约优先的API定义,与业务逻辑分离实现(详情请查看
    orpc-contract
    技能)
  • @orpc/openapi
    :包含
    OpenAPIHandler
    OpenAPILink
    、OpenAPI 3.1规范生成功能
  • 集成包如
    @orpc/tanstack-query
    @orpc/nest

Define procedures

定义过程

Build a procedure with the
os
builder: describe input with a schema, implement with
.handler
. Zod, Valibot, ArkType, and any other Standard Schema library work for
.input
,
.output
, and error
data
.
ts
import { os } from '@orpc/server'
import * as z from 'zod'

export const listPlanets = os
  .handler(async () => [{ id: 1, name: 'Earth' }]) // no .input: takes no arguments

export const findPlanet = os
  .input(z.object({ id: z.number() }))
  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))
.handler
is the only required step. The full chain, every other step optional:
ts
const example = os
  .$context<{ headers: Headers }>() // initial context this procedure requires
  .errors({ NOT_FOUND: {} }) // typed errors
  .use(requireAuth) // middleware
  .input(z.object({ id: z.number() }))
  .output(z.object({ id: z.number(), name: z.string() })) // optional; also speeds up type checking
  .handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))
Every builder step returns a new instance, so share base builders freely:
const authed = os.use(requireAuth)
then build many procedures from
authed
.
使用
os
构建器创建过程:通过Schema描述输入,通过
.handler
实现逻辑。Zod、Valibot、ArkType以及其他任意Standard Schema库均可用于
.input
.output
和错误
data
的验证。
ts
import { os } from '@orpc/server'
import * as z from 'zod'

export const listPlanets = os
  .handler(async () => [{ id: 1, name: 'Earth' }]) // 无.input:不接收参数

export const findPlanet = os
  .input(z.object({ id: z.number() }))
  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))
.handler
是唯一必需的步骤。完整的构建链中,其他步骤均为可选:
ts
const example = os
  .$context<{ headers: Headers }>() // 声明该过程所需的初始上下文
  .errors({ NOT_FOUND: {} }) // 声明类型化错误
  .use(requireAuth) // 使用中间件
  .input(z.object({ id: z.number() }))
  .output(z.object({ id: z.number(), name: z.string() })) // 可选;同时可加快类型检查速度
  .handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))
每个构建步骤都会返回新的实例,因此可以自由共享基础构建器:
const authed = os.use(requireAuth)
,然后基于
authed
创建多个过程。

Assemble a router

组装路由

A router is a plain object mapping keys to procedures (or nested routers). Do not use the keys
then
,
bind
,
valueOf
,
toString
,
toJSON
.
ts
export const router = {
  planet: { list: listPlanets, find: findPlanet },
  admin: os.use(requireAuth).router({ deletePlanet }), // apply shared middleware to a subtree
  planetLazy: os.lazy(() => import('./planet')), // code-split; module's default export is a router
}
Infer types with
InferRouterInputs
/
InferRouterOutputs
from
@orpc/server
. Applying
.use
at both router and procedure level can run the same middleware twice; see the dedupe pattern below.
路由是一个普通对象,将键映射到过程(或嵌套路由)。请勿使用
then
bind
valueOf
toString
toJSON
作为键名。
ts
export const router = {
  planet: { list: listPlanets, find: findPlanet },
  admin: os.use(requireAuth).router({ deletePlanet }), // 为子路由应用共享中间件
  planetLazy: os.lazy(() => import('./planet')), // 代码拆分;模块的默认导出为路由
}
使用
@orpc/server
中的
InferRouterInputs
/
InferRouterOutputs
推断类型。同时在路由和过程级别调用
.use
会导致同一中间件执行两次;请查看下方的去重模式。

Middleware and context

中间件与上下文

Context comes from two places. Initial context is declared with
.$context
and passed explicitly when serving or calling (environment values: headers, env, db). Injected context is added at runtime by middleware via
next({ context })
(runtime values: the authenticated user).
ts
import { ORPCError, os } from '@orpc/server'

const base = os.$context<{ headers: Headers }>()

const requireAuth = base.middleware(async ({ context, next }) => {
  const user = await parseUser(context.headers)
  if (!user) {
    throw new ORPCError('UNAUTHORIZED')
  }
  return next({ context: { user } }) // handler now sees context.user, typed non-null
})
.use
accepts named middleware or inline functions. Middleware registered before
.input
runs before validation, the rest after. Middleware can also declare typed input (
os.middleware(async ({ next }, id: number) => ...)
); adapt mismatched shapes with
.use(mw.adaptInput(input => input.id))
.
Best practice, dedupe expensive middleware: the same middleware can run twice in one call (router-level plus procedure-level
.use
, or a procedure
call
ing another). Cache the result in context:
ts
const authProvider = os
  .$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()
  .middleware(async ({ context, next }) => {
    const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)
    return next({ context: { auth, authLoaded: true } })
  })
上下文来自两个来源。初始上下文通过
.$context
声明,并在部署或调用时显式传入(环境值:请求头、环境变量、数据库)。注入上下文由中间件通过
next({ context })
在运行时添加(运行时值:已认证用户)。
ts
import { ORPCError, os } from '@orpc/server'

const base = os.$context<{ headers: Headers }>()

const requireAuth = base.middleware(async ({ context, next }) => {
  const user = await parseUser(context.headers)
  if (!user) {
    throw new ORPCError('UNAUTHORIZED')
  }
  return next({ context: { user } }) // 处理器现在可以看到类型非空的context.user
})
.use
接受命名中间件或内联函数。在
.input
之前注册的中间件会在验证前运行,其余则在验证后运行。中间件也可以声明类型化输入(
os.middleware(async ({ next }, id: number) => ...)
);可通过
.use(mw.adaptInput(input => input.id))
适配不匹配的形状。
最佳实践:对开销较大的中间件进行去重:同一中间件可能在一次调用中执行两次(路由级别加过程级别
.use
,或一个过程调用另一个过程)。将结果缓存在上下文中:
ts
const authProvider = os
  .$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()
  .middleware(async ({ context, next }) => {
    const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)
    return next({ context: { auth, authLoaded: true } })
  })

Typesafe errors

类型安全错误

Throw
ORPCError
(a
code
plus optional
message
and
data
). Both
message
and
data
are sent to the client, so never put secrets in them. Throw only
Error
instances, never literals.
Define errors with
.errors
so clients can infer each error's shape:
ts
const find = os
  .errors({
    NOT_FOUND: { message: 'Planet not found' }, // default message
    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },
  })
  .handler(async ({ input, errors }) => {
    throw errors.NOT_FOUND()
  })
throw new ORPCError('NOT_FOUND')
inside that handler is converted to the matching typed error when code and data match. Convert custom error classes to
ORPCError
in a middleware
try/catch
.
抛出
ORPCError
(包含
code
以及可选的
message
data
)。
message
data
都会发送给客户端,因此切勿在其中包含敏感信息。仅抛出
Error
实例,切勿抛出字面量。
通过
.errors
定义错误,以便客户端推断每个错误的形状:
ts
const find = os
  .errors({
    NOT_FOUND: { message: 'Planet not found' }, // 默认消息
    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },
  })
  .handler(async ({ input, errors }) => {
    throw errors.NOT_FOUND()
  })
在该处理器中抛出
throw new ORPCError('NOT_FOUND')
会在代码和数据匹配时转换为对应的类型化错误。在中间件的
try/catch
中将自定义错误类转换为
ORPCError

Serve with RPCHandler

使用RPCHandler部署服务

RPCHandler
matches requests to procedures, validates input, runs handlers, and encodes results. Pick the adapter for your runtime. Fetch API (Bun, Deno, Cloudflare Workers):
ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { CORSHandlerPlugin } from '@orpc/server/plugins'

const handler = new RPCHandler(router, {
  plugins: [new CORSHandlerPlugin()],
  interceptors: [onError(error => console.error(error))],
})

export async function fetch(request: Request): Promise<Response> {
  const { matched, response } = await handler.handle(request, {
    prefix: '/rpc',
    context: { headers: request.headers }, // provide the router's initial context here
  })
  return matched ? response : new Response('Not found', { status: 404 })
}
// Bun.serve({ fetch }) / Deno.serve(fetch) / export default { fetch } on Workers
Node HTTP:
ts
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'

const handler = new RPCHandler(router)

const server = createServer(async (req, res) => {
  const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })
  if (matched)
    return
  res.statusCode = 404
  res.end('Not found')
})

server.listen(3000)
Unmatched requests fall through to your own handling. By default
RPCHandler
accepts only
POST
,
PUT
,
PATCH
, and
DELETE
; enabling
GET
via
allowMethods
is a CSRF risk with cookie auth, see RPC Handler. Handler options also include
interceptors
,
routingInterceptors
,
clientInterceptors
,
plugins
,
filter
, and
errorStatusMap
. Adapters also exist for AWS Lambda, Fastify, WebSocket, Message Port, and Expo.
RPCHandler
将请求匹配到对应过程、验证输入、运行处理器并编码结果。根据你的运行时选择对应的适配器。Fetch API(Bun、Deno、Cloudflare Workers):
ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { CORSHandlerPlugin } from '@orpc/server/plugins'

const handler = new RPCHandler(router, {
  plugins: [new CORSHandlerPlugin()],
  interceptors: [onError(error => console.error(error))],
})

export async function fetch(request: Request): Promise<Response> {
  const { matched, response } = await handler.handle(request, {
    prefix: '/rpc',
    context: { headers: request.headers }, // 在此提供路由的初始上下文
  })
  return matched ? response : new Response('Not found', { status: 404 })
}
// 在Bun中执行Bun.serve({ fetch }) / Deno中执行Deno.serve(fetch) / 在Workers中导出default { fetch }
Node HTTP:
ts
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'

const handler = new RPCHandler(router)

const server = createServer(async (req, res) => {
  const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })
  if (matched)
    return
  res.statusCode = 404
  res.end('Not found')
})

server.listen(3000)
未匹配的请求将交由你自行处理。默认情况下,
RPCHandler
仅接受
POST
PUT
PATCH
DELETE
请求;通过
allowMethods
启用
GET
请求在使用Cookie认证时存在CSRF风险,详情请查看RPC Handler。处理器选项还包括
interceptors
routingInterceptors
clientInterceptors
plugins
filter
errorStatusMap
。还提供了适配AWS LambdaFastifyWebSocketMessage PortExpo的适配器。

Call procedures

调用过程

Server side (same process, no HTTP; also the fastest way to test procedures):
ts
import { call, createRouterClient } from '@orpc/server'

const planet = await call(findPlanet, { id: 1 }, { context: { headers } })

const client = createRouterClient(router, { context: { headers } }) // context can be a function
const planets = await client.planet.list()
Client side,
RPCLink
turns calls into HTTP requests. Import the router as a type only so no server code reaches the client bundle:
ts
import type { RouterClient } from '@orpc/server'
import type { router } from '../server/router'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  origin: 'http://127.0.0.1:3000',
  url: '/rpc', // must match the server's prefix
  headers: () => ({ authorization: `Bearer ${token}` }), // options accept functions
})

export const orpc: RouterClient<typeof router> = createORPCClient(link)

const planet = await orpc.planet.find({ id: 1 })
Client error handling: plain
try/catch
works, but
safe
preserves typed error inference:
ts
import { createSafeClient, isInferableError, safe } from '@orpc/client'

const [error, data] = await safe(orpc.planet.find({ id: 1 }))
if (isInferableError(error)) {
  console.log(error.code, error.data) // typed from the procedure's .errors
}
else if (error) {
  // unknown error
}

const safeClient = createSafeClient(orpc) // every call returns [error, data]
服务端(同一进程,无需HTTP;也是测试过程的最快方式):
ts
import { call, createRouterClient } from '@orpc/server'

const planet = await call(findPlanet, { id: 1 }, { context: { headers } })

const client = createRouterClient(router, { context: { headers } }) // 上下文可以是函数
const planets = await client.planet.list()
客户端:
RPCLink
将调用转换为HTTP请求。仅将路由作为类型导入,避免服务端代码进入客户端包:
ts
import type { RouterClient } from '@orpc/server'
import type { router } from '../server/router'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  origin: 'http://127.0.0.1:3000',
  url: '/rpc', // 必须与服务端的prefix匹配
  headers: () => ({ authorization: `Bearer ${token}` }), // 选项接受函数
})

export const orpc: RouterClient<typeof router> = createORPCClient(link)

const planet = await orpc.planet.find({ id: 1 })
客户端错误处理:普通的
try/catch
即可生效,但
safe
方法可保留类型化错误推断:
ts
import { createSafeClient, isInferableError, safe } from '@orpc/client'

const [error, data] = await safe(orpc.planet.find({ id: 1 }))
if (isInferableError(error)) {
  console.log(error.code, error.data) // 类型来自过程的.errors定义
}
else if (error) {
  // 未知错误
}

const safeClient = createSafeClient(orpc) // 每次调用都会返回[error, data]

Beyond the basics

进阶内容

  • Streaming / SSE: return an async generator from
    .handler
    , validate events with
    asyncIteratorObject
    , resume via
    lastEventId
    : AsyncIteratorObject
  • OpenAPI: serve the same router as REST with routing metadata plus
    OpenAPIHandler
    , and generate a spec (covered in depth by the
    orpc-openapi
    skill): OpenAPI Handler
  • Contract-first: define contracts with
    @orpc/contract
    , implement with
    implement
    (covered in depth by the
    orpc-contract
    skill): Contracts
  • Plugins for handler and link: batch, CORS, dedupe, retry, compression, request limits, smart coercion, static files, timeout, tmp file upload, and more. Fetch a plugin's docs page before configuring it; option names are not guessable: Plugins
  • Integrations: TanStack Query (
    createTanstackQueryUtils
    ), SWR, Pinia Colada, Next.js, NestJS, AI SDK, OpenTelemetry
  • Testing:
    call
    procedures directly; mock with
    implement(router.planet.list).handler(() => [])
    , and run the project's typecheck before declaring success, since end-to-end types are oRPC's first correctness signal: Testing and Mocking
  • Monorepos: TypeScript project references keep client types resolvable: Monorepo Setup
  • Migrating from tRPC or oRPC v1: use the
    orpc-migrate
    skill
  • 流式传输/SSE:从
    .handler
    返回异步生成器,使用
    asyncIteratorObject
    验证事件,通过
    lastEventId
    恢复:AsyncIteratorObject
  • OpenAPI:结合路由元数据与
    OpenAPIHandler
    ,将同一路由作为REST API部署并生成规范(详情请查看
    orpc-openapi
    技能):OpenAPI Handler
  • 契约优先:使用
    @orpc/contract
    定义契约,通过
    implement
    实现(详情请查看
    orpc-contract
    技能):Contracts
  • 处理器与链接插件:批量处理、CORS、去重、重试、压缩、请求限制、智能类型转换、静态文件、超时、临时文件上传等。配置插件前请查看其文档页面;选项名称无法通过猜测获得:Plugins
  • 集成:TanStack Query
    createTanstackQueryUtils
    )、SWRPinia ColadaNext.jsNestJSAI SDKOpenTelemetry
  • 测试:直接使用
    call
    调用过程;使用
    implement(router.planet.list).handler(() => [])
    进行模拟,并在完成前运行项目的类型检查,因为端到端类型是oRPC的首要正确性信号:Testing and Mocking
  • 单仓库:TypeScript项目引用可确保客户端类型可解析:Monorepo Setup
  • 从tRPC或oRPC v1迁移:使用
    orpc-migrate
    技能

Full documentation

完整文档

This skill is an overview; fetch exact docs instead of guessing APIs, and if this skill and a fetched page disagree, trust the page. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):
Doc map, all under
https://orpc.dev/docs/
:
  • Top level:
    procedure
    ,
    router
    ,
    middleware
    ,
    context
    ,
    error-handling
    ,
    metadata
    , plus
    binary-data
    (file uploads) and
    async-iterator-object
    (streaming/SSE)
  • rpc/*
    ,
    openapi/*
    : protocol details, handlers, links;
    contract/*
    : contract-first (the
    orpc-contract
    skill)
  • client/*
    : server- and client-side clients, error handling,
    DynamicLink
  • adapters/*
    : per-runtime serving quirks (fetch-api, node-http, aws-lambda, fastify, websocket, message-port, expo)
  • plugins/*
    : twenty handler/link plugins;
    helpers/*
    : cookie, encryption, form-data, publisher, ratelimit, signing, base64url
  • integrations/*
    : framework glue;
    recipes/*
    : guidance (testing, SSR, monorepos, validation)
  • migrations/from-v1
    ,
    migrations/from-trpc
    : upgrades (use the
    orpc-migrate
    skill)
本技能仅为概述;请从文档获取准确的API信息,若本技能与文档内容不一致,请以文档为准。文档托管于https://orpc.dev(v1文档保留在https://v1.orpc.dev):
文档映射,均位于
https://orpc.dev/docs/
路径下:
  • 顶层:
    procedure
    router
    middleware
    context
    error-handling
    metadata
    ,以及
    binary-data
    (文件上传)和
    async-iterator-object
    (流式传输/SSE)
  • rpc/*
    openapi/*
    :协议细节、处理器、链接;
    contract/*
    :契约优先(
    orpc-contract
    技能)
  • client/*
    :服务端与客户端、错误处理、
    DynamicLink
  • adapters/*
    :各运行时的部署细节(fetch-api、node-http、aws-lambda、fastify、websocket、message-port、expo)
  • plugins/*
    :20个处理器/链接插件;
    helpers/*
    :Cookie、加密、表单数据、发布器、限流、签名、base64url
  • integrations/*
    :框架集成;
    recipes/*
    :实践指南(测试、SSR、单仓库、验证)
  • migrations/from-v1
    migrations/from-trpc
    :升级指南(使用
    orpc-migrate
    技能)