web-data-fetching-trpc
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesetRPC Type-Safe API Patterns
tRPC类型安全API模式
Quick Guide: tRPC provides end-to-end type safety by sharing TypeScript types directly from server to client -- no code generation, no schema files. Exporttype from your router (this is the key bridge). Use Zod for input validation,AppRouterwith proper codes for errors, and middleware for auth. v11 is the current stable version: transformer goes insideTRPCError, subscriptions use async generators (nothttpBatchLink()), andobservable()is the recommended React integration.@trpc/tanstack-react-query
<critical_requirements>
快速指南: tRPC通过直接从服务器向客户端共享TypeScript类型来提供端到端的类型安全——无需代码生成,无需Schema文件。从你的路由中导出类型(这是关键桥梁)。使用Zod进行输入验证,使用带有正确错误码的AppRouter处理错误,使用中间件处理认证。v11是当前稳定版本:转换器需放在TRPCError内部,订阅使用异步生成器(而非httpBatchLink()),推荐使用observable()进行React集成。@trpc/tanstack-react-query
<critical_requirements>
CRITICAL: Before Using This Skill
重要提示:使用此技能前须知
(You MUST export type from your tRPC router for client-side type inference)
AppRouter(You MUST use with appropriate error codes -- never throw raw Error objects)
TRPCError(You MUST use Zod for input validation on ALL procedures accepting user input)
(You MUST place transformer inside in v11 -- NOT at client level)
httpBatchLink()</critical_requirements>
Auto-detection: tRPC router, initTRPC, createTRPCClient, createTRPCContext, @trpc/server, @trpc/client, @trpc/react-query, @trpc/tanstack-react-query, TRPCError, procedure, publicProcedure, protectedProcedure, query, mutation, subscription, httpBatchLink, queryOptions, mutationOptions, useTRPC
When to use:
- Building APIs in TypeScript monorepos with shared types
- End-to-end type safety without code generation
- Full-stack TypeScript applications where both client and server are TypeScript
- Projects where types should flow automatically from backend to frontend
When NOT to use:
- Public APIs consumed by third parties (use OpenAPI/REST)
- Non-TypeScript clients (mobile apps, other languages)
- Need HTTP caching at CDN level (tRPC uses POST by default)
- GraphQL requirements with partial queries
Key patterns covered:
- Router and procedure definition (initTRPC, router, procedure)
- Input validation with Zod schemas
- Context and middleware for authentication
- Error handling with TRPCError codes
- React integration via (recommended) or
@trpc/tanstack-react-query(classic)@trpc/react-query - Optimistic updates, infinite queries, subscriptions
Detailed Resources:
- examples/core.md - Router setup, CRUD, provider, type inference, queryOptions
- examples/middleware.md - Logging, rate limiting, org-scoped access
- examples/infinite-queries.md - Cursor pagination, infinite scroll
- examples/optimistic-updates.md - Optimistic updates with rollback
- examples/subscriptions.md - Async generator subscriptions, SSE
- examples/file-uploads.md - FormData file uploads (v11+)
- reference.md - Decision frameworks, error codes, anti-patterns, v11 migration
<philosophy>
(必须从你的tRPC路由中导出类型,以实现客户端类型推断)
AppRouter(必须使用带有合适错误码的——绝不要抛出原始Error对象)
TRPCError(必须对所有接收用户输入的流程使用Zod进行输入验证)
(在v11版本中,必须将转换器放在内部——而非客户端层级)
httpBatchLink()</critical_requirements>
自动检测: tRPC router、initTRPC、createTRPCClient、createTRPCContext、@trpc/server、@trpc/client、@trpc/react-query、@trpc/tanstack-react-query、TRPCError、procedure、publicProcedure、protectedProcedure、query、mutation、subscription、httpBatchLink、queryOptions、mutationOptions、useTRPC
适用场景:
- 在TypeScript单体仓库中构建API,共享类型
- 无需代码生成的端到端类型安全
- 客户端和服务器均为TypeScript的全栈TypeScript应用
- 类型需自动从后端流向前端的项目
不适用场景:
- 供第三方使用的公开API(使用OpenAPI/REST)
- 非TypeScript客户端(移动应用、其他语言)
- 需要CDN层级HTTP缓存的场景(tRPC默认使用POST)
- 需要部分查询的GraphQL需求
涵盖的核心模式:
- 路由和流程定义(initTRPC、router、procedure)
- 使用Zod Schema进行输入验证
- 用于认证的上下文和中间件
- 使用TRPCError错误码处理错误
- 通过(推荐)或
@trpc/tanstack-react-query(经典版)进行React集成@trpc/react-query - 乐观更新、无限查询、订阅
详细资源:
- examples/core.md - 路由设置、CRUD、提供者、类型推断、queryOptions
- examples/middleware.md - 日志、限流、组织范围访问
- examples/infinite-queries.md - 游标分页、无限滚动
- examples/optimistic-updates.md - 带回滚的乐观更新
- examples/subscriptions.md - 异步生成器订阅、SSE
- examples/file-uploads.md - FormData文件上传(v11+)
- reference.md - 决策框架、错误码、反模式、v11迁移
<philosophy>
Philosophy
设计理念
tRPC eliminates API layer friction by sharing types directly between server and client. No schemas to write, no code to generate -- export your router type and import it client-side for full autocompletion and type safety.
Core principles:
- Zero schema duplication: Types flow from backend to frontend automatically
- TypeScript-native: Leverages TypeScript's type inference, not code generation
- Procedure-based: Queries read data, mutations write data -- clear separation
- Composable middleware: Build reusable authentication and validation layers
- Built on TanStack Query: Full caching, invalidation, and optimistic updates via React Query
Trade-offs:
- Requires TypeScript on both ends (no polyglot support)
- Best in monorepos where types can be shared directly
- Not suitable for public APIs needing OpenAPI documentation
- Uses POST by default -- no HTTP caching without configuration
<patterns>
tRPC通过在服务器和客户端之间直接共享类型来消除API层的摩擦。无需编写Schema,无需生成代码——导出你的路由类型并在客户端导入,即可获得完整的自动补全和类型安全。
核心原则:
- 零Schema重复: 类型自动从后端流向前端
- TypeScript原生: 利用TypeScript的类型推断,而非代码生成
- 基于流程: 查询用于读取数据,变更用于写入数据——清晰分离
- 可组合中间件: 构建可复用的认证和验证层
- 基于TanStack Query构建: 通过React Query实现完整的缓存、失效和乐观更新
权衡:
- 两端均需使用TypeScript(不支持多语言)
- 在可直接共享类型的单体仓库中表现最佳
- 不适用于需要OpenAPI文档的公开API
- 默认使用POST——不配置则无HTTP缓存
<patterns>
Core Patterns
核心模式
Pattern 1: tRPC Initialization and Router Setup
模式1:tRPC初始化和路由设置
Initialize tRPC once per application. Export the router and procedure factories.
typescript
import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
import type { Context } from "./context";
const t = initTRPC.context<Context>().create({
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;Why good: Single initialization point, error formatter provides structured Zod errors to client, exported factories enable composition across router files
See examples/core.md Pattern 1 for complete router and context factory.
每个应用初始化一次tRPC。导出路由和流程工厂。
typescript
import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
import type { Context } from "./context";
const t = initTRPC.context<Context>().create({
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;优势: 单一初始化点,错误格式化器向客户端提供结构化的Zod错误,导出的工厂支持跨路由文件的组合
查看examples/core.md模式1获取完整的路由和上下文工厂。
Pattern 2: Procedures with Zod Input Validation
模式2:带Zod输入验证的流程
Zod schemas provide runtime validation AND TypeScript inference from a single source.
typescript
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 is typed: { email: string; name: string }
return ctx.db.user.create({ data: input });
}),
});typescript
// BAD: No input validation -- input is 'unknown'
publicProcedure.mutation(async ({ input }) => {
return ctx.db.user.create({ data: input as any }); // Dangerous!
});Why bad: Without Zod validation, input is unknown type, no runtime validation, injection risks, defeats TypeScript
as anySee examples/core.md Pattern 2 for complete CRUD router.
Zod Schema提供运行时验证和TypeScript推断,只需一份定义。
typescript
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 }
return ctx.db.user.create({ data: input });
}),
});typescript
// 错误示例:无输入验证——input类型为'unknown'
publicProcedure.mutation(async ({ input }) => {
return ctx.db.user.create({ data: input as any }); // 危险!
});弊端: 没有Zod验证的话,input类型为unknown,无运行时验证,存在注入风险,破坏TypeScript的类型安全
as any查看examples/core.md模式2获取完整的CRUD路由。
Pattern 3: Authentication Middleware
模式3:认证中间件
Middleware narrows context types -- becomes non-nullable after auth middleware.
ctx.usertypescript
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);Why good: Auth enforced at procedure definition, TypeScript narrows to non-nullable, eliminates duplicated if-checks in every handler
ctx.userSee examples/middleware.md for logging, rate limiting, and org-scoped access patterns.
中间件缩小上下文类型范围——经过认证中间件后,变为非空。
ctx.usertypescript
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);优势: 在流程定义阶段强制认证,TypeScript将缩小为非空类型,消除每个处理器中重复的if检查
ctx.user查看examples/middleware.md获取日志、限流和组织范围访问模式。
Pattern 4: AppRouter Type Export
模式4:导出AppRouter类型
This is the KEY to tRPC's type safety. Export the router type for client-side inference.
typescript
export const appRouter = router({
user: userRouter,
post: postRouter,
});
// THIS IS ESSENTIAL -- without it, clients have no type inference
export type AppRouter = typeof appRouter;Use / for extracting procedure types:
inferRouterInputsinferRouterOutputstypescript
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
type RouterInputs = inferRouterInputs<AppRouter>;
type RouterOutputs = inferRouterOutputs<AppRouter>;
// Extract specific type
type User = RouterOutputs["user"]["getById"];See examples/core.md Pattern 4 for complete type inference utilities.
这是tRPC类型安全的关键。导出路由类型以实现客户端推断。
typescript
export const appRouter = router({
user: userRouter,
post: postRouter,
});
// 这是必不可少的——没有它,客户端无法进行类型推断
export type AppRouter = typeof appRouter;使用/提取流程类型:
inferRouterInputsinferRouterOutputstypescript
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
type RouterInputs = inferRouterInputs<AppRouter>;
type RouterOutputs = inferRouterOutputs<AppRouter>;
// 提取特定类型
type User = RouterOutputs["user"]["getById"];查看examples/core.md模式4获取完整的类型推断工具。
Pattern 5: React Integration (v11 Recommended)
模式5:React集成(推荐v11版本)
v11 introduces with / factories that work directly with TanStack Query hooks.
@trpc/tanstack-react-queryqueryOptionsmutationOptionstypescript
// Setup: createTRPCContext provides typed hooks
import { createTRPCContext } from "@trpc/tanstack-react-query";
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
// Usage: standard TanStack Query hooks with tRPC type safety
const trpc = useTRPC();
const { data } = useQuery(trpc.user.getById.queryOptions({ id: userId }));v11 CRITICAL: Transformer must be inside , NOT at level.
httpBatchLink()createTRPCClient()typescript
// BAD: v11 error
createTRPCClient({ transformer: superjson, links: [...] });
// GOOD: transformer inside the link
httpBatchLink({ url: "/api/trpc", transformer: superjson });See examples/core.md Patterns 3 and 5 for complete provider and component setup.
v11版本引入,其/工厂可直接与TanStack Query钩子配合使用。
@trpc/tanstack-react-queryqueryOptionsmutationOptionstypescript
// 设置:createTRPCContext提供类型化钩子
import { createTRPCContext } from "@trpc/tanstack-react-query";
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
// 使用:标准TanStack Query钩子搭配tRPC类型安全
const trpc = useTRPC();
const { data } = useQuery(trpc.user.getById.queryOptions({ id: userId }));v11重要提示: 转换器必须放在内部,而非层级。
httpBatchLink()createTRPCClient()typescript
// 错误示例:v11错误用法
createTRPCClient({ transformer: superjson, links: [...] });
// 正确用法:转换器放在链接内部
httpBatchLink({ url: "/api/trpc", transformer: superjson });查看examples/core.md模式3和5获取完整的提供者和组件设置。
Pattern 6: Error Handling with TRPCError
模式6:使用TRPCError处理错误
Use standardized error codes that map to HTTP status codes.
typescript
// Server: throw TRPCError with appropriate code
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete",
cause: error, // Preserves original stack trace
});typescript
// Client: typed error handling
const trpc = useTRPC();
const deletePost = useMutation({
...trpc.post.delete.mutationOptions(),
onError: (error) => {
switch (error.data?.code) {
case "NOT_FOUND":
toast.error("Not found");
break;
case "FORBIDDEN":
toast.error("Not allowed");
break;
}
},
});See reference.md for complete error code table with HTTP status mappings.
使用标准化的错误码,这些错误码映射到HTTP状态码。
typescript
// 服务器端:抛出带有合适错误码的TRPCError
throw new TRPCError({
code: "NOT_FOUND",
message: "用户不存在",
});
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "删除失败",
cause: error, // 保留原始堆栈跟踪
});typescript
// 客户端:类型化错误处理
const trpc = useTRPC();
const deletePost = useMutation({
...trpc.post.delete.mutationOptions(),
onError: (error) => {
switch (error.data?.code) {
case "NOT_FOUND":
toast.error("未找到");
break;
case "FORBIDDEN":
toast.error("无权限");
break;
}
},
});查看reference.md获取完整的错误码表及HTTP状态码映射。
Pattern 7: Optimistic Updates
模式7:乐观更新
Cancel queries, snapshot state, optimistically update, rollback on error, invalidate on settle.
typescript
const trpc = useTRPC();
const queryClient = useQueryClient();
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: any) =>
old?.map((t: any) =>
t.id === id ? { ...t, completed: !t.completed } : t,
),
);
return { previousTodos };
},
onError: (err, vars, context) => {
if (context?.previousTodos)
queryClient.setQueryData(
trpc.todo.list.queryKey(),
context.previousTodos,
);
},
onSettled: () =>
queryClient.invalidateQueries({ queryKey: trpc.todo.list.queryKey() }),
});Why good: Immediate UI feedback, automatic rollback on failure, eventual consistency via invalidation
See examples/optimistic-updates.md for complete pattern with like button example.
</patterns>
<red_flags>
取消查询、快照状态、乐观更新、错误时回滚、结算时失效。
typescript
const trpc = useTRPC();
const queryClient = useQueryClient();
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: any) =>
old?.map((t: any) =>
t.id === id ? { ...t, completed: !t.completed } : t,
),
);
return { previousTodos };
},
onError: (err, vars, context) => {
if (context?.previousTodos)
queryClient.setQueryData(
trpc.todo.list.queryKey(),
context.previousTodos,
);
},
onSettled: () =>
queryClient.invalidateQueries({ queryKey: trpc.todo.list.queryKey() }),
});优势: 即时UI反馈,失败时自动回滚,通过失效实现最终一致性
查看examples/optimistic-updates.md获取完整模式及点赞按钮示例。
</patterns>
<red_flags>
RED FLAGS
注意事项
High Priority Issues:
- Missing -- clients have no type inference, defeats purpose of tRPC
export type AppRouter - Raw -- should use
throw new Error()with appropriate code for HTTP mappingTRPCError - Procedures without validation -- no runtime validation, type is
.input()unknown - Auth checks in procedure body -- should use middleware for protected procedures
- Transformer at client level in v11 -- must be inside , not at
httpBatchLink()levelcreateTRPCClient()
Medium Priority Issues:
- Missing SuperJSON transformer -- Date/Map/Set won't serialize correctly
- No error formatter -- Zod errors should be formatted for better client DX
- Optimistic updates without rollback -- must include handler to restore previous state
onError - Using for subscriptions -- v11 uses async generators;
observable()is the v10 patternobservable() - Using in middleware -- v11 changed to
rawInputfunctiongetRawInput()
Gotchas & Edge Cases:
- combines requests -- all batched requests share the same HTTP status code
httpBatchLink - SuperJSON transformer must be configured on BOTH client and server
- Context is created per-request -- don't store mutable state in context
- Middleware runs in order -- auth middleware should come before rate limiting
- Query keys are auto-generated -- use method (v11) or
queryKey()for manual accessgetQueryKey() - Subscription reconnection with requires
tracked()in input schemalastEventId - Don't retry mutations () -- retrying writes can cause duplicates
retry: false
</red_flags>
<critical_reminders>
高优先级问题:
- 缺少——客户端无法进行类型推断,违背tRPC的设计初衷
export type AppRouter - 直接抛出——应使用带有合适错误码的
throw new Error()以实现HTTP映射TRPCError - 流程未使用验证——无运行时验证,类型为
.input()unknown - 在流程体内进行认证检查——应使用中间件保护流程
- v11版本中转换器放在客户端层级——必须放在内部,而非
httpBatchLink()层级createTRPCClient()
中等优先级问题:
- 缺少SuperJSON转换器——Date/Map/Set无法正确序列化
- 无错误格式化器——Zod错误应格式化以提升客户端开发体验
- 乐观更新无回滚机制——必须包含处理器以恢复之前的状态
onError - 使用进行订阅——v11版本使用异步生成器;
observable()是v10版本的模式observable() - 在中间件中使用——v11版本已改为
rawInput函数getRawInput()
陷阱与边缘情况:
- 合并请求——所有批量请求共享相同的HTTP状态码
httpBatchLink - SuperJSON转换器必须在客户端和服务器端都配置
- 上下文是每个请求创建的——不要在上下文中存储可变状态
- 中间件按顺序运行——认证中间件应放在限流中间件之前
- 查询键是自动生成的——使用方法(v11)或
queryKey()手动访问getQueryKey() - 使用进行订阅重连需要输入Schema中包含
tracked()lastEventId - 不要重试变更操作()——重试写入可能导致重复数据
retry: false
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
重要提醒
(You MUST export type from your tRPC router for client-side type inference)
AppRouter(You MUST use with appropriate error codes -- never throw raw Error objects)
TRPCError(You MUST use Zod for input validation on ALL procedures accepting user input)
(You MUST place transformer inside in v11 -- NOT at client level)
httpBatchLink()Failure to follow these rules will break type safety, cause runtime errors, and defeat the purpose of using tRPC.
</critical_reminders>
(必须从你的tRPC路由中导出类型,以实现客户端类型推断)
AppRouter(必须使用带有合适错误码的——绝不要抛出原始Error对象)
TRPCError(必须对所有接收用户输入的流程使用Zod进行输入验证)
(在v11版本中,必须将转换器放在内部——而非客户端层级)
httpBatchLink()不遵循这些规则会破坏类型安全,导致运行时错误,违背使用tRPC的初衷。
</critical_reminders>