关于 web-data-fetching-trpc
面向 TypeScript 开发者的 tRPC 编码模式指南,介绍路由器、过程、输入验证和客户端类型推导。内容涵盖 v11 的集成方式、错误处理、中间件及订阅等实践。
主要功能
- 01路由器类型共享
- 02过程输入验证
- 03中间件收窄上下文类型
- 04类型化查询与变更选项
- 05错误处理与订阅模式
适用场景
- 01构建端到端类型安全的内部 API
- 02为过程参数添加运行时验证
- 03为认证过程定义受保护上下文
- 04实现分页、订阅及乐观更新
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-data-fetching-trpc安装后在 Claude Code 中输入/web-data-fetching-trpc确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/core.mdfile-uploads.mdinfinite-queries.mdmiddleware.mdoptimistic-updates.mdsubscriptions.md详细介绍
tRPC 模式
快速指南: tRPC 通过一个导出的类型将类型从服务端传递到客户端,而不是通过生成的架构,因此
export type AppRouter = typeof appRouter就是整个桥梁,没有它,下游的一切都会失效。过程接收经过验证的输入并返回一个值;TRPCError代码会转换为 HTTP 状态码;中间件会收窄上下文类型,因此经过身份验证的过程中的ctx.user不可为空。在 v11 中,转换器移入了链接内部,订阅采用异步生成器,而@trpc/tanstack-react-query是当前的 React 集成。
详细资源:
- examples/core.md — 初始化、上下文、CRUD 路由器、提供程序、推断类型、
queryOptions - examples/middleware.md — 日志记录、速率限制、资源范围访问控制
- examples/infinite-queries.md — 端到端的游标分页
- examples/optimistic-updates.md — 完整的快照与回滚周期
- examples/subscriptions.md — 支持可恢复事件 ID 的异步生成器订阅
- examples/file-uploads.md — 输入架构中的
File(v11+) - reference.md — 错误代码到 HTTP 状态码对照表、批处理与失效说明、v10 → v11 迁移
适用路径
@trpc/tanstack-react-query— 当前的集成方式。createTRPCContext会生成一个useTRPC钩子,每个过程都会提供可直接用于标准查询钩子的queryOptions()、mutationOptions()、infiniteQueryOptions()和queryKey()。 模式 5。@trpc/react-query— 经典集成方式,在 v11 中仍受支持。过程本身会提供trpc.x.useQuery()钩子,而缓存键通过getQueryKey(trpc.x)获取,而非从过程的queryKey()获取。方便时再迁移;两种方式可以共存。
<critical_requirements>
编写 tRPC 代码前
导出路由器类型:export type AppRouter = typeof appRouter。 这一行就是客户端契约的全部内容——没有它,客户端就会退回到 unknown,tRPC 提供的所有保障也会随之消失;而且忘记导出时不会在出错位置提示错误。
为每个接受输入的过程在 .input() 上配置验证器。 它既是运行时检查,也是处理程序参数类型的来源,因此没有验证器的过程会接收 unknown,让人忍不住进行类型断言。
抛出带有代码的 TRPCError,不要直接抛出 Error。 代码用于映射 HTTP 状态,也是客户端据以分支处理的依据;直接抛出的 Error 到达客户端时会变成不透明的 500 错误。
将转换器放在 httpBatchLink() 内,而不是 createTRPCClient() 上。 v11 调整了它的位置,放在旧位置会在客户端构造时触发错误。
</critical_requirements>
自动检测: initTRPC、createTRPCClient、createTRPCContext、createTRPCOptionsProxy、@trpc/server、@trpc/client、@trpc/react-query、@trpc/tanstack-react-query、TRPCError、publicProcedure、protectedProcedure、httpBatchLink、httpSubscriptionLink、loggerLink、inferRouterInputs、inferRouterOutputs、useTRPC、tracked、AppRouter
适用范围:
- 路由器和过程的定义,以及将多个路由器组合为一个
- 输入验证及其推断出的类型
- 每个请求的上下文,以及能够收窄上下文的中间件
- 错误代码和客户端接收的错误结构
- 将过程转换为有类型的查询和变更选项
- 订阅、文件输入和游标分页
其他部分负责:
- 面向第三方或非 TypeScript 消费者发布的 API;这类 API 需要语言无关的契约文档,而不是共享类型
- 缓存策略、重试和失效语义——本技能负责确定如何将过程转换为查询客户端使用的选项,客户端则决定如何处理这些选项
.input()使用的模式库;该版本支持的任何验证器都可使用,示例中展示了一种- 会话和令牌的签发;上下文会使用会话,而不是建立会话
设计理念
这里没有任何 API 描述——没有模式文件、没有生成的客户端,也没有两个部分之间的构建步骤。路由器推断出的类型_就是_契约,它通过在代码库中导入类型来共享。
这样能立即保证准确性:过程的返回类型一旦改变,每个调用位置都会在同一次类型检查中标红,无需担心忘记重新生成。代价则是无法支持多种编程语言、没有发布的契约,也无法使用 HTTP 缓存——默认情况下,请求会通过 POST 发出,因此端点前面的 CDN 无法缓存。这就是为什么 tRPC 适合单个 TypeScript 代码库中的内部 API,却不太适合公共 API。
核心模式
模式 1:初始化
每个应用只初始化一次,并导出各个路由器组合时使用的工厂。
const t = initTRPC.context<Context>().create({
transformer: superjson, // Date, Map and Set survive the wire
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const middleware = t.middleware;
错误格式化器可以将验证失败转换为表单能够按字段显示的内容,而不是一条笼统的消息。
完整代码:examples/core.md——包括每个请求的上下文工厂
模式 2:过程和输入验证
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
});
export const userRouter = router({
create: protectedProcedure
.input(createUserSchema)
.mutation(async ({ input, ctx }) => {
// input: { email: string; name: string } — validated, and typed from the same schema
return ctx.db.user.create({ data: input });
}),
});
完整代码:examples/core.md
模式 3:收窄上下文的中间件
const isAuthenticated = middleware(async ({ ctx, next }) => {
if (!ctx.session || !ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { ...ctx, session: ctx.session, user: ctx.user } });
});
export const protectedProcedure = publicProcedure.use(isAuthenticated);
next({ ctx }) 的返回类型使基于 protectedProcedure 构建的每个过程中,ctx.user 都不会为 null——因此,忘记检查会变成编译错误,而不是代码审查意见。
模式 4:类型桥接
export const appRouter = router({ user: userRouter, post: postRouter });
export type AppRouter = typeof appRouter;
type RouterInputs = inferRouterInputs<AppRouter>;
type RouterOutputs = inferRouterOutputs<AppRouter>;
type User = RouterOutputs["user"]["getById"];
组件属性使用 RouterOutputs[...],而不是手写接口,因此只要过程移除了某个字段,所有读取该字段的组件都会标红。
完整代码:examples/core.md
模式 5:类型化查询和变更选项
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
const trpc = useTRPC();
const { data } = useQuery(trpc.user.getById.queryOptions({ id: userId }));
每个过程都带有 queryOptions()、mutationOptions()、infiniteQueryOptions() 和
queryKey()。选项对象会传入普通钩子,因此查询客户端能对选项对象执行的任何操作,
在这里也同样适用。
完整代码:examples/core.md — 提供程序、链接和组件
模式 6:错误
// Server
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete",
cause: error,
});
// Client
onError: (error) => {
switch (error.data?.code) {
case "NOT_FOUND":
return toast.error("Not found");
case "FORBIDDEN":
return toast.error("Not allowed");
}
};
cause 会在服务器日志中保留原始堆栈,而客户端仍只会收到代码和消息。代码到状态的
映射表见 reference.md。
模式 7:乐观更新
取消、快照、写入、失败时回滚、结算时失效——五个步骤都不能少,因为回滚才能确保乐观 写入安全。
const toggleTodo = useMutation({
...trpc.todo.toggle.mutationOptions(),
onMutate: async ({ id }) => {
await queryClient.cancelQueries({ queryKey: trpc.todo.list.queryKey() });
const previousTodos = queryClient.getQueryData(trpc.todo.list.queryKey());
queryClient.setQueryData(trpc.todo.list.queryKey(), (old) =>
toggle(old, id),
);
return { previousTodos };
},
onError: (err, vars, context) =>
queryClient.setQueryData(trpc.todo.list.queryKey(), context?.previousTodos),
onSettled: () =>
queryClient.invalidateQueries({ queryKey: trpc.todo.list.queryKey() }),
});
cancelQueries 不能省略:如果正在进行的重新获取在乐观写入后才完成,它会用变更前的
服务器状态覆盖乐观写入。
完整代码:examples/optimistic-updates.md
<red_flags>
注意事项
运行时会出错:
- 没有
export type AppRouter— 客户端会将类型推断为unknown,每次过程调用都没有类型, 而且遗漏此声明的文件不会报错。 - 在 v11 的
createTRPCClient()上配置转换器 — 客户端构造会抛出异常,并指出转换器本应 配置在哪个链接上。 - 只在一端配置转换器 — 两端对数据传输格式的理解不一致,
Date、Map和Set会变成 其他形式。 - 在订阅中使用
observable()— 这是 v10 的形式;v11 订阅使用异步生成器。 - 在中间件中使用
rawInput— v11 已将其替换为await getRawInput()。 - 乐观写入没有快照,也没有
onError— 变更失败后,界面仍会显示从未发生过的更改。
容易产生意外的行为:
httpBatchLink会将并发调用合并为一个请求,因此批次中的每个过程共用一个 HTTP 状态码 — 其中一个过程返回 401,其他过程看到的状态码也会是 401。- 上下文按请求构建,因此存储在其中的任何可变内容都会被丢弃,无法共享。
- 中间件按声明顺序运行,这决定了每一层能看到什么:先进行身份验证、再限流,意味着限流 会按已知用户进行。
- 重试变更会重复写入 — 对变更设置
retry: false。 - 可恢复订阅需要在输入架构中包含
lastEventId,这样tracked()才有位置可以恢复。 - 在过程体中编写身份验证检查,会导致类型系统认为
ctx.user仍可为 null,因此后续忘记 检查它的过程仍能通过编译。
</red_flags>