mcpField
W
web-data-fetching-trpc

web-data-fetching-trpc

github.com/agents-inc开发2662 次安装2 天前更新
代码仓库

关于 web-data-fetching-trpc

面向 TypeScript 开发者的 tRPC 编码模式指南,介绍路由器、过程、输入验证和客户端类型推导。内容涵盖 v11 的集成方式、错误处理、中间件及订阅等实践。

主要功能

  1. 01路由器类型共享
  2. 02过程输入验证
  3. 03中间件收窄上下文类型
  4. 04类型化查询与变更选项
  5. 05错误处理与订阅模式

适用场景

  1. 01构建端到端类型安全的内部 API
  2. 02为过程参数添加运行时验证
  3. 03为认证过程定义受保护上下文
  4. 04实现分页、订阅及乐观更新

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-data-fetching-trpc

安装后在 Claude Code 中输入/web-data-fetching-trpc确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
core.md
file-uploads.md
infinite-queries.md
middleware.md
optimistic-updates.md
subscriptions.md

详细介绍

tRPC 模式

快速指南: tRPC 通过一个导出的类型将类型从服务端传递到客户端,而不是通过生成的架构,因此 export type AppRouter = typeof appRouter 就是整个桥梁,没有它,下游的一切都会失效。过程接收经过验证的输入并返回一个值;TRPCError 代码会转换为 HTTP 状态码;中间件会收窄上下文类型,因此经过身份验证的过程中的 ctx.user 不可为空。在 v11 中,转换器移入了链接内部,订阅采用异步生成器,而 @trpc/tanstack-react-query 是当前的 React 集成。

详细资源:


适用路径

  • @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——因此,忘记检查会变成编译错误,而不是代码审查意见。

完整代码:examples/middleware.md


模式 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>

评论

支持 Markdown