- 后端
- RPC框架
- API设计
【免费下载链接】orpc
Typesafe APIs Made Simple 🪄
本指南以 oRPC 官方技能文档 skills/orpc-openapi/SKILL.md 为骨架,系统讲解如何把 oRPC Router 同时暴露为 RPC 协议与符合规范的 OpenAPI HTTP 接口:从openapi()元数据与.route扩展定义路由,到用OpenAPIHandler提供服务、用OpenAPILink实现端到端类型安全调用,再到用OpenAPIGenerator生成 OpenAPI 3.2/3.1/3.0 规范文档并托管 Scalar/Swagger 交互文档。读完你可以独立配置一套"双协议、单 Router"的 Typesafe API,并理解各配置项在 packages/openapi/src/meta.ts 等源码中的底层合并与默认值规则。
概览:一个 Router,两种协议
oRPC 的核心设计是让同一套 procedure 同时"说"两种语言:
- RPC 协议:由
RPCHandler/RPCLink提供服务与调用,适合内部端到端类型安全场景; - OpenAPI HTTP 协议(REST 风格):由
OpenAPIHandler/OpenAPILink提供服务与调用,兼容任何 OpenAPI 生态工具链。
两者接受同一个 Router 对象,因此无需为 REST 单独维护一份定义。@orpc/openapi包(源码见 packages/openapi)承担了 OpenAPI 侧的全部能力:路由元数据、输入输出编解码、规范文档生成与交互文档托管。
本文面向oRPC v2。如果你需要契约优先(contract-first)工作流,请参见
orpc-contract技能;核心 builder、middleware、context 与客户端基础概念,请参见orpc技能。
路由(Routing):从默认 POST 到完全自定义
默认路由规则
一个 procedure 默认使用POST方法,路径由 Router 结构推导:planet.create会暴露为POST /planet/create。默认值定义在 packages/openapi/src/constants.ts:
export const DEFAULT_OPENAPI_METHOD = 'POST' export const DEFAULT_OPENAPI_SUCCESS_DESCRIPTION = 'OK' export const DEFAULT_OPENAPI_INPUT_STRUCTURE = 'compact' export const DEFAULT_OPENAPI_OUTPUT_STRUCTURE = 'compact'通过 openapi() 元数据覆盖
在 server(os)与 contract(oc)builder 上均可使用openapi元数据覆盖方法、路径与成功状态码:
import { openapi } from '@orpc/openapi' import { os } from '@orpc/server' import { z } from 'zod' const getPlanet = os .meta(openapi({ method: 'GET', path: '/planets/{id}', successStatus: 200 })) .input(z.object({ id: z.string() })) .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))其中openapi的完整可配置字段(见 packages/openapi/src/meta.ts)包括:
| 字段 | 默认值 | 说明 |
|---|---|---|
method | 'POST' | 支持的 HTTP 方法:HEAD、GET、POST、PUT、DELETE、PATCH、QUERY(QUERY需要 OpenAPI 3.2,见下文生成器小节) |
path | Router 段以/连接 | 支持{param}动态参数语法 |
operationId | Router 段以.连接 | 规范文档中的唯一操作标识 |
summary/description | 无 | 生成 spec 中的操作摘要与描述 |
deprecated | false | 在生成 spec 中标记为已废弃 |
tags | 无 | 操作标签 |
successStatus | 200 | 成功响应状态码,须小于 400 |
successDescription | 'OK' | 成功响应描述 |
paramsStyles/queryStyles | 见下文 | 路径/查询参数解码策略 |
requestBodyHint/responseBodyHint | 根据头部推断 | 请求/响应体解析提示 |
inputStructure | 'compact' | 输入结构:compact或detailed |
outputStructure | 'compact' | 输出结构:compact或detailed |
spec | 无 | 覆盖或扩展生成的 Operation 对象 |
prefix | 无 | 路径前缀 |
路径参数
- 在
path中写{id},并在 input schema 中定义同名必填字段; - 使用
{+path}表示 catch-all 段,匹配值可以包含/(例如/files/{+path})。
prefix:为 procedure 或整个 Router 加前缀
os.meta(openapi({ prefix: '/api/v2' })).router({...})关键实践:lazy Router 上务必设置prefix,这样只有匹配前缀的请求才会触发懒加载,避免无关请求加载子 Router。从源码看,prefix的合并使用mergeHttpPath按定义顺序拼接(packages/openapi/src/meta.ts)。
多次 .meta(openapi(...)) 的合并规则
openapi元数据是可重复叠加的,packages/openapi/src/meta.ts 中的init实现体现了精确的合并语义:
prefix与tags:按定义顺序拼接(tags数组连接、prefix路径拼接);method/path/successStatus:后定义者覆盖前者(last-wins);queryStyles/paramsStyles:按参数键逐项合并,同一参数以最近定义为准;spec:两个函数会链式组合(后者接收前者的结果);函数与对象混用时函数作用于对象;两个对象时后者获胜;- 显式设为
undefined:重置为默认值,而不是继续合并。
使用 .route 扩展直接定义路由
如果不希望每次调用.meta(openapi(...)),可以启用.route扩展。它需要一个在启动时必定执行的模块(基础 builder 文件或服务入口)中的副作用导入:
import '@orpc/openapi/extensions/route' const ping = os.route({ method: 'GET', path: '/ping' }).handler(async () => 'pong')从源码看,route本质上是meta(openapi(meta))的语法糖:它在@orpc/server的Builder与@orpc/contract的ContractBuilder上通过模块扩展注入了同名方法(见 packages/openapi/src/extensions/route.ts)。
输入与输出映射(Input & Output Mapping)
compact 模式(默认)
路径参数与查询参数或请求体会合并为扁平对象,具体取决于 HTTP 方法:GET /planets/earth?q=life得到输入{ id: 'earth', q: 'life' }。handler 的返回值直接作为响应体,状态码取successStatus。
从 packages/openapi/src/adapters/standard/openapi-handler-codec.ts 的实现看,compact 模式下:无 body 的方法(如 GET)走 query 解码,有 body 的方法走 body 解码;若既有 params 又有数据,则合并;非对象数据(原始值、数组、Blob、ReadableStream 等)无法与 params 合并,会直接返回。
detailed 模式
设置inputStructure: 'detailed'后,输入变为{ params, query, headers, body }四段,schema 只需定义实际需要的字段:
// GET /users/42?search=hello const inputValue = { params: { id: 1 }, query: { search: 'hello' }, headers: { 'content-type': 'application/json' }, body: 'body value', } const inputSchema = z.object({ params: z.object({ id: z.coerce.number() }), query: z.object({ search: z.string() }), headers: z.object({ 'content-type': z.string() }), body: z.string(), })outputStructure: 'detailed'则允许返回{ status?, headers?, body? },按响应动态设置状态码:
const outputValue = { status: 201, headers: { 'x-custom-header': 'value' }, body: 'body value', } const outputSchema = z.object({ status: z.literal(201).meta({ description: 'Record Created' }), headers: z.object({ 'x-custom-header': z.string() }), body: z.string(), })status未提供时回退到successStatus;建议用字面量类型(如z.literal(201))以便生成的 spec 能反映精确状态码。headers为Record<string, string | string[] | undefined>。
查询字符串与表单数据的 bracket notation 解码
查询字符串和表单数据默认使用bracket notation解码(实现见 packages/openapi/src/bracket-notation.ts)。设计嵌套 query 输入的 schema 前务必阅读仓库文档 apps/content/docs/openapi/bracket-notation.mdx,其能力边界如下:
- 重复键成为数组:
?color=red&color=blue得到['red', 'blue'];color[]=red也会追加; [number]定位数组下标,[key]定位对象属性:?filter[status]=active得到{ filter: { status: 'active' } };- 无法表达:空对象/空数组、根级数组、键全为数字的对象;query/form 值永远以字符串到达(form data 中可能是文件)。
实现层面,BracketNotationSerializer支持maxDeserializingEmptySlots(默认 1000)来限制显式数组下标产生的空槽数量:例如?arr[5000]=x会因超出上限而把数组降级为对象,防止内存被超大稀疏数组占用。
paramsStyles 与 queryStyles:逐参数覆盖解码策略
paramsStyles可选值:
| 策略 | 编码段 | 解码结果 | OpenAPI 参数风格 |
|---|---|---|---|
primitive(默认) | /users/42 | { id: '42' } | simple |
comma-delimited-array | /users/a,b,c | { id: ['a', 'b', 'c'] } | simple |
comma-delimited-object | /users/a,1,b,2 | { id: { a: '1', b: '2' } } | simple |
queryStyles在paramsStyles三种策略之外还支持:
| 策略 | 编码 | 解码结果 |
|---|---|---|
primitive | ?a=1&a=2 | { a: '2' }(取最后一次出现) |
array | ?a=1&a=2 | { a: ['1', '2'] }(单值也产出数组) |
comma-delimited-array | ?a=1,2,3 | { a: ['1', '2', '3'] } |
comma-delimited-object | ?a=A,1,B,2 | { a: { A: '1', B: '2' } } |
space-delimited-array | ?a=1 2 3 | { a: ['1', '2', '3'] } |
pipe-delimited-array | ?a=1\|2\|3 | { a: ['1', '2', '3'] } |
json | ?meta={"key":"value"} | { meta: { key: 'value' } }(解析失败回退原始字符串) |
注意:所有
*-delimited-*策略都不支持键或值包含分隔符本身。例如openapi.path = '/users/{id}/{tags}/{filters}'配合GET /users/42/red,blue/size,large,brand,nike,可用paramsStyles分别指定id: 'primitive'、tags: 'comma-delimited-array'、filters: 'comma-delimited-object'。
requestBodyHint 与 responseBodyHint
当仅凭 HTTP 头无法让解析器确定 body 处理方式时(例如裸ReadableStream上传),使用requestBodyHint/responseBodyHint给出提示,取值包括'json'、'form-data'、'event-stream'、'octet-stream'、'file'等。注意:standard-server 的Content-Type头优先级高于此提示;form-data与url-search-params同样使用 bracket notation 解码,结果会是对象或数组。
服务端:OpenAPIHandler
基本用法
OpenAPIHandler从适配器子路径导入(@orpc/openapi/fetch、@orpc/openapi/node等,仓库适配器目录见 packages/openapi/src/adapters):
import { SmartCoercionHandlerPlugin } from '@orpc/json-schema' import { OpenAPIHandler } from '@orpc/openapi/fetch' import { ZodToJsonSchemaConverter } from '@orpc/zod' const handler = new OpenAPIHandler(router, { plugins: [ new SmartCoercionHandlerPlugin({ converters: [new ZodToJsonSchemaConverter()] }), ], }) export async function fetch(request: Request): Promise<Response> { const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} }) return matched ? response : new Response('Not Found', { status: 404 }) }从源码看,OpenAPIHandler由三部分组装而成:OpenAPIHandlerCodec(负责 OpenAPI 编解码)、StandardHandler(通用标准 handler)与FetchHandler(Fetch API 适配层),见 packages/openapi/src/adapters/fetch/openapi-handler.ts。handle返回的matched标志用于判断是否命中路由,便于与其他 handler 串联。
与 RPCHandler 共存
OpenAPIHandler与RPCHandler接受同一个 Router,因此可分别挂载在不同前缀(如/api与/rpc),逐个尝试并返回第一个matched的响应:
const openapi = new OpenAPIHandler(router, openapiOptions) const rpc = new RPCHandler(router, rpcOptions) export async function fetch(request: Request): Promise<Response> { const api = await openapi.handle(request, { prefix: '/api', context: {} }) if (api.matched) return api.response const rpcRes = await rpc.handle(request, { prefix: '/rpc', context: {} }) return rpcRes.matched ? rpcRes.response : new Response('Not Found', { status: 404 }) }Smart Coercion:字符串到原生类型
query、path、form 的值以字符串到达,当 input schema 期望非字符串类型时,应添加SmartCoercionHandlerPlugin。它只做schema 驱动、无损的转换:
'123'→123'true'/'on'→true- ISO 字符串 →
Date - 数组 →
Set/Map(通过x-native-type)
模糊的值(无法确定如何转换)保持原样,交给 schema 校验。其实现(packages/json-schema/src/smart-coercion-handler-plugin.ts)基于 JSON Schema converter 产出 schema,再用JsonSchemaCoercer在clientInterceptors阶段、schema 校验之前完成转换,并用WeakMap缓存转换结果。
何时可以跳过:如果已在 schema 中自行coerce,或对性能敏感(它带来额外运行时开销)。
其他 handler 选项
interceptors/routingInterceptors/clientInterceptors:日志、错误映射等拦截器;filter:从匹配中排除某些 procedure;errorStatusMap+customErrorResponseBodyEncoder:自定义错误响应。默认情况下,ORPCError的 code 通过COMMON_ERROR_STATUS_MAP映射到 HTTP 状态码(见 packages/client/src/error.ts),例如NOT_FOUND→404、BAD_REQUEST→400、UNAUTHORIZED→401、TOO_MANY_REQUESTS→429、INTERNAL_SERVER_ERROR→500。errorStatusMap的值必须为 4xx 或 5xx(≥400),可以直接展开COMMON_ERROR_STATUS_MAP再覆盖个别项。
客户端:OpenAPILink
基本用法
OpenAPILink通过类型安全客户端调用 OpenAPI 形态的 oRPC API(或任何符合规范的服务端)。它需要 contract 或 router 类型来获知每个 procedure 的路由:
import type { RouterContractClient } from '@orpc/contract' import type { JsonifiedClient } from '@orpc/openapi' import { createORPCClient } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi/fetch' const link = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api', headers: ({ context }) => ({ authorization: context?.token ? `Bearer ${context.token}` : undefined, }), }) const client: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(link)从源码看,OpenAPILink由OpenAPILinkCodec(编解码,含url、headers、serializer、customErrorResponseBodyDecoder等选项,见 packages/openapi/src/adapters/standard/openapi-link-codec.ts)与FetchLinkTransport(Fetch 传输层)组成,见 packages/openapi/src/adapters/fetch/openapi-link.ts。url是相对 base URL(不含 origin,默认/),应与 OpenAPI handler 的挂载路径一致。
JsonifiedClient:为什么输出类型需要包装
OpenAPI 序列化是单向的:Date在响应中会变成字符串。因此@orpc/openapi提供了JsonifiedClient类型(packages/openapi/src/types.ts),把输出与错误数据中的Date、bigint、URL映射为string,Map/Set映射为数组等,File/Blob保持原样。
如果使用 Router 而非 contract,客户端类型写成JsonifiedClient<RouterClient<typeof router>>(RouterClient来自@orpc/server)。
用 SmartCoercionLinkPlugin 恢复原生类型
在响应侧添加SmartCoercionLinkPlugin可以恢复原生类型,之后便可从客户端类型中去掉JsonifiedClient包装。注意它的第一个参数是 contract:
import { SmartCoercionLinkPlugin } from '@orpc/json-schema' const link = new OpenAPILink(contract, { plugins: [new SmartCoercionLinkPlugin(contract, { converters: [new ZodToJsonSchemaConverter()] })], })其实现(packages/json-schema/src/smart-coercion-link-plugin.ts)通过getProcedureContractOrThrow按 path 定位 procedure,在~response-validation之后对输出与错误值做 schema 驱动的无损 coercion。
向客户端分发契约
如果想把契约分发给客户端而不打包服务端代码,可将其 minify 为 JSON 后导入(配合类型断言),完整流程见orpc-contract技能中的 "Ship the contract" 章节;仓库中也有无运行时导入的用法文档 apps/content/docs/openapi/link-without-runtime-imports.mdx。
规范文档与交互文档
OpenAPIGenerator:生成 OpenAPI 3.2(可降级)
OpenAPIGenerator将 Router 或 contract 转为 OpenAPI 文档:
import { OpenAPIGenerator } from '@orpc/openapi' import { OpenAPIReferenceHandlerPlugin } from '@orpc/openapi/plugins' import { ZodToJsonSchemaConverter } from '@orpc/zod' const generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()] }) const handler = new OpenAPIHandler(router, { plugins: [ new OpenAPIReferenceHandlerPlugin({ spec: () => generator.generate(router, { base: { info: { title: 'Planet API', version: '1.0.0' }, servers: [{ url: '/api' }], // 生产环境请使用绝对 URL }, }), }), ], })生成器的关键行为(源码见 packages/openapi/src/openapi-generator.ts):
- 默认版本
3.2.0;可通过version指定任意3.0.x或3.1.x,文档会先按 3.2 生成再整体降级(内部使用@openapi-spec/downgrader的downgradeSpecV32ToV31/downgradeSpecV31ToV30); QUERYprocedure 必须使用 3.2,否则生成时报OpenAPIGeneratorError;base提供info、servers、components等起始字段,同样随版本降级,openapi字段由version派生;filter可排除部分 procedure;errorStatusMap与customErrorResponseBodySchema控制错误响应的 schema;- 根级
$defs会被提升到components.schemas,customComponentName可自定义提升后组件名(重名时自动加后缀); - 未提供 converter 的 schema 回退到
StandardJsonSchemaConverter(标准 JSON Schema 转换)。
用 OpenAPI 元数据丰富文档
通过openapi元数据可丰富生成的操作对象:operationId、summary、description、tags、successDescription,以及spec回调——它接收生成的 operation 对象并返回扩展后的对象(如 security requirements、额外响应)。即使目标是 3.1/3.0,spec与base也应写成 OpenAPI 3.2 对象,生成器负责整体降级。
除 Zod 外,Valibot(@orpc/valibot)与 ArkType(@orpc/arktype)同样提供 converter,仓库中对应实现见 packages/valibot/src/converter.ts 与 packages/arktype/src/converter.ts。
OpenAPIReferenceHandlerPlugin:Scalar 与 Swagger
OpenAPIReferenceHandlerPlugin默认在 handler 前缀下提供:
GET /spec.json:OpenAPI 规范 JSON;GET /:Scalar API 参考 UI(provider: 'swagger'时切换为 Swagger UI)。
可配置项(源码见 packages/openapi/src/plugins/openapi-reference.ts):
| 选项 | 默认值 | 说明 |
|---|---|---|
spec | 必填 | 静态或动态(函数)的 OpenAPI 文档 |
allow | 始终允许 | 返回false时该请求视为未匹配,可用于鉴权限制 |
specPath | /spec.json | 规范 JSON 路径 |
docsPath | / | 文档 UI 路径 |
provider | 'scalar' | 'scalar'或'swagger' |
providerConfig | 无 | 透传给 Scalar / Swagger UI 的配置 |
providerScriptUrl/providerCssUrl | 各 CDN 默认地址 | 覆盖 UI 资源地址 |
docsTitle/docsHead | spec.info.title/ 空 | 页面标题与<head>注入 HTML |
从实现细节看,该插件以routingInterceptors方式注入,仅在GET请求且未命中其他 procedure 时介入;响应会序列化 spec JSON 文件或生成嵌入 UI 的 HTML 页面,并对标题/配置做 HTML 与 JSON 双重转义以防注入。
Contract-first 工作流
契约优先工作流属于orpc-contract技能范畴:用oc(@orpc/contract)定义契约形状、用implement(@orpc/server)实现、从客户端消费契约、以 minified JSON 分发契约,或用 Hey API 从现有 OpenAPI spec 反向生成契约。
在 OpenAPI 侧,contract 与 Router 行为完全一致:
- 用
.meta(openapi({...}))在oc上附加路由(见上文 Routing 一节); - 照常使用
OpenAPIHandler服务实现后的 Router; - 直接从 contract 生成 spec 文档;
- 将 contract 作为运行时值传给
OpenAPILink。
收尾验证清单
在宣布完成前,务必实测以下端点:
- 请求 handler 前缀下的
/spec.json,确认操作对象存在; - 请求一个已配置的路由端点(如
GET /api/planets/earth),确认方法、路径、状态码与配置一致; - 若启用了 Smart Coercion,确认 query/path 中的字符串被正确转换为 schema 期望的类型;
- 若同时挂载了
RPCHandler,确认两者互不干扰、matched判定符合预期。
更详细的参数说明与边界情况可继续阅读仓库文档:apps/content/docs/openapi/routing.mdx、apps/content/docs/openapi/input-and-output-mapping.mdx、apps/content/docs/openapi/handler.mdx、apps/content/docs/openapi/link.mdx、apps/content/docs/openapi/specification.mdx 与 apps/content/docs/openapi/scalar.mdx;源码级实现可分别在 packages/openapi、packages/json-schema/src/smart-coercion-handler-plugin.ts 与 packages/json-schema/src/smart-coercion-link-plugin.ts 中深入研读。
- 后端
- RPC框架
- API设计
【免费下载链接】orpc
Typesafe APIs Made Simple 🪄
相关推荐
解决iOS开发痛点:ExpandableCell让复杂列表变得简单高效
解决iOS开发痛点:ExpandableCell让复杂列表变得简单高效 在iOS开发中,创建可扩展和可折叠的表格视图单元格一直是个技术挑战。传统方法需要处理复杂
lnd REST API WebSocket 实战指南:用流式 RPC 与双端流实现实时通知与通道审批
lnd REST API WebSocket 实战指南:用流式 RPC 与双端流实现实时通知与通道审批 导读 Lightning Network Daemon(
区块链Apache Iceberg REST Catalog 协议与 API 规范深度指南:从 OpenAPI 契约到引擎接入实战
Apache Iceberg REST Catalog 协议与 API 规范深度指南:从 OpenAPI 契约到引擎接入实战 Apache Iceberg 的
数据湖大数据数据存储
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考