oRPC 契约优先开发
意图
将契约作为唯一事实来源,保存在 web/contract/* 中。
默认查询用法:当端点行为与契约 1:1 对应时,在调用点使用 useQuery(consoleQuery|marketplaceQuery.xxx.queryOptions(...))。
保持抽象最小化,并保留 TypeScript 推断。
最小结构
web/contract/ ├── base.ts ├── router.ts ├── marketplace.ts └── console/ ├── billing.ts └── ...其他领域 web/service/client.ts
核心工作流
- 在 web/contract/console/{domain}.ts 或 web/contract/marketplace.ts 中定义契约
使用 base.route({...}).output(type<...>()) 作为基线。
仅当请求包含 params/query/body 时才添加 .input(type<...>())。
对于没有输入的 GET,省略 .input(...)(不要使用 .input(type<unknown>()))。
- 在 web/contract/router.ts 中注册契约
直接从领域文件导入,并按 API 前缀嵌套。
- 通过 oRPC 查询工具从 UI 调用点消费。
import { useQuery } from '@tanstack/react-query' import { consoleQuery } from '@/service/client'
const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({ staleTime: 5 * 60 * 1000, throwOnError: true, select: invoice => invoice.url, }))
查询使用决策规则
默认:调用点直接使用 *.queryOptions(...)。
如果 3 个以上调用点共享相同的额外选项(例如 retry: false),则提取一个小型 queryOptions 助手,而不是 use-* 透传 hook。
仅在需要编排时创建 web/service/use-{domain}.ts:
- 组合多个查询/变更。
- 共享领域级派生状态或失效助手。
const invoicesBaseQueryOptions = () => consoleQuery.billing.invoices.queryOptions({ retry: false })
const invoiceQuery = useQuery({ ...invoicesBaseQueryOptions(), throwOnError: true, })
变更使用决策规则
默认:从 consoleQuery / marketplaceQuery 调用变更助手,例如 useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))。
如果变更流程高度自定义,使用 oRPC 客户端作为 mutationFn(例如 consoleClient.xxx / marketplaceClient.xxx),而不是手写通用的非 oRPC 变更逻辑。
关键 API 指南(.key vs .queryKey vs .mutationKey)
.key(...):
用于部分匹配操作(推荐用于失效/重新获取/取消模式)。
示例:queryClient.invalidateQueries({ queryKey: consoleQuery.billing.key() })
.queryKey(...):
用于某个具体查询的完整 key(精确查询标识/直接缓存寻址)。
.mutationKey(...):
用于某个具体变更的完整 key。
典型用例:变更默认值注册、变更状态过滤(useIsMutating、queryClient.isMutating),或显式的 devtools 分组。
反模式
不要用 options?: Partial<UseQueryOptions> 包装 useQuery。
当 oRPC queryOptions 已存在且适合用例时,不要拆分本地 queryKey/queryFn。
不要为单个端点创建薄薄的 use-* 透传 hook。
原因:这些模式会降低推断能力(data 可能变为 unknown,尤其是在 throwOnError/select 附近),并增加不必要的间接层。
契约规则
输入结构:始终使用 { params, query?, body? } 格式。
无输入 GET:省略 .input(...);不要使用 .input(type<unknown>())。
路径参数:在路径中使用 {paramName},在 params 对象中匹配。
路由嵌套:按 API 前缀分组(例如 /billing/* -> billing: {})。
禁止 barrel 文件:直接从具体文件导入。
类型:从 @/types/ 导入,使用 type<T>() helper。
变更:优先使用 mutationOptions;主要为了默认值/过滤/devtools 才使用显式 mutationKey。
类型导出
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>