orpc-contract
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseoRPC Contract-First
oRPC 契约优先开发
Contract-first oRPC splits an API into two artifacts: a contract (schemas, errors, metadata, no business logic) defined with from , and an implementation built from it with from . Server and client both depend on the contract, never on each other, so the API shape can live in its own package, be reviewed on its own, and ship to consumers as a typed SDK. Prefer it when server and client are separate packages or teams, when the shape starts from an existing OpenAPI spec, or when publishing a client to npm. Stay with the -first flow when one codebase holds both sides and the client can import the router type directly; converting later is cheap because a plain router already works as a router contract (resolve lazy routers with first).
oc@orpc/contractimplement@orpc/serverosunlazyRouterThis skill targets oRPC v2. The skill carries the v2 install and version check plus core builder, middleware, serving, and client concepts. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its docs page first (see Full documentation).
orpc契约优先的 oRPC 将 API 拆分为两个构件:一个是使用 中的 定义的契约(包含模式、错误信息、元数据,无业务逻辑),另一个是通过 中的 基于契约构建的实现。服务端和客户端均依赖契约,而非彼此依赖,因此 API 结构可以独立存放在单独的包中,单独进行审核,并作为类型化 SDK 交付给使用者。当服务端与客户端属于不同包或团队、API 结构基于现有 OpenAPI 规范构建,或是需要将客户端发布至 npm 时,推荐使用此方式。若服务端与客户端在同一代码库中,且客户端可直接导入路由类型,则可采用 优先流程;后续转换成本很低,因为普通路由本身即可作为路由契约(转换前需先用 解析延迟加载的路由)。
@orpc/contractoc@orpc/serverimplementosunlazyRouter本技能针对 oRPC v2 版本。 技能包含 v2 的安装、版本检查,以及核心构建、中间件、服务部署和客户端相关概念。预训练的 oRPC 知识基于 v1 版本,对 v2 往往不适用:若对以下任何 API 存在疑问,请优先查阅其文档页面(参见完整文档)。
orpcDefine the contract with oc
使用 oc 定义契约
Every chain is optional and each call returns a new instance, so share base contracts freely. A contract has no . Zod, Valibot, ArkType, and any other Standard Schema library work.
.handlerts
import { oc } from '@orpc/contract'
import * as z from 'zod'
export const contract = {
planet: {
list: oc
.output(z.array(z.object({ id: z.number(), name: z.string() }))),
find: oc
.errors({ NOT_FOUND: {} })
.input(z.object({ id: z.number() }))
.output(z.object({ id: z.number(), name: z.string() })),
},
}- Always define : without it clients infer the output as
.output.unknown - A router contract is a plain object mapping keys to procedure contracts or nested objects. Avoid the keys ,
then,bind,valueOf,toString.toJSON - Attach shared metadata to a whole subtree with .
oc.meta(someMeta).router({...}) - declares typesafe errors; implementations throw them via
.errors({ NOT_FOUND: {} })and clients infer their shapes.errors.NOT_FOUND() - Repeated /
.inputcalls stack schemas instead of replacing them: object input schemas compose into one flat value, output schemas pipe. Use this to extend a base contract without repeating fields..output - Schema-library-free contracts use the utility from
type:@orpc/contract, optionally with a mapping function asoc.input(type<{ value: number }>()).type<Input, Output>(fn) - REST routes attach via from
.meta(openapi({ method: 'GET', path: '/planets/{id}' })), exactly as on@orpc/openapi; routing rules,os, and spec generation belong to theprefixskill.orpc-openapi
Infer types with , , and from .
InferRouterContractInputsInferRouterContractOutputsInferRouterContractErrors@orpc/contract每个链式调用都是可选的,且每次调用都会返回新实例,因此可以自由共享基础契约。契约不包含 。Zod、Valibot、ArkType 及其他任何标准模式库均适用。
.handlerts
import { oc } from '@orpc/contract'
import * as z from 'zod'
export const contract = {
planet: {
list: oc
.output(z.array(z.object({ id: z.number(), name: z.string() }))),
find: oc
.errors({ NOT_FOUND: {} })
.input(z.object({ id: z.number() }))
.output(z.object({ id: z.number(), name: z.string() })),
},
}- 务必定义 :否则客户端会将输出推断为
.output。unknown - 路由契约是一个普通对象,键对应过程契约或嵌套对象。避免使用 、
then、bind、valueOf、toString作为键名。toJSON - 使用 为整个子树附加共享元数据。
oc.meta(someMeta).router({...}) - 声明类型安全的错误;实现时可通过
.errors({ NOT_FOUND: {} })抛出错误,客户端会自动推断错误结构。errors.NOT_FOUND() - 重复调用 /
.input会堆叠模式而非替换:对象输入模式会组合成一个扁平值,输出模式会按顺序执行。可利用此特性扩展基础契约,无需重复字段。.output - 无需模式库的契约可使用 中的
@orpc/contract工具:type,也可选择传入映射函数oc.input(type<{ value: number }>())。type<Input, Output>(fn) - REST 路由可通过 中的
@orpc/openapi附加,与.meta(openapi({ method: 'GET', path: '/planets/{id}' }))中的用法完全一致;路由规则、os和规范生成属于prefix技能的范畴。orpc-openapi
可使用 中的 、 和 推断类型。
@orpc/contractInferRouterContractInputsInferRouterContractOutputsInferRouterContractErrorsImplement with implement
使用 implement 实现契约
implement.routerts
import { implement } from '@orpc/server'
const implementer = implement(contract).$context<{ db: DB }>()
const listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())
const findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {
const planet = await context.db.find(input.id)
if (!planet)
throw errors.NOT_FOUND()
return planet
})
export const router = implementer.router({
planet: { list: listPlanets, find: findPlanet },
})- declares the initial context the procedures require, as on
.$context.os - Apply middleware per procedure with before
.use(mw). That runs after input validation (the contract already registered.handler); to wrap validation, apply it at router level:.inputfor every procedure, orimplementer.use(mw)for a subtree. Router-level plus procedure-levelimplementer.planet.use(mw).listcan run the same middleware twice; use the dedupe pattern from the.useskill.orpc - creates middleware that infers the contract's typesafe errors. When not every procedure defines a code, guard with the
implementer.middleware(fn)operator:in. Any type-compatible middleware also works.if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS() - The result is a normal router: serve it with (
RPCHandlerskill) ororpc(OpenAPIHandlerskill), call it in-process withorpc-openapiorcallfromcreateRouterClient.@orpc/server
implement.routerts
import { implement } from '@orpc/server'
const implementer = implement(contract).$context<{ db: DB }>()
const listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())
const findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {
const planet = await context.db.find(input.id)
if (!planet)
throw errors.NOT_FOUND()
return planet
})
export const router = implementer.router({
planet: { list: listPlanets, find: findPlanet },
})- 声明过程所需的初始上下文,与
.$context中的用法一致。os - 在 之前使用
.handler可为单个过程应用中间件。该中间件会在输入验证之后运行(契约已注册.use(mw));若要包裹验证逻辑,请在路由级别应用:.input对所有过程生效,implementer.use(mw)对指定子树生效。路由级别和过程级别的implementer.planet.use(mw).list可能会导致同一中间件运行两次;可使用.use技能中的去重模式解决此问题。orpc - 创建的中间件可推断契约的类型安全错误。若并非所有过程都定义了错误码,请使用
implementer.middleware(fn)操作符进行判断:in。任何类型兼容的中间件均适用。if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS() - 最终结果是一个普通路由:可使用 (
RPCHandler技能)或orpc(OpenAPIHandler技能)部署,也可使用orpc-openapi中的@orpc/server或call在进程内调用。createRouterClient
Consume the contract from clients
通过客户端调用契约
RPCLinkOpenAPILinkts
import type { RouterContractClient } from '@orpc/contract'
import type { JsonifiedClient } from '@orpc/openapi'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { OpenAPILink } from '@orpc/openapi/fetch'
// RPC protocol (server side is RPCHandler)
const rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })
const client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)
// OpenAPI protocol (OpenAPIHandler or any spec-compliant server)
const openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })
const apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)- Router-first equivalents use from
RouterClient<typeof router>in the same positions.@orpc/server - is required over
JsonifiedClientbecause OpenAPI serialization is one-way (aOpenAPILinkreturns as a string); dropping it via Smart Coercion, plusDateoptions and CORS caveats, are in theOpenAPILinkskill.orpc-openapi - Per-call client context is the second type parameter, , then
RouterContractClient<typeof contract, ClientContext>; link options likeclient.planet.find(input, { context: { token } })accept functions of that context.headers - Export as a type from the server package so clients never import the contract module itself (still needed as a runtime value for
RouterContractClient<typeof contract>; ship the minified JSON below).OpenAPILink - In very large codebases, skip the root client: pin each procedure with and build per-procedure clients with
.meta(meta.path([...]))fromcreateContractClientFactory(@orpc/contractfromcreateContractJsonifiedClientFactorywhen the link needs@orpc/openapi); fetchJsonifiedClientbefore adopting it.contract/client-factory
RPCLinkOpenAPILinkts
import type { RouterContractClient } from '@orpc/contract'
import type { JsonifiedClient } from '@orpc/openapi'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { OpenAPILink } from '@orpc/openapi/fetch'
// RPC 协议(服务端使用 RPCHandler)
const rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })
const client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)
// OpenAPI 协议(OpenAPIHandler 或任何符合规范的服务端)
const openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })
const apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)- 路由优先的等效用法是在相同位置使用 中的
@orpc/server。RouterClient<typeof router> - 必须使用
OpenAPILink,因为 OpenAPI 序列化是单向的(JsonifiedClient会返回为字符串);可通过智能强制转换省略该类型,相关Date选项和 CORS 注意事项请参考OpenAPILink技能。orpc-openapi - 单次调用的客户端上下文是第二个类型参数:,调用方式为
RouterContractClient<typeof contract, ClientContext>;client.planet.find(input, { context: { token } })等链接选项可接受该上下文的函数。headers - 从服务端包导出 作为类型,这样客户端无需导入契约模块本身(
RouterContractClient<typeof contract>仍需契约作为运行时值;可使用下文的压缩 JSON)。OpenAPILink - 在超大型代码库中,可跳过根客户端:使用 固定每个过程,并通过
.meta(meta.path([...]))中的@orpc/contract创建单个过程的客户端(若链接需要createContractClientFactory,则使用JsonifiedClient中的@orpc/openapi);使用前请查阅createContractJsonifiedClientFactory文档。contract/client-factory
Ship the contract
交付契约
When the contract is derived from a router, importing it on the client is heavy and may expose internals. Minify and export JSON instead:
ts
import fs from 'node:fs'
import { minifyRouterContract } from '@orpc/contract'
import { unlazyRouter } from '@orpc/server'
const minified = minifyRouterContract(await unlazyRouter(router))
fs.writeFileSync('./contract.json', JSON.stringify(minified))minifyRouterContractnew OpenAPILink(contract as typeof router, ...)To publish a typed SDK to npm, export a factory that pairs the contract with a link:
ts
import type { RouterContractClient } from '@orpc/contract'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
export function createMyApi(apiKey: string): RouterContractClient<typeof contract> {
const link = new RPCLink({
origin: 'https://example.com',
url: '/rpc',
headers: { 'x-api-key': apiKey },
})
return createORPCClient(link)
}Bundle with , point at types plus import entries, list and as dependencies, and publish. Consumers get a fully typed client that works with every oRPC client integration (TanStack Query included). Fetch for the complete .
tsdown --dts src/index.tsexportsdist@orpc/client@orpc/contractrecipes/publish-client-to-npmpackage.json当契约由路由派生而来时,客户端导入契约会增加体积,且可能暴露内部实现。建议将契约压缩为 JSON 后导出:
ts
import fs from 'node:fs'
import { minifyRouterContract } from '@orpc/contract'
import { unlazyRouter } from '@orpc/server'
const minified = minifyRouterContract(await unlazyRouter(router))
fs.writeFileSync('./contract.json', JSON.stringify(minified))minifyRouterContractnew OpenAPILink(contract as typeof router, ...)若要将类型化 SDK 发布至 npm,请导出一个将契约与链接配对的工厂函数:
ts
import type { RouterContractClient } from '@orpc/contract'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
export function createMyApi(apiKey: string): RouterContractClient<typeof contract> {
const link = new RPCLink({
origin: 'https://example.com',
url: '/rpc',
headers: { 'x-api-key': apiKey },
})
return createORPCClient(link)
}使用 进行打包,将 指向 目录的类型文件和导入入口,将 和 列为依赖项,然后发布。使用者将获得一个完全类型化的客户端,可与所有 oRPC 客户端集成(包括 TanStack Query)。完整的 配置请查阅 文档。
tsdown --dts src/index.tsexportsdist@orpc/client@orpc/contractpackage.jsonrecipes/publish-client-to-npmGenerate the contract from an existing OpenAPI spec
从现有 OpenAPI 规范生成契约
Use Hey API's plugin instead of hand-writing the contract. Install as a dev dependency (oRPC v2 output requires the tag until the next stable Hey API release) and create :
orpc@hey-api/openapi-ts@nextnextopenapi-ts.config.tsts
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'https://example.com/openapi.json', // local file or URL
output: 'src/contract',
plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],
})Then run . It writes (one procedure contract per operation, routed via with , plus a combined router) and . The generated files import , , and , so install those too. From there, implement the contract on your own server, or point at the existing spec-compliant server.
npx @hey-api/openapi-tsorpc.gen.ts.meta(openapi({...}))inputStructure: 'detailed'contractzod.gen.ts@orpc/contract@orpc/openapizodOpenAPILink无需手动编写契约,可使用 Hey API 的 插件。安装 作为开发依赖(oRPC v2 输出需要 标签,直至 Hey API 发布下一个稳定版本),并创建 :
orpc@hey-api/openapi-ts@nextnextopenapi-ts.config.tsts
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'https://example.com/openapi.json', // 本地文件或 URL
output: 'src/contract',
plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],
})然后运行 。该命令会生成 (每个操作对应一个过程契约,通过 进行路由, 设为 ,并包含一个组合后的 路由)和 。生成的文件会导入 、 和 ,因此需要安装这些依赖。之后,你可以在自己的服务端实现该契约,或是将 指向现有的符合规范的服务端。
npx @hey-api/openapi-tsorpc.gen.ts.meta(openapi({...}))inputStructure'detailed'contractzod.gen.ts@orpc/contract@orpc/openapizodOpenAPILinkFull documentation
完整文档
If this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):
- https://orpc.dev/llms.txt : index of every page with descriptions
- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)
- Append to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/contract/procedure.md)
.md
Pages to fetch when you need details beyond this skill:
- Contract: ,
contract/procedure,contract/router,contract/implementationcontract/generate-from-openapi - Clients: ,
client/client-side,client/server-side,client/error-handlingopenapi/link - Workflows: ,
recipes/publish-client-to-npm,contract/client-factoryrecipes/monorepo-setup
若本技能与查阅的文档页面存在冲突,请以文档页面为准:本技能仅为摘要,且 v2 版本仍在迭代中。文档地址为 https://orpc.dev(v1 版本文档保留在 https://v1.orpc.dev):
- https://orpc.dev/llms.txt : 所有页面的索引及描述
- https://orpc.dev/llms-full.txt : 完整文档的单文件版本(体积较大;建议查阅单个页面)
- 在任何文档 URL 后添加 即可获取该页面的原始 markdown 源码(例如 https://orpc.dev/docs/contract/procedure.md)
.md
若需要本技能未涵盖的细节,请查阅以下页面:
- 契约相关:、
contract/procedure、contract/router、contract/implementationcontract/generate-from-openapi - 客户端相关:、
client/client-side、client/server-side、client/error-handlingopenapi/link - 工作流相关:、
recipes/publish-client-to-npm、contract/client-factoryrecipes/monorepo-setup