trpc-type-safety
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesetRPC - End-to-End Type Safety
tRPC - 端到端类型安全
progressive_disclosure: entry_point: summary sections: - id: summary title: "tRPC Overview" tokens: 70 next: [when_to_use, quick_start] - id: when_to_use title: "When to Use tRPC" tokens: 150 next: [quick_start, core_concepts] - id: quick_start title: "Quick Start" tokens: 300 next: [core_concepts, router_definition] - id: core_concepts title: "Core Concepts" tokens: 400 next: [router_definition, procedures] - id: router_definition title: "Router Definition" tokens: 350 next: [procedures, context] - id: procedures title: "Procedures (Query & Mutation)" tokens: 400 next: [input_validation, context] - id: input_validation title: "Input Validation with Zod" tokens: 350 next: [context, middleware] - id: context title: "Context Management" tokens: 400 next: [middleware, error_handling] - id: middleware title: "Middleware" tokens: 400 next: [error_handling, client_setup] - id: error_handling title: "Error Handling" tokens: 350 next: [client_setup, react_integration] - id: client_setup title: "Client Setup" tokens: 400 next: [react_integration, nextjs_integration] - id: react_integration title: "React Query Integration" tokens: 450 next: [nextjs_integration, subscriptions] - id: nextjs_integration title: "Next.js App Router Integration" tokens: 500 next: [subscriptions, file_uploads] - id: subscriptions title: "Real-time Subscriptions" tokens: 400 next: [file_uploads, batching] - id: file_uploads title: "File Uploads" tokens: 300 next: [batching, typescript_inference] - id: batching title: "Batch Requests & Data Loaders" tokens: 350 next: [typescript_inference, testing] - id: typescript_inference title: "TypeScript Inference Patterns" tokens: 300 next: [testing, production_patterns] - id: testing title: "Testing Strategies" tokens: 400 next: [production_patterns, comparison] - id: production_patterns title: "Production Patterns" tokens: 450 next: [comparison, migration] - id: comparison title: "Comparison with REST & GraphQL" tokens: 250 next: [migration, best_practices] - id: migration title: "Migration from REST" tokens: 300 next: [best_practices] - id: best_practices title: "Best Practices & Performance" tokens: 400
progressive_disclosure: entry_point: summary sections: - id: summary title: "tRPC 概述" tokens: 70 next: [when_to_use, quick_start] - id: when_to_use title: "何时使用tRPC" tokens: 150 next: [quick_start, core_concepts] - id: quick_start title: "快速开始" tokens: 300 next: [core_concepts, router_definition] - id: core_concepts title: "核心概念" tokens: 400 next: [router_definition, procedures] - id: router_definition title: "路由定义" tokens: 350 next: [procedures, context] - id: procedures title: "过程(查询与变更)" tokens: 400 next: [input_validation, context] - id: input_validation title: "使用Zod进行输入验证" tokens: 350 next: [context, middleware] - id: context title: "上下文管理" tokens: 400 next: [middleware, error_handling] - id: middleware title: "中间件" tokens: 400 next: [error_handling, client_setup] - id: error_handling title: "错误处理" tokens: 350 next: [client_setup, react_integration] - id: client_setup title: "客户端配置" tokens: 400 next: [react_integration, nextjs_integration] - id: react_integration title: "React Query 集成" tokens: 450 next: [nextjs_integration, subscriptions] - id: nextjs_integration title: "Next.js App Router 集成" tokens: 500 next: [subscriptions, file_uploads] - id: subscriptions title: "实时订阅" tokens: 400 next: [file_uploads, batching] - id: file_uploads title: "文件上传" tokens: 300 next: [batching, typescript_inference] - id: batching title: "批量请求与数据加载器" tokens: 350 next: [typescript_inference, testing] - id: typescript_inference title: "TypeScript 推断模式" tokens: 300 next: [testing, production_patterns] - id: testing title: "测试策略" tokens: 400 next: [production_patterns, comparison] - id: production_patterns title: "生产环境实践" tokens: 450 next: [comparison, migration] - id: comparison title: "与REST和GraphQL的对比" tokens: 250 next: [migration, best_practices] - id: migration title: "从REST迁移" tokens: 300 next: [best_practices] - id: best_practices title: "最佳实践与性能优化" tokens: 400
Summary
概述
tRPC enables end-to-end type safety between TypeScript clients and servers without code generation. Define your API once, get automatic type inference everywhere.
Key Benefits: Zero codegen, TypeScript inference, React Query integration, minimal boilerplate.
tRPC 无需代码生成,即可在TypeScript客户端与服务器之间实现端到端类型安全。只需定义一次API,就能在各处自动获得类型推断。
核心优势:零代码生成、TypeScript类型推断、React Query集成、极少样板代码。
When to Use tRPC
何时使用tRPC
✅ Perfect For:
- Full-stack TypeScript applications (Next.js, T3 stack)
- Projects where client and server share TypeScript codebase
- Teams wanting REST-like simplicity with GraphQL-like type safety
- Apps using React Query for data fetching
- Internal APIs where you control both client and server
❌ Avoid When:
- Public APIs consumed by non-TypeScript clients
- Microservices in different languages
- Mobile apps using Swift/Kotlin (use REST/GraphQL instead)
- Need API documentation for external developers (OpenAPI better)
When to Choose:
- tRPC: Full-stack TypeScript, monorepo, internal tools
- REST: Public APIs, language-agnostic, broad compatibility
- GraphQL: Complex data graphs, multiple clients, flexible queries
✅ 适用场景:
- 全栈TypeScript应用(Next.js、T3栈)
- 客户端与服务器共享TypeScript代码库的项目
- 想要兼具REST简洁性与GraphQL类型安全性的团队
- 使用React Query进行数据获取的应用
- 由你控制客户端与服务器的内部API
❌ 避免场景:
- 供非TypeScript客户端使用的公共API
- 不同语言编写的微服务
- 使用Swift/Kotlin的移动应用(建议使用REST/GraphQL)
- 需要为外部开发者提供API文档的场景(OpenAPI更合适)
选型建议:
- tRPC:全栈TypeScript、单仓项目、内部工具
- REST:公共API、语言无关、广泛兼容性
- GraphQL:复杂数据图、多客户端、灵活查询
Quick Start
快速开始
Installation
安装
bash
undefinedbash
undefinedServer dependencies
服务器依赖
npm install @trpc/server zod
npm install @trpc/server zod
React/Next.js client dependencies
React/Next.js 客户端依赖
npm install @trpc/client @trpc/react-query @tanstack/react-query
undefinednpm install @trpc/client @trpc/react-query @tanstack/react-query
undefinedDefine Router (Server)
定义路由(服务器端)
typescript
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}` };
}),
createPost: t.procedure
.input(z.object({ title: z.string(), content: z.string() }))
.mutation(async ({ input }) => {
// Save to database
return { id: 1, ...input };
}),
});
export type AppRouter = typeof appRouter;typescript
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}` };
}),
createPost: t.procedure
.input(z.object({ title: z.string(), content: z.string() }))
.mutation(async ({ input }) => {
// 保存到数据库
return { id: 1, ...input };
}),
});
export type AppRouter = typeof appRouter;Use in Client (React)
在客户端中使用(React)
typescript
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/trpc';
export const trpc = createTRPCReact<AppRouter>();
// Component
function MyComponent() {
const { data } = trpc.hello.useQuery({ name: 'World' });
const createPost = trpc.createPost.useMutation();
return <div>{data?.greeting}</div>; // Fully typed!
}Next: Learn core concepts or dive into router definition.
typescript
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/trpc';
export const trpc = createTRPCReact<AppRouter>();
// 组件
function MyComponent() {
const { data } = trpc.hello.useQuery({ name: 'World' });
const createPost = trpc.createPost.useMutation();
return <div>{data?.greeting}</div>; // 完全类型化!
}下一步:了解核心概念或深入学习路由定义。
Core Concepts
核心概念
The tRPC Philosophy
tRPC 设计理念
tRPC provides type-safe remote procedure calls by sharing TypeScript types between client and server. No code generation—just TypeScript's inference.
tRPC 通过在客户端与服务器之间共享TypeScript类型,提供类型安全的远程过程调用。无需代码生成——仅依赖TypeScript的类型推断。
Key Components
核心组件
- Router: Collection of procedures (API endpoints)
- Procedure: Single API operation (query or mutation)
- Context: Request-scoped data (user, database, etc.)
- Middleware: Intercept/modify requests (auth, logging)
- Input/Output: Validated with Zod schemas
- 路由(Router):过程的集合(即API端点)
- 过程(Procedure):单个API操作(查询或变更)
- 上下文(Context):请求作用域的数据(用户信息、数据库连接等)
- 中间件(Middleware):拦截或修改请求(鉴权、日志等)
- 输入/输出:使用Zod schema进行验证
Type Flow
类型流转
typescript
// Server defines types
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(({ input }) => ({ id: input, name: 'Alice' })),
});
// Client gets automatic types
const user = await trpc.getUser.query('123');
// user is typed as { id: string, name: string }typescript
// 服务器定义类型
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(({ input }) => ({ id: input, name: 'Alice' })),
});
// 客户端自动获取类型
const user = await trpc.getUser.query('123');
// user 的类型为 { id: string, name: string }Architecture Pattern
架构模式
┌─────────────┐ Type-safe ┌──────────────┐
│ Client │ ←────────────────→ │ Server │
│ (React) │ No codegen! │ (Node.js) │
└─────────────┘ └──────────────┘
↓ ↓
React Query tRPC Router
(caching) (procedures)Advantages:
- Changes propagate instantly (no build step)
- Rename refactoring works across client/server
- Impossible to call wrong types
- Auto-complete for all API methods
┌─────────────┐ 类型安全 ┌──────────────┐
│ 客户端 │ ←────────────────→ │ 服务器 │
│ (React) │ 无需代码生成! │ (Node.js) │
└─────────────┘ └──────────────┘
↓ ↓
React Query tRPC 路由
(缓存) (过程集合)优势:
- 变更即时传播(无需构建步骤)
- 重命名重构可跨客户端/服务器生效
- 不可能调用错误的类型
- 所有API方法均支持自动补全
Router Definition
路由定义
Basic Router Structure
基础路由结构
typescript
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
// Procedures go here
});
export type AppRouter = typeof appRouter;typescript
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
// 过程定义在此处
});
export type AppRouter = typeof appRouter;Nested Routers (Namespacing)
嵌套路由(命名空间)
typescript
const userRouter = t.router({
getById: t.procedure
.input(z.string())
.query(({ input }) => getUser(input)),
create: t.procedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input }) => createUser(input)),
});
const postRouter = t.router({
list: t.procedure.query(() => getPosts()),
create: t.procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => createPost(input)),
});
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
// Client usage:
// trpc.user.getById.useQuery('123')
// trpc.post.list.useQuery()typescript
const userRouter = t.router({
getById: t.procedure
.input(z.string())
.query(({ input }) => getUser(input)),
create: t.procedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(({ input }) => createUser(input)),
});
const postRouter = t.router({
list: t.procedure.query(() => getPosts()),
create: t.procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => createPost(input)),
});
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
// 客户端用法:
// trpc.user.getById.useQuery('123')
// trpc.post.list.useQuery()Router Merging
路由合并
typescript
import { adminRouter } from './admin';
import { publicRouter } from './public';
export const appRouter = t.mergeRouters(publicRouter, adminRouter);typescript
import { adminRouter } from './admin';
import { publicRouter } from './public';
export const appRouter = t.mergeRouters(publicRouter, adminRouter);Router Organization Best Practices
路由组织最佳实践
server/
├── trpc.ts # tRPC instance, context, middleware
├── routers/
│ ├── user.ts # User-related procedures
│ ├── post.ts # Post-related procedures
│ └── index.ts # Combine all routers
└── index.ts # Export AppRouter typeserver/
├── trpc.ts # tRPC 实例、上下文、中间件
├── routers/
│ ├── user.ts # 用户相关过程
│ ├── post.ts # 文章相关过程
│ └── index.ts # 合并所有路由
└── index.ts # 导出AppRouter类型Procedures (Query & Mutation)
过程(查询与变更)
Query Procedures (Read Operations)
查询过程(读取操作)
typescript
const router = t.router({
// Simple query
getUser: t.procedure
.input(z.string())
.query(({ input }) => {
return db.user.findUnique({ where: { id: input } });
}),
// Query with multiple inputs
searchUsers: t.procedure
.input(z.object({
query: z.string(),
limit: z.number().default(10),
}))
.query(({ input }) => {
return db.user.findMany({
where: { name: { contains: input.query } },
take: input.limit,
});
}),
});typescript
const router = t.router({
// 简单查询
getUser: t.procedure
.input(z.string())
.query(({ input }) => {
return db.user.findUnique({ where: { id: input } });
}),
// 多输入查询
searchUsers: t.procedure
.input(z.object({
query: z.string(),
limit: z.number().default(10),
}))
.query(({ input }) => {
return db.user.findMany({
where: { name: { contains: input.query } },
take: input.limit,
});
}),
});Mutation Procedures (Write Operations)
变更过程(写入操作)
typescript
const router = t.router({
createUser: t.procedure
.input(z.object({
name: z.string().min(3),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
updateUser: t.procedure
.input(z.object({
id: z.string(),
data: z.object({
name: z.string().optional(),
email: z.string().email().optional(),
}),
}))
.mutation(async ({ input }) => {
return await db.user.update({
where: { id: input.id },
data: input.data,
});
}),
});typescript
const router = t.router({
createUser: t.procedure
.input(z.object({
name: z.string().min(3),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
updateUser: t.procedure
.input(z.object({
id: z.string(),
data: z.object({
name: z.string().optional(),
email: z.string().email().optional(),
}),
}))
.mutation(async ({ input }) => {
return await db.user.update({
where: { id: input.id },
data: input.data,
});
}),
});Query vs Mutation
查询 vs 变更
| Aspect | Query | Mutation |
|---|---|---|
| Purpose | Read data | Modify data |
| HTTP Method | GET | POST |
| Caching | Cached by React Query | Not cached |
| Idempotent | Yes | No |
| Side Effects | None | Database writes, emails, etc. |
| 维度 | 查询 | 变更 |
|---|---|---|
| 用途 | 读取数据 | 修改数据 |
| HTTP方法 | GET | POST |
| 缓存 | 由React Query缓存 | 不缓存 |
| 幂等性 | 是 | 否 |
| 副作用 | 无 | 数据库写入、发送邮件等 |
Output Typing
输出类型定义
typescript
const router = t.router({
getUser: t.procedure
.input(z.string())
.output(z.object({ id: z.string(), name: z.string() })) // Optional
.query(({ input }) => {
return { id: input, name: 'Alice' };
}),
});Note: Output validation adds runtime overhead—use for critical data only.
typescript
const router = t.router({
getUser: t.procedure
.input(z.string())
.output(z.object({ id: z.string(), name: z.string() })) // 可选
.query(({ input }) => {
return { id: input, name: 'Alice' };
}),
});注意:输出验证会增加运行时开销——仅对关键数据使用。
Input Validation with Zod
使用Zod进行输入验证
Why Zod?
为什么选择Zod?
tRPC uses Zod for runtime type validation and TypeScript inference. Zod schemas provide:
- Runtime validation (prevent invalid data)
- TypeScript types (auto-inferred from schema)
- Transformation (parse, coerce, default values)
tRPC 使用Zod进行运行时类型验证和TypeScript类型推断。Zod schema提供:
- 运行时验证(防止无效数据)
- TypeScript类型(从schema自动推断)
- 转换(解析、强制转换、默认值)
Basic Validation
基础验证
typescript
import { z } from 'zod';
const router = t.router({
createPost: t.procedure
.input(z.object({
title: z.string().min(5).max(100),
content: z.string(),
published: z.boolean().default(false),
tags: z.array(z.string()).optional(),
}))
.mutation(({ input }) => {
// input is fully typed and validated
return createPost(input);
}),
});typescript
import { z } from 'zod';
const router = t.router({
createPost: t.procedure
.input(z.object({
title: z.string().min(5).max(100),
content: z.string(),
published: z.boolean().default(false),
tags: z.array(z.string()).optional(),
}))
.mutation(({ input }) => {
// input 已完全类型化并验证
return createPost(input);
}),
});Advanced Validation
高级验证
typescript
const createUserInput = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18),
role: z.enum(['user', 'admin']),
metadata: z.record(z.string(), z.unknown()).optional(),
});
const router = t.router({
createUser: t.procedure
.input(createUserInput)
.mutation(({ input }) => {
// All validation passed
return saveUser(input);
}),
});typescript
const createUserInput = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(18),
role: z.enum(['user', 'admin']),
metadata: z.record(z.string(), z.unknown()).optional(),
});
const router = t.router({
createUser: t.procedure
.input(createUserInput)
.mutation(({ input }) => {
// 所有验证已通过
return saveUser(input);
}),
});Transformations
类型转换
typescript
const router = t.router({
getUser: t.procedure
.input(
z.object({
id: z.string().transform((id) => parseInt(id, 10)),
})
)
.query(({ input }) => {
// input.id is now a number
return db.user.findUnique({ where: { id: input.id } });
}),
});typescript
const router = t.router({
getUser: t.procedure
.input(
z.object({
id: z.string().transform((id) => parseInt(id, 10)),
})
)
.query(({ input }) => {
// input.id 现在是数字类型
return db.user.findUnique({ where: { id: input.id } });
}),
});Reusable Schemas
可复用Schema
typescript
// schemas/user.ts
export const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
export const UpdateUserSchema = CreateUserSchema.partial().extend({
id: z.string(),
});
// routers/user.ts
const router = t.router({
create: t.procedure.input(CreateUserSchema).mutation(/*...*/),
update: t.procedure.input(UpdateUserSchema).mutation(/*...*/),
});typescript
// schemas/user.ts
export const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
export const UpdateUserSchema = CreateUserSchema.partial().extend({
id: z.string(),
});
// routers/user.ts
const router = t.router({
create: t.procedure.input(CreateUserSchema).mutation(/*...*/),
update: t.procedure.input(UpdateUserSchema).mutation(/*...*/),
});Context Management
上下文管理
What is Context?
什么是上下文?
Context provides request-scoped data to all procedures—authentication, database connections, logging, etc.
上下文为所有过程提供请求作用域的数据——如认证信息、数据库连接、日志工具等。
Creating Context
创建上下文
typescript
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
export async function createContext(opts: CreateNextContextOptions) {
const session = await getSession(opts.req);
return {
session,
db: prisma,
req: opts.req,
res: opts.res,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();typescript
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
export async function createContext(opts: CreateNextContextOptions) {
const session = await getSession(opts.req);
return {
session,
db: prisma,
req: opts.req,
res: opts.res,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();Using Context in Procedures
在过程中使用上下文
typescript
const router = t.router({
getMe: t.procedure.query(({ ctx }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return ctx.db.user.findUnique({
where: { id: ctx.session.user.id },
});
}),
createPost: t.procedure
.input(z.object({ title: z.string() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({
data: {
title: input.title,
authorId: ctx.session.user.id,
},
});
}),
});typescript
const router = t.router({
getMe: t.procedure.query(({ ctx }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return ctx.db.user.findUnique({
where: { id: ctx.session.user.id },
});
}),
createPost: t.procedure
.input(z.object({ title: z.string() }))
.mutation(async ({ ctx, input }) => {
return ctx.db.post.create({
data: {
title: input.title,
authorId: ctx.session.user.id,
},
});
}),
});Context Best Practices
上下文最佳实践
typescript
// ✅ Good: Lazy database connection
export async function createContext(opts: CreateNextContextOptions) {
return {
getDB: () => prisma, // Lazy
session: await getSession(opts.req),
};
}
// ❌ Bad: Heavy computation in context
export async function createContext(opts: CreateNextContextOptions) {
const allUsers = await prisma.user.findMany(); // Too expensive!
return { allUsers };
}typescript
// ✅ 推荐:延迟数据库连接
export async function createContext(opts: CreateNextContextOptions) {
return {
getDB: () => prisma, // 延迟初始化
session: await getSession(opts.req),
};
}
// ❌ 不推荐:在上下文中执行重计算
export async function createContext(opts: CreateNextContextOptions) {
const allUsers = await prisma.user.findMany(); // 开销过大!
return { allUsers };
}Middleware
中间件
What is Middleware?
什么是中间件?
Middleware intercepts procedure calls to add cross-cutting concerns: logging, timing, authentication, rate limiting.
中间件用于拦截过程调用,添加横切关注点:日志、计时、认证、限流等。
Basic Middleware
基础中间件
typescript
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
console.log(`→ ${type} ${path}`);
const result = await next();
const duration = Date.now() - start;
console.log(`✓ ${type} ${path} - ${duration}ms`);
return result;
});
const loggedProcedure = t.procedure.use(loggerMiddleware);typescript
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
console.log(`→ ${type} ${path}`);
const result = await next();
const duration = Date.now() - start;
console.log(`✓ ${type} ${path} - ${duration}ms`);
return result;
});
const loggedProcedure = t.procedure.use(loggerMiddleware);Authentication Middleware
认证中间件
typescript
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.session.user, // Narrow type
},
});
});
// Protected procedure builder
const protectedProcedure = t.procedure.use(isAuthed);
const router = t.router({
// Public
getPublicPosts: t.procedure.query(() => getPosts()),
// Protected - requires authentication
getMyPosts: protectedProcedure.query(({ ctx }) => {
// ctx.user is guaranteed to exist
return getPostsByUser(ctx.user.id);
}),
});typescript
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
...ctx,
user: ctx.session.user, // 缩小类型范围
},
});
});
// 受保护的过程构建器
const protectedProcedure = t.procedure.use(isAuthed);
const router = t.router({
// 公开接口
getPublicPosts: t.procedure.query(() => getPosts()),
// 受保护接口 - 需要认证
getMyPosts: protectedProcedure.query(({ ctx }) => {
// ctx.user 已保证存在
return getPostsByUser(ctx.user.id);
}),
});Chaining Middleware
链式中间件
typescript
const timingMiddleware = t.middleware(async ({ next }) => {
const start = performance.now();
const result = await next();
console.log(`Execution time: ${performance.now() - start}ms`);
return result;
});
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
await checkRateLimit(ctx.session?.user?.id);
return next();
});
const protectedProcedure = t.procedure
.use(timingMiddleware)
.use(rateLimitMiddleware)
.use(isAuthed);typescript
const timingMiddleware = t.middleware(async ({ next }) => {
const start = performance.now();
const result = await next();
console.log(`执行时间: ${performance.now() - start}ms`);
return result;
});
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
await checkRateLimit(ctx.session?.user?.id);
return next();
});
const protectedProcedure = t.procedure
.use(timingMiddleware)
.use(rateLimitMiddleware)
.use(isAuthed);Context Transformation
上下文转换
typescript
const enrichContextMiddleware = t.middleware(async ({ ctx, next }) => {
const user = ctx.session?.user
? await ctx.db.user.findUnique({ where: { id: ctx.session.user.id } })
: null;
return next({
ctx: {
...ctx,
user, // Full user object
},
});
});typescript
const enrichContextMiddleware = t.middleware(async ({ ctx, next }) => {
const user = ctx.session?.user
? await ctx.db.user.findUnique({ where: { id: ctx.session.user.id } })
: null;
return next({
ctx: {
...ctx,
user, // 完整用户对象
},
});
});Error Handling
错误处理
TRPCError
TRPCError
typescript
import { TRPCError } from '@trpc/server';
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input } });
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `User ${input} not found`,
});
}
return user;
}),
});typescript
import { TRPCError } from '@trpc/server';
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input } });
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `用户 ${input} 不存在`,
});
}
return user;
}),
});Error Codes
错误码
| Code | HTTP Status | Use Case |
|---|---|---|
| 400 | Invalid input |
| 401 | Not authenticated |
| 403 | Not authorized |
| 404 | Resource not found |
| 408 | Request timeout |
| 409 | Resource conflict |
| 412 | Precondition failed |
| 413 | Request too large |
| 429 | Rate limit exceeded |
| 499 | Client closed connection |
| 500 | Server error |
| 错误码 | HTTP状态码 | 适用场景 |
|---|---|---|
| 400 | 输入无效 |
| 401 | 未认证 |
| 403 | 未授权 |
| 404 | 资源不存在 |
| 408 | 请求超时 |
| 409 | 资源冲突 |
| 412 | 前置条件失败 |
| 413 | 请求过大 |
| 429 | 超出限流 |
| 499 | 客户端关闭连接 |
| 500 | 服务器错误 |
Custom Error Handling
自定义错误处理
typescript
const router = t.router({
deleteUser: t.procedure
.input(z.string())
.mutation(async ({ input, ctx }) => {
try {
return await ctx.db.user.delete({ where: { id: input } });
} catch (error) {
if (error.code === 'P2025') { // Prisma not found
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
cause: error,
});
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to delete user',
cause: error,
});
}
}),
});typescript
const router = t.router({
deleteUser: t.procedure
.input(z.string())
.mutation(async ({ input, ctx }) => {
try {
return await ctx.db.user.delete({ where: { id: input } });
} catch (error) {
if (error.code === 'P2025') { // Prisma 未找到资源
throw new TRPCError({
code: 'NOT_FOUND',
message: '用户不存在',
cause: error,
});
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: '删除用户失败',
cause: error,
});
}
}),
});Error Formatting
错误格式化
typescript
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});typescript
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});Client-Side Error Handling
客户端错误处理
typescript
function MyComponent() {
const mutation = trpc.createUser.useMutation({
onError: (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
router.push('/login');
} else {
toast.error(error.message);
}
},
});
}typescript
function MyComponent() {
const mutation = trpc.createUser.useMutation({
onError: (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
router.push('/login');
} else {
toast.error(error.message);
}
},
});
}Client Setup
客户端配置
Vanilla Client
原生客户端
typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
});
// Usage
const user = await client.user.getById.query('123');
const newPost = await client.post.create.mutate({ title: 'Hello' });typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
});
// 使用示例
const user = await client.user.getById.query('123');
const newPost = await client.post.create.mutate({ title: 'Hello' });React Client Setup
React 客户端配置
typescript
// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers';
export const trpc = createTRPCReact<AppRouter>();
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from '../utils/trpc';
export default function App({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}typescript
// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers';
export const trpc = createTRPCReact<AppRouter>();
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from '../utils/trpc';
export default function App({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</trpc.Provider>
);
}Next.js API Route
Next.js API 路由
typescript
// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers';
import { createContext } from '../../../server/context';
export default createNextApiHandler({
router: appRouter,
createContext,
});typescript
// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers';
import { createContext } from '../../../server/context';
export default createNextApiHandler({
router: appRouter,
createContext,
});Headers & Authentication
请求头与认证
typescript
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
headers: async () => {
const token = await getAuthToken();
return {
authorization: token ? `Bearer ${token}` : undefined,
};
},
}),
],
});typescript
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
headers: async () => {
const token = await getAuthToken();
return {
authorization: token ? `Bearer ${token}` : undefined,
};
},
}),
],
});React Query Integration
React Query 集成
useQuery Hook
useQuery 钩子
typescript
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = trpc.user.getById.useQuery(userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data.name}</div>;
}typescript
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = trpc.user.getById.useQuery(userId);
if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return <div>{data.name}</div>;
}Query Options
查询选项
typescript
const { data } = trpc.posts.list.useQuery(undefined, {
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
onSuccess: (data) => console.log('Fetched', data.length, 'posts'),
});typescript
const { data } = trpc.posts.list.useQuery(undefined, {
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5分钟
cacheTime: 10 * 60 * 1000, // 10分钟
retry: 3,
onSuccess: (data) => console.log('获取到', data.length, '篇文章'),
});useMutation Hook
useMutation 钩子
typescript
function CreatePostForm() {
const utils = trpc.useContext();
const createPost = trpc.post.create.useMutation({
onSuccess: () => {
// Invalidate and refetch
utils.post.list.invalidate();
},
});
const handleSubmit = (data: { title: string }) => {
createPost.mutate(data);
};
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<button disabled={createPost.isLoading}>
{createPost.isLoading ? 'Creating...' : 'Create'}
</button>
{createPost.error && <p>{createPost.error.message}</p>}
</form>
);
}typescript
function CreatePostForm() {
const utils = trpc.useContext();
const createPost = trpc.post.create.useMutation({
onSuccess: () => {
// 使缓存失效并重新获取
utils.post.list.invalidate();
},
});
const handleSubmit = (data: { title: string }) => {
createPost.mutate(data);
};
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<button disabled={createPost.isLoading}>
{createPost.isLoading ? '创建中...' : '创建'}
</button>
{createPost.error && <p>{createPost.error.message}</p>}
</form>
);
}Optimistic Updates
乐观更新
typescript
const createPost = trpc.post.create.useMutation({
onMutate: async (newPost) => {
// Cancel outgoing refetches
await utils.post.list.cancel();
// Snapshot previous value
const previousPosts = utils.post.list.getData();
// Optimistically update
utils.post.list.setData(undefined, (old) => [
...(old ?? []),
{ id: 'temp', ...newPost },
]);
return { previousPosts };
},
onError: (err, newPost, context) => {
// Rollback on error
utils.post.list.setData(undefined, context?.previousPosts);
},
onSettled: () => {
// Refetch after success or error
utils.post.list.invalidate();
},
});typescript
const createPost = trpc.post.create.useMutation({
onMutate: async (newPost) => {
// 取消正在进行的重新获取
await utils.post.list.cancel();
// 快照之前的值
const previousPosts = utils.post.list.getData();
// 乐观更新
utils.post.list.setData(undefined, (old) => [
...(old ?? []),
{ id: 'temp', ...newPost },
]);
return { previousPosts };
},
onError: (err, newPost, context) => {
// 错误时回滚
utils.post.list.setData(undefined, context?.previousPosts);
},
onSettled: () => {
// 成功或错误后重新获取
utils.post.list.invalidate();
},
});Infinite Queries
无限查询
typescript
// Server
const router = t.router({
posts: t.procedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(10),
}))
.query(({ input }) => {
const posts = getPosts(input.cursor, input.limit);
return {
posts,
nextCursor: posts.length === input.limit ? input.cursor + input.limit : undefined,
};
}),
});
// Client
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = trpc.posts.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
return (
<div>
{data?.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}typescript
// 服务器端
const router = t.router({
posts: t.procedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(10),
}))
.query(({ input }) => {
const posts = getPosts(input.cursor, input.limit);
return {
posts,
nextCursor: posts.length === input.limit ? input.cursor + input.limit : undefined,
};
}),
});
// 客户端
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = trpc.posts.useInfiniteQuery(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
return (
<div>
{data?.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
加载更多
</button>
)}
</div>
);
}Next.js App Router Integration
Next.js App Router 集成
Server Components
服务器组件
typescript
// app/users/page.tsx (Server Component)
import { createCaller } from '../server/routers';
import { createContext } from '../server/context';
export default async function UsersPage() {
const ctx = await createContext({ req: null, res: null });
const caller = createCaller(ctx);
const users = await caller.user.list();
return (
<div>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}typescript
// app/users/page.tsx (服务器组件)
import { createCaller } from '../server/routers';
import { createContext } from '../server/context';
export default async function UsersPage() {
const ctx = await createContext({ req: null, res: null });
const caller = createCaller(ctx);
const users = await caller.user.list();
return (
<div>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}Server Actions
服务器操作
typescript
// app/actions.ts
'use server';
import { createCaller } from '../server/routers';
import { createContext } from '../server/context';
export async function createPost(formData: FormData) {
const ctx = await createContext({ req: null, res: null });
const caller = createCaller(ctx);
return caller.post.create({
title: formData.get('title') as string,
content: formData.get('content') as string,
});
}typescript
// app/actions.ts
'use server';
import { createCaller } from '../server/routers';
import { createContext } from '../server/context';
export async function createPost(formData: FormData) {
const ctx = await createContext({ req: null, res: null });
const caller = createCaller(ctx);
return caller.post.create({
title: formData.get('title') as string,
content: formData.get('content') as string,
});
}App Router Provider
App Router 提供者
typescript
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from './trpc';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}typescript
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from './trpc';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Client Components in App Router
App Router 中的客户端组件
typescript
// app/posts/create-button.tsx
'use client';
import { trpc } from '../trpc';
export function CreatePostButton() {
const createPost = trpc.post.create.useMutation();
return (
<button onClick={() => createPost.mutate({ title: 'New Post' })}>
Create Post
</button>
);
}typescript
// app/posts/create-button.tsx
'use client';
import { trpc } from '../trpc';
export function CreatePostButton() {
const createPost = trpc.post.create.useMutation();
return (
<button onClick={() => createPost.mutate({ title: 'New Post' })}>
创建文章
</button>
);
}API Route Handler (App Router)
API 路由处理器(App Router)
typescript
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '../../../../server/routers';
import { createContext } from '../../../../server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };typescript
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '../../../../server/routers';
import { createContext } from '../../../../server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };Real-time Subscriptions
实时订阅
WebSocket Setup (Server)
WebSocket 配置(服务器端)
typescript
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import ws from 'ws';
const wss = new ws.Server({ port: 3001 });
applyWSSHandler({
wss,
router: appRouter,
createContext,
});
console.log('WebSocket server listening on port 3001');typescript
import { applyWSSHandler } from '@trpc/server/adapters/ws';
import ws from 'ws';
const wss = new ws.Server({ port: 3001 });
applyWSSHandler({
wss,
router: appRouter,
createContext,
});
console.log('WebSocket 服务器监听端口3001');Subscription Procedure
订阅过程
typescript
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';
const ee = new EventEmitter();
const router = t.router({
onPostAdd: t.procedure.subscription(() => {
return observable<Post>((emit) => {
const onAdd = (data: Post) => emit.next(data);
ee.on('add', onAdd);
return () => {
ee.off('add', onAdd);
};
});
}),
createPost: t.procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => {
const post = { id: Date.now().toString(), ...input };
ee.emit('add', post); // Emit to subscribers
return post;
}),
});typescript
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';
const ee = new EventEmitter();
const router = t.router({
onPostAdd: t.procedure.subscription(() => {
return observable<Post>((emit) => {
const onAdd = (data: Post) => emit.next(data);
ee.on('add', onAdd);
return () => {
ee.off('add', onAdd);
};
});
}),
createPost: t.procedure
.input(z.object({ title: z.string() }))
.mutation(({ input }) => {
const post = { id: Date.now().toString(), ...input };
ee.emit('add', post); // 向订阅者发送事件
return post;
}),
});Client WebSocket Setup
客户端 WebSocket 配置
typescript
import { createWSClient, wsLink } from '@trpc/client';
const wsClient = createWSClient({
url: 'ws://localhost:3001',
});
const trpcClient = trpc.createClient({
links: [
wsLink({
client: wsClient,
}),
],
});typescript
import { createWSClient, wsLink } from '@trpc/client';
const wsClient = createWSClient({
url: 'ws://localhost:3001',
});
const trpcClient = trpc.createClient({
links: [
wsLink({
client: wsClient,
}),
],
});useSubscription Hook
useSubscription 钩子
typescript
function PostFeed() {
const [posts, setPosts] = useState<Post[]>([]);
trpc.onPostAdd.useSubscription(undefined, {
onData: (post) => {
setPosts((prev) => [post, ...prev]);
},
onError: (err) => {
console.error('Subscription error:', err);
},
});
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}typescript
function PostFeed() {
const [posts, setPosts] = useState<Post[]>([]);
trpc.onPostAdd.useSubscription(undefined, {
onData: (post) => {
setPosts((prev) => [post, ...prev]);
},
onError: (err) => {
console.error('订阅错误:', err);
},
});
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}Subscription with Input
带输入的订阅
typescript
// Server
const router = t.router({
onUserStatusChange: t.procedure
.input(z.string())
.subscription(({ input }) => {
return observable<UserStatus>((emit) => {
const onChange = (userId: string, status: UserStatus) => {
if (userId === input) {
emit.next(status);
}
};
ee.on('statusChange', onChange);
return () => ee.off('statusChange', onChange);
});
}),
});
// Client
trpc.onUserStatusChange.useSubscription('user-123', {
onData: (status) => console.log('Status:', status),
});typescript
// 服务器端
const router = t.router({
onUserStatusChange: t.procedure
.input(z.string())
.subscription(({ input }) => {
return observable<UserStatus>((emit) => {
const onChange = (userId: string, status: UserStatus) => {
if (userId === input) {
emit.next(status);
}
};
ee.on('statusChange', onChange);
return () => ee.off('statusChange', onChange);
});
}),
});
// 客户端
trpc.onUserStatusChange.useSubscription('user-123', {
onData: (status) => console.log('状态:', status),
});File Uploads
文件上传
Multipart Form Data (Server)
多部分表单数据(服务器端)
typescript
// Next.js API route with file upload
import { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const form = formidable({ multiples: false });
form.parse(req, async (err, fields, files) => {
if (err) return res.status(500).json({ error: 'Upload failed' });
const file = files.file as formidable.File;
const buffer = fs.readFileSync(file.filepath);
// Upload to S3, etc.
const url = await uploadToS3(buffer, file.originalFilename);
res.json({ url });
});
}typescript
// 支持文件上传的Next.js API路由
import { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const form = formidable({ multiples: false });
form.parse(req, async (err, fields, files) => {
if (err) return res.status(500).json({ error: '上传失败' });
const file = files.file as formidable.File;
const buffer = fs.readFileSync(file.filepath);
// 上传到S3等存储服务
const url = await uploadToS3(buffer, file.originalFilename);
res.json({ url });
});
}Base64 Upload (tRPC)
Base64 上传(tRPC)
typescript
// For small files only (<1MB)
const router = t.router({
uploadAvatar: t.procedure
.input(z.object({
fileName: z.string(),
fileData: z.string(), // Base64
}))
.mutation(async ({ input }) => {
const buffer = Buffer.from(input.fileData, 'base64');
const url = await uploadToS3(buffer, input.fileName);
return { url };
}),
});
// Client
const uploadAvatar = trpc.uploadAvatar.useMutation();
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
uploadAvatar.mutate({
fileName: file.name,
fileData: base64.split(',')[1], // Remove data:image/...;base64,
});
};
reader.readAsDataURL(file);
};typescript
// 仅适用于小文件(<1MB)
const router = t.router({
uploadAvatar: t.procedure
.input(z.object({
fileName: z.string(),
fileData: z.string(), // Base64格式
}))
.mutation(async ({ input }) => {
const buffer = Buffer.from(input.fileData, 'base64');
const url = await uploadToS3(buffer, input.fileName);
return { url };
}),
});
// 客户端
const uploadAvatar = trpc.uploadAvatar.useMutation();
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
uploadAvatar.mutate({
fileName: file.name,
fileData: base64.split(',')[1], // 移除data:image/...;base64,前缀
});
};
reader.readAsDataURL(file);
};Signed URL Pattern (Recommended)
签名URL模式(推荐)
typescript
// Step 1: Get signed upload URL from tRPC
const router = t.router({
getUploadUrl: t.procedure
.input(z.object({
fileName: z.string(),
fileType: z.string(),
}))
.mutation(async ({ input }) => {
const signedUrl = await s3.getSignedUrl('putObject', {
Bucket: 'my-bucket',
Key: input.fileName,
ContentType: input.fileType,
Expires: 60, // 1 minute
});
return { uploadUrl: signedUrl, fileUrl: `https://cdn.example.com/${input.fileName}` };
}),
});
// Step 2: Client uploads directly to S3
async function uploadFile(file: File) {
// Get signed URL
const { uploadUrl, fileUrl } = await trpc.getUploadUrl.mutate({
fileName: file.name,
fileType: file.type,
});
// Upload directly to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
// Save file URL to database via tRPC
await trpc.user.updateAvatar.mutate({ url: fileUrl });
}typescript
// 步骤1:从tRPC获取签名上传URL
const router = t.router({
getUploadUrl: t.procedure
.input(z.object({
fileName: z.string(),
fileType: z.string(),
}))
.mutation(async ({ input }) => {
const signedUrl = await s3.getSignedUrl('putObject', {
Bucket: 'my-bucket',
Key: input.fileName,
ContentType: input.fileType,
Expires: 60, // 1分钟
});
return { uploadUrl: signedUrl, fileUrl: `https://cdn.example.com/${input.fileName}` };
}),
});
// 步骤2:客户端直接上传到S3
async function uploadFile(file: File) {
// 获取签名URL
const { uploadUrl, fileUrl } = await trpc.getUploadUrl.mutate({
fileName: file.name,
fileType: file.type,
});
// 直接上传到S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
// 通过tRPC将文件URL保存到数据库
await trpc.user.updateAvatar.mutate({ url: fileUrl });
}Batch Requests & Data Loaders
批量请求与数据加载器
Automatic Batching
自动批量处理
typescript
// Client configuration
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
maxBatchSize: 10, // Batch up to 10 requests
}),
],
});
// Multiple calls made close together are batched into one HTTP request
const user1 = trpc.user.getById.useQuery('1');
const user2 = trpc.user.getById.useQuery('2');
const user3 = trpc.user.getById.useQuery('3');
// → Single HTTP request with 3 procedure callstypescript
// 客户端配置
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
maxBatchSize: 10, // 最多批量处理10个请求
}),
],
});
// 短时间内的多个调用会被批量处理为单个HTTP请求
const user1 = trpc.user.getById.useQuery('1');
const user2 = trpc.user.getById.useQuery('2');
const user3 = trpc.user.getById.useQuery('3');
// → 单个HTTP请求包含3个过程调用DataLoader Pattern
DataLoader 模式
typescript
import DataLoader from 'dataloader';
// Create DataLoader in context
export async function createContext() {
const userLoader = new DataLoader(async (ids: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
// Return in same order as input
return ids.map((id) => users.find((u) => u.id === id));
});
return { userLoader };
}
// Use in procedures
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(({ ctx, input }) => {
return ctx.userLoader.load(input); // Batched!
}),
getPosts: t.procedure.query(async ({ ctx }) => {
const posts = await db.post.findMany({ take: 10 });
// N+1 problem solved—all authors fetched in one query
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
...post,
author: await ctx.userLoader.load(post.authorId),
}))
);
return postsWithAuthors;
}),
});typescript
import DataLoader from 'dataloader';
// 在上下文中创建DataLoader
export async function createContext() {
const userLoader = new DataLoader(async (ids: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
// 按输入顺序返回结果
return ids.map((id) => users.find((u) => u.id === id));
});
return { userLoader };
}
// 在过程中使用
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(({ ctx, input }) => {
return ctx.userLoader.load(input); // 已批量处理!
}),
getPosts: t.procedure.query(async ({ ctx }) => {
const posts = await db.post.findMany({ take: 10 });
// 解决N+1问题——所有作者通过一次查询获取
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
...post,
author: await ctx.userLoader.load(post.authorId),
}))
);
return postsWithAuthors;
}),
});Conditional Batching
条件批量处理
typescript
import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
const trpcClient = trpc.createClient({
links: [
splitLink({
// Batch queries, don't batch mutations
condition: (op) => op.type === 'query',
true: httpBatchLink({ url: '/api/trpc' }),
false: httpLink({ url: '/api/trpc' }),
}),
],
});typescript
import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
const trpcClient = trpc.createClient({
links: [
splitLink({
// 对查询进行批量处理,不对变更进行批量处理
condition: (op) => op.type === 'query',
true: httpBatchLink({ url: '/api/trpc' }),
false: httpLink({ url: '/api/trpc' }),
}),
],
});TypeScript Inference Patterns
TypeScript 推断模式
Inferring Types from Router
从路由推断类型
typescript
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';
// Input types
type RouterInputs = inferRouterInputs<AppRouter>;
type CreateUserInput = RouterInputs['user']['create'];
// Output types
type RouterOutputs = inferRouterOutputs<AppRouter>;
type User = RouterOutputs['user']['getById'];
// Use in components
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}typescript
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from './server';
// 输入类型
type RouterInputs = inferRouterInputs<AppRouter>;
type CreateUserInput = RouterInputs['user']['create'];
// 输出类型
type RouterOutputs = inferRouterOutputs<AppRouter>;
type User = RouterOutputs['user']['getById'];
// 在组件中使用
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}Procedure Helpers
过程助手类型
typescript
import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';
type CreatePostInput = inferProcedureInput<AppRouter['post']['create']>;
type Post = inferProcedureOutput<AppRouter['post']['getById']>;typescript
import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';
type CreatePostInput = inferProcedureInput<AppRouter['post']['create']>;
type Post = inferProcedureOutput<AppRouter['post']['getById']>;Context Type Inference
上下文类型推断
typescript
import { inferAsyncReturnType } from '@trpc/server';
export async function createContext() {
return {
db: prisma,
user: null as User | null,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();typescript
import { inferAsyncReturnType } from '@trpc/server';
export async function createContext() {
return {
db: prisma,
user: null as User | null,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
const t = initTRPC.context<Context>().create();Generic Procedures
通用过程
typescript
// Reusable pagination
function createPaginatedProcedure<T>(
getData: (cursor: number, limit: number) => Promise<T[]>
) {
return t.procedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(10),
}))
.query(async ({ input }) => {
const items = await getData(input.cursor ?? 0, input.limit);
return {
items,
nextCursor: items.length === input.limit
? (input.cursor ?? 0) + input.limit
: undefined,
};
});
}
const router = t.router({
posts: createPaginatedProcedure((cursor, limit) =>
db.post.findMany({ skip: cursor, take: limit })
),
users: createPaginatedProcedure((cursor, limit) =>
db.user.findMany({ skip: cursor, take: limit })
),
});typescript
// 可复用的分页过程
function createPaginatedProcedure<T>(
getData: (cursor: number, limit: number) => Promise<T[]>
) {
return t.procedure
.input(z.object({
cursor: z.number().optional(),
limit: z.number().default(10),
}))
.query(async ({ input }) => {
const items = await getData(input.cursor ?? 0, input.limit);
return {
items,
nextCursor: items.length === input.limit
? (input.cursor ?? 0) + input.limit
: undefined,
};
});
}
const router = t.router({
posts: createPaginatedProcedure((cursor, limit) =>
db.post.findMany({ skip: cursor, take: limit })
),
users: createPaginatedProcedure((cursor, limit) =>
db.user.findMany({ skip: cursor, take: limit })
),
});Testing Strategies
测试策略
Unit Testing Procedures
单元测试过程
typescript
import { createCaller } from '../routers';
describe('User Router', () => {
it('should create user', async () => {
const ctx = {
db: mockDb,
session: null,
};
const caller = createCaller(ctx);
const result = await caller.user.create({
name: 'Alice',
email: 'alice@example.com',
});
expect(result).toMatchObject({
name: 'Alice',
email: 'alice@example.com',
});
});
});typescript
import { createCaller } from '../routers';
describe('用户路由', () => {
it('应创建用户', async () => {
const ctx = {
db: mockDb,
session: null,
};
const caller = createCaller(ctx);
const result = await caller.user.create({
name: 'Alice',
email: 'alice@example.com',
});
expect(result).toMatchObject({
name: 'Alice',
email: 'alice@example.com',
});
});
});Integration Testing
集成测试
typescript
import { httpBatchLink } from '@trpc/client';
import { createTRPCProxyClient } from '@trpc/client';
import type { AppRouter } from '../server';
describe('tRPC Integration', () => {
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
});
it('should fetch user', async () => {
const user = await client.user.getById.query('123');
expect(user.id).toBe('123');
});
});typescript
import { httpBatchLink } from '@trpc/client';
import { createTRPCProxyClient } from '@trpc/client';
import type { AppRouter } from '../server';
describe('tRPC 集成测试', () => {
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
});
it('应获取用户', async () => {
const user = await client.user.getById.query('123');
expect(user.id).toBe('123');
});
});Mocking Context
模拟上下文
typescript
import { createCaller } from '../routers';
const mockContext = {
db: {
user: {
findUnique: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
create: vi.fn(),
},
},
session: {
user: { id: '1', email: 'alice@example.com' },
},
};
it('should get current user', async () => {
const caller = createCaller(mockContext);
const user = await caller.user.getMe();
expect(mockContext.db.user.findUnique).toHaveBeenCalledWith({
where: { id: '1' },
});
expect(user.name).toBe('Alice');
});typescript
import { createCaller } from '../routers';
const mockContext = {
db: {
user: {
findUnique: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
create: vi.fn(),
},
},
session: {
user: { id: '1', email: 'alice@example.com' },
},
};
it('应获取当前用户', async () => {
const caller = createCaller(mockContext);
const user = await caller.user.getMe();
expect(mockContext.db.user.findUnique).toHaveBeenCalledWith({
where: { id: '1' },
});
expect(user.name).toBe('Alice');
});Testing React Hooks
测试 React 钩子
typescript
import { renderHook, waitFor } from '@testing-library/react';
import { createWrapper } from './test-utils';
it('should fetch posts', async () => {
const { result } = renderHook(() => trpc.post.list.useQuery(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toHaveLength(10);
});
// test-utils.ts
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../utils/trpc';
export function createWrapper() {
const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
});
return ({ children }) => (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}typescript
import { renderHook, waitFor } from '@testing-library/react';
import { createWrapper } from './test-utils';
it('应获取文章列表', async () => {
const { result } = renderHook(() => trpc.post.list.useQuery(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toHaveLength(10);
});
// test-utils.ts
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../utils/trpc';
export function createWrapper() {
const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
});
return ({ children }) => (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</trpc.Provider>
);
}Production Patterns
生产环境实践
Error Monitoring
错误监控
typescript
import * as Sentry from '@sentry/node';
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
// Log to Sentry
if (error.code === 'INTERNAL_SERVER_ERROR') {
Sentry.captureException(error);
}
return {
...shape,
data: {
...shape.data,
// Don't expose internal errors in production
message: process.env.NODE_ENV === 'production' && error.code === 'INTERNAL_SERVER_ERROR'
? 'Internal server error'
: shape.message,
},
};
},
});typescript
import * as Sentry from '@sentry/node';
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
// 记录到Sentry
if (error.code === 'INTERNAL_SERVER_ERROR') {
Sentry.captureException(error);
}
return {
...shape,
data: {
...shape.data,
// 生产环境不暴露内部错误信息
message: process.env.NODE_ENV === 'production' && error.code === 'INTERNAL_SERVER_ERROR'
? '内部服务器错误'
: shape.message,
},
};
},
});Rate Limiting
限流
typescript
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
const identifier = ctx.session?.user?.id ?? ctx.req.ip;
const { success } = await ratelimit.limit(identifier);
if (!success) {
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: 'Rate limit exceeded',
});
}
return next();
});typescript
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
const identifier = ctx.session?.user?.id ?? ctx.req.ip;
const { success } = await ratelimit.limit(identifier);
if (!success) {
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: '请求过于频繁,请稍后再试',
});
}
return next();
});Caching
缓存
typescript
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
// Check cache
const cached = await redis.get(`user:${input}`);
if (cached) return JSON.parse(cached);
// Fetch from database
const user = await db.user.findUnique({ where: { id: input } });
// Cache for 5 minutes
await redis.setex(`user:${input}`, 300, JSON.stringify(user));
return user;
}),
});typescript
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const router = t.router({
getUser: t.procedure
.input(z.string())
.query(async ({ input }) => {
// 检查缓存
const cached = await redis.get(`user:${input}`);
if (cached) return JSON.parse(cached);
// 从数据库获取
const user = await db.user.findUnique({ where: { id: input } });
// 缓存5分钟
await redis.setex(`user:${input}`, 300, JSON.stringify(user));
return user;
}),
});Request Logging
请求日志
typescript
const loggingMiddleware = t.middleware(async ({ path, type, next, input }) => {
const start = Date.now();
console.log(`→ ${type} ${path}`, { input });
try {
const result = await next();
const duration = Date.now() - start;
console.log(`✓ ${type} ${path} - ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(`✗ ${type} ${path} - ${duration}ms`, { error });
throw error;
}
});typescript
const loggingMiddleware = t.middleware(async ({ path, type, next, input }) => {
const start = Date.now();
console.log(`→ ${type} ${path}`, { input });
try {
const result = await next();
const duration = Date.now() - start;
console.log(`✓ ${type} ${path} - ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(`✗ ${type} ${path} - ${duration}ms`, { error });
throw error;
}
});OpenTelemetry Integration
OpenTelemetry 集成
typescript
import { trace } from '@opentelemetry/api';
const tracingMiddleware = t.middleware(async ({ path, type, next }) => {
const tracer = trace.getTracer('trpc');
return tracer.startActiveSpan(`trpc.${type}.${path}`, async (span) => {
try {
const result = await next();
span.setStatus({ code: 0 }); // OK
return result;
} catch (error) {
span.setStatus({ code: 2, message: error.message }); // ERROR
span.recordException(error);
throw error;
} finally {
span.end();
}
});
});typescript
import { trace } from '@opentelemetry/api';
const tracingMiddleware = t.middleware(async ({ path, type, next }) => {
const tracer = trace.getTracer('trpc');
return tracer.startActiveSpan(`trpc.${type}.${path}`, async (span) => {
try {
const result = await next();
span.setStatus({ code: 0 }); // 成功
return result;
} catch (error) {
span.setStatus({ code: 2, message: error.message }); // 错误
span.recordException(error);
throw error;
} finally {
span.end();
}
});
});Comparison with REST & GraphQL
与REST和GraphQL的对比
Feature Comparison
功能对比
| Feature | tRPC | REST | GraphQL |
|---|---|---|---|
| Type Safety | Full (TypeScript) | Manual/codegen | Manual/codegen |
| Code Generation | None | Optional (OpenAPI) | Required |
| Learning Curve | Low | Low | Medium/High |
| Client Libraries | TypeScript only | Any language | Any language |
| API Documentation | TypeScript types | OpenAPI/Swagger | Schema/introspection |
| Public APIs | ❌ No | ✅ Yes | ✅ Yes |
| Flexible Queries | ❌ Fixed | ❌ Fixed | ✅ Yes |
| Overfetching | Minimal | Common | None |
| Caching | React Query | HTTP caching | Complex |
| Real-time | WebSocket | SSE/WebSocket | Subscriptions |
| File Uploads | Workarounds | Native | Complex |
| 功能 | tRPC | REST | GraphQL |
|---|---|---|---|
| 类型安全 | 完整(TypeScript) | 手动/代码生成 | 手动/代码生成 |
| 代码生成 | 无需 | 可选(OpenAPI) | 必须 |
| 学习曲线 | 低 | 低 | 中/高 |
| 客户端库 | 仅TypeScript | 任意语言 | 任意语言 |
| API文档 | TypeScript类型 | OpenAPI/Swagger | Schema/自省 |
| 公共API | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| 灵活查询 | ❌ 固定 | ❌ 固定 | ✅ 支持 |
| 过度获取 | 极少 | 常见 | 无 |
| 缓存 | React Query | HTTP缓存 | 复杂 |
| 实时能力 | WebSocket | SSE/WebSocket | 订阅 |
| 文件上传 | 需变通方案 | 原生支持 | 复杂 |
When to Choose Each
选型建议
tRPC:
- ✅ Full-stack TypeScript monorepo
- ✅ Internal tools and dashboards
- ✅ Next.js applications
- ✅ Rapid development with small teams
- ❌ Public APIs for external consumers
- ❌ Multi-language clients
REST:
- ✅ Public APIs with broad compatibility
- ✅ Multi-language services
- ✅ HTTP caching requirements
- ✅ File uploads and downloads
- ❌ Complex nested data structures
- ❌ Need for type safety without codegen
GraphQL:
- ✅ Complex data graphs
- ✅ Multiple client types (web, mobile, etc.)
- ✅ Need for flexible queries
- ✅ Avoiding overfetching
- ❌ Simple CRUD operations
- ❌ Small teams (complexity overhead)
tRPC:
- ✅ 全栈TypeScript单仓项目
- ✅ 内部工具与仪表盘
- ✅ Next.js应用
- ✅ 小团队快速开发
- ❌ 面向外部消费者的公共API
- ❌ 多语言客户端
REST:
- ✅ 兼容性广的公共API
- ✅ 多语言服务
- ✅ 需要HTTP缓存
- ✅ 文件上传与下载
- ❌ 复杂嵌套数据结构
- ❌ 无需代码生成的类型安全需求
GraphQL:
- ✅ 复杂数据图
- ✅ 多客户端类型(Web、移动等)
- ✅ 需要灵活查询
- ✅ 避免过度获取
- ❌ 简单CRUD操作
- ❌ 小团队(复杂度开销大)
Migration Path
迁移路径
tRPC can coexist with REST/GraphQL:
typescript
// Use tRPC for internal, REST for public
const router = t.router({
internal: internalRouter, // tRPC only
});
// Expose REST endpoints separately
app.get('/api/public/users', publicRestHandler);tRPC 可以与REST/GraphQL共存:
typescript
// 内部接口使用tRPC,公共接口使用REST
const router = t.router({
internal: internalRouter, // 仅tRPC
});
// 单独暴露REST端点
app.get('/api/public/users', publicRestHandler);Migration from REST
从REST迁移
Gradual Migration Strategy
渐进式迁移策略
- Add tRPC alongside REST: Don't rewrite everything at once
- New features in tRPC: Start with new endpoints
- Migrate high-value endpoints: Focus on complex or frequently used APIs
- Keep public APIs in REST: Only migrate internal consumption
- 在REST旁添加tRPC:不要一次性重写所有代码
- 新功能使用tRPC:从新端点开始
- 迁移高价值端点:聚焦复杂或高频使用的API
- 公共API保留REST:仅迁移内部消费的接口
Converting REST to tRPC
将REST转换为tRPC
Before (REST):
typescript
// pages/api/users/[id].ts
export default async function handler(req, res) {
if (req.method === 'GET') {
const user = await db.user.findUnique({ where: { id: req.query.id } });
res.json(user);
} else if (req.method === 'PATCH') {
const user = await db.user.update({
where: { id: req.query.id },
data: req.body,
});
res.json(user);
}
}
// Client
const response = await fetch(`/api/users/${id}`);
const user = await response.json(); // No types!After (tRPC):
typescript
// server/routers/user.ts
export const userRouter = t.router({
getById: t.procedure
.input(z.string())
.query(({ input }) => db.user.findUnique({ where: { id: input } })),
update: t.procedure
.input(z.object({
id: z.string(),
data: z.object({ name: z.string().optional() }),
}))
.mutation(({ input }) => db.user.update({
where: { id: input.id },
data: input.data,
})),
});
// Client
const user = await trpc.user.getById.query(id); // Fully typed!转换前(REST):
typescript
// pages/api/users/[id].ts
export default async function handler(req, res) {
if (req.method === 'GET') {
const user = await db.user.findUnique({ where: { id: req.query.id } });
res.json(user);
} else if (req.method === 'PATCH') {
const user = await db.user.update({
where: { id: req.query.id },
data: req.body,
});
res.json(user);
}
}
// 客户端
const response = await fetch(`/api/users/${id}`);
const user = await response.json(); // 无类型!转换后(tRPC):
typescript
// server/routers/user.ts
export const userRouter = t.router({
getById: t.procedure
.input(z.string())
.query(({ input }) => db.user.findUnique({ where: { id: input } })),
update: t.procedure
.input(z.object({
id: z.string(),
data: z.object({ name: z.string().optional() }),
}))
.mutation(({ input }) => db.user.update({
where: { id: input.id },
data: input.data,
})),
});
// 客户端
const user = await trpc.user.getById.query(id); // 完全类型化!Shared Validation
共享验证规则
typescript
// Reuse Zod schemas across REST and tRPC during migration
import { createUserSchema } from '../schemas/user';
// tRPC
const router = t.router({
createUser: t.procedure
.input(createUserSchema)
.mutation(({ input }) => createUser(input)),
});
// REST (validate with same schema)
export default async function handler(req, res) {
const parsed = createUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error });
}
const user = await createUser(parsed.data);
res.json(user);
}typescript
// 迁移期间在REST和tRPC之间复用Zod schema
import { createUserSchema } from '../schemas/user';
// tRPC
const router = t.router({
createUser: t.procedure
.input(createUserSchema)
.mutation(({ input }) => createUser(input)),
});
// REST(使用相同schema验证)
export default async function handler(req, res) {
const parsed = createUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error });
}
const user = await createUser(parsed.data);
res.json(user);
}Best Practices & Performance
最佳实践与性能优化
Code Organization
代码组织
server/
├── trpc.ts # tRPC instance, base procedures
├── context.ts # Context creation
├── middleware/
│ ├── auth.ts # Authentication middleware
│ ├── logging.ts # Logging middleware
│ └── rateLimit.ts # Rate limiting
├── routers/
│ ├── _app.ts # Root router
│ ├── user.ts # User procedures
│ ├── post.ts # Post procedures
│ └── admin/
│ └── index.ts # Admin-only procedures
└── schemas/
├── user.ts # User Zod schemas
└── post.ts # Post Zod schemasserver/
├── trpc.ts # tRPC实例、基础过程
├── context.ts # 上下文创建
├── middleware/
│ ├── auth.ts # 认证中间件
│ ├── logging.ts # 日志中间件
│ └── rateLimit.ts # 限流中间件
├── routers/
│ ├── _app.ts # 根路由
│ ├── user.ts # 用户相关过程
│ ├── post.ts # 文章相关过程
│ └── admin/
│ └── index.ts # 仅管理员可用的过程
└── schemas/
├── user.ts # 用户Zod schema
└── post.ts # 文章Zod schemaPerformance Tips
性能优化技巧
-
Use batching for multiple queries:typescript
httpBatchLink({ url: '/api/trpc', maxBatchSize: 10 }) -
Implement DataLoader for N+1 queries:typescript
const userLoader = new DataLoader(batchLoadUsers); -
Cache expensive queries:typescript
trpc.posts.list.useQuery(undefined, { staleTime: 5 * 60 * 1000 }); -
Optimize database queries:typescript
// ❌ Bad: N+1 query const posts = await db.post.findMany(); const postsWithAuthors = await Promise.all( posts.map((p) => db.user.findUnique({ where: { id: p.authorId } })) ); // ✅ Good: Single query with include const posts = await db.post.findMany({ include: { author: true }, }); -
Use React Query's deduplication:typescript
// Multiple components can call same query—React Query deduplicates const { data } = trpc.user.getMe.useQuery();
-
对多个查询使用批量处理:typescript
httpBatchLink({ url: '/api/trpc', maxBatchSize: 10 }) -
使用DataLoader解决N+1查询问题:typescript
const userLoader = new DataLoader(batchLoadUsers); -
缓存开销大的查询:typescript
trpc.posts.list.useQuery(undefined, { staleTime: 5 * 60 * 1000 }); -
优化数据库查询:typescript
// ❌ 不良实践:N+1查询 const posts = await db.post.findMany(); const postsWithAuthors = await Promise.all( posts.map((p) => db.user.findUnique({ where: { id: p.authorId } })) ); // ✅ 推荐:单次查询包含关联数据 const posts = await db.post.findMany({ include: { author: true }, }); -
使用React Query的请求去重:typescript
// 多个组件可调用同一查询——React Query会自动去重 const { data } = trpc.user.getMe.useQuery();
Security Best Practices
安全最佳实践
- Always validate input with Zod
- Use middleware for authentication:
typescript
const protectedProcedure = t.procedure.use(isAuthed); - Sanitize error messages in production
- Implement rate limiting
- Use HTTPS in production
- Set CORS properly:
typescript
createNextApiHandler({ router: appRouter, createContext, onError: ({ error }) => { if (error.code === 'INTERNAL_SERVER_ERROR') { console.error('Internal error:', error); } }, });
- 始终使用Zod验证输入
- 使用中间件处理认证:
typescript
const protectedProcedure = t.procedure.use(isAuthed); - 生产环境中 sanitize 错误信息
- 实现限流
- 生产环境使用HTTPS
- 正确配置CORS:
typescript
createNextApiHandler({ router: appRouter, createContext, onError: ({ error }) => { if (error.code === 'INTERNAL_SERVER_ERROR') { console.error('内部错误:', error); } }, });
Type Safety Tips
类型安全技巧
-
Export router type, not implementation:typescript
export type AppRouter = typeof appRouter; // ✅ // Don't export `appRouter` itself to client -
Usefor better inference:
satisfiestypescriptconst input = { name: 'Alice', age: 30, } satisfies CreateUserInput; -
Avoidin context:
anytypescript// ❌ Bad ctx: { user: any } // ✅ Good ctx: { user: User | null }
-
导出路由类型,而非实现:typescript
export type AppRouter = typeof appRouter; // ✅ 推荐 // 不要向客户端导出`appRouter`本身 -
使用提升推断效果:
satisfiestypescriptconst input = { name: 'Alice', age: 30, } satisfies CreateUserInput; -
避免在上下文中使用:
anytypescript// ❌ 不良实践 ctx: { user: any } // ✅ 推荐 ctx: { user: User | null }
Development Workflow
开发工作流
- Define schema first: Write Zod schemas before procedures
- Test procedures in isolation: Use for unit tests
createCaller - Use TypeScript strict mode: Catch type errors early
- Enable React Query DevTools:
typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; <ReactQueryDevtools initialIsOpen={false} />
- 先定义schema:在编写过程前先写Zod schema
- 独立测试过程:使用进行单元测试
createCaller - 启用TypeScript严格模式:提前捕获类型错误
- 启用React Query DevTools:
typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; <ReactQueryDevtools initialIsOpen={false} />
Common Pitfalls
常见陷阱
❌ Don't return sensitive data:
typescript
// Bad: Exposes password hash
.query(() => db.user.findMany())
// Good: Select specific fields
.query(() => db.user.findMany({ select: { id: true, name: true } }))❌ Don't use mutations for reads:
typescript
// Bad: Side-effect-free operation as mutation
getMostRecentPost: t.procedure.mutation(() => getPost())
// Good: Use query for reads
getMostRecentPost: t.procedure.query(() => getPost())❌ Don't skip input validation:
typescript
// Bad: No validation
.input(z.any())
// Good: Strict validation
.input(z.object({ id: z.string().uuid() }))❌ 不要返回敏感数据:
typescript
// 不良实践:暴露密码哈希
.query(() => db.user.findMany())
// 推荐:仅返回指定字段
.query(() => db.user.findMany({ select: { id: true, name: true } }))❌ 不要对读取操作使用变更过程:
typescript
// 不良实践:无副作用操作使用mutation
getMostRecentPost: t.procedure.mutation(() => getPost())
// 推荐:读取操作使用query
getMostRecentPost: t.procedure.query(() => getPost())❌ 不要跳过输入验证:
typescript
// 不良实践:无验证
.input(z.any())
// 推荐:严格验证
.input(z.object({ id: z.string().uuid() }))Monitoring & Observability
监控与可观测性
typescript
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
// Log metrics
metrics.increment('trpc.error', { code: error.code });
// Send to error tracking
if (error.code === 'INTERNAL_SERVER_ERROR') {
Sentry.captureException(error);
}
return shape;
},
});
const loggingMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
// Log performance metrics
metrics.timing('trpc.duration', Date.now() - start, { path, type });
return result;
});typescript
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
// 记录指标
metrics.increment('trpc.error', { code: error.code });
// 发送到错误追踪系统
if (error.code === 'INTERNAL_SERVER_ERROR') {
Sentry.captureException(error);
}
return shape;
},
});
const loggingMiddleware = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
// 记录性能指标
metrics.timing('trpc.duration', Date.now() - start, { path, type });
return result;
});Summary
总结
tRPC enables type-safe APIs with minimal boilerplate:
- ✅ No code generation: Types inferred from TypeScript
- ✅ React Query integration: Built-in caching and optimistic updates
- ✅ Next.js first-class support: App Router, Server Components
- ✅ Developer experience: Auto-complete, refactoring, type errors
Best for: Full-stack TypeScript apps, Next.js projects, internal tools
Avoid for: Public APIs, multi-language services
Get Started: Install → Define router → Use in client → Enjoy type safety!
Related Skills: Zod (validation), React Query (caching), Next.js (integration)
tRPC 用极少的样板代码实现类型安全API:
- ✅ 无需代码生成:从TypeScript推断类型
- ✅ React Query集成:内置缓存与乐观更新
- ✅ Next.js一等支持:App Router、服务器组件
- ✅ 开发者体验:自动补全、重构、类型错误提示
最佳适用场景:全栈TypeScript应用、Next.js项目、内部工具
不适用场景:公共API、多语言服务
快速开始:安装 → 定义路由 → 在客户端使用 → 享受类型安全!
相关技术:Zod(验证)、React Query(缓存)、Next.js(集成)