trpc-type-safety

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

tRPC - 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
undefined
bash
undefined

Server 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
undefined
npm install @trpc/client @trpc/react-query @tanstack/react-query
undefined

Define 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

核心组件

  1. Router: Collection of procedures (API endpoints)
  2. Procedure: Single API operation (query or mutation)
  3. Context: Request-scoped data (user, database, etc.)
  4. Middleware: Intercept/modify requests (auth, logging)
  5. Input/Output: Validated with Zod schemas
  1. 路由(Router):过程的集合(即API端点)
  2. 过程(Procedure):单个API操作(查询或变更)
  3. 上下文(Context):请求作用域的数据(用户信息、数据库连接等)
  4. 中间件(Middleware):拦截或修改请求(鉴权、日志等)
  5. 输入/输出:使用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 type

server/
├── 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 变更

AspectQueryMutation
PurposeRead dataModify data
HTTP MethodGETPOST
CachingCached by React QueryNot cached
IdempotentYesNo
Side EffectsNoneDatabase writes, emails, etc.
维度查询变更
用途读取数据修改数据
HTTP方法GETPOST
缓存由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

错误码

CodeHTTP StatusUse Case
BAD_REQUEST
400Invalid input
UNAUTHORIZED
401Not authenticated
FORBIDDEN
403Not authorized
NOT_FOUND
404Resource not found
TIMEOUT
408Request timeout
CONFLICT
409Resource conflict
PRECONDITION_FAILED
412Precondition failed
PAYLOAD_TOO_LARGE
413Request too large
TOO_MANY_REQUESTS
429Rate limit exceeded
CLIENT_CLOSED_REQUEST
499Client closed connection
INTERNAL_SERVER_ERROR
500Server error
错误码HTTP状态码适用场景
BAD_REQUEST
400输入无效
UNAUTHORIZED
401未认证
FORBIDDEN
403未授权
NOT_FOUND
404资源不存在
TIMEOUT
408请求超时
CONFLICT
409资源冲突
PRECONDITION_FAILED
412前置条件失败
PAYLOAD_TOO_LARGE
413请求过大
TOO_MANY_REQUESTS
429超出限流
CLIENT_CLOSED_REQUEST
499客户端关闭连接
INTERNAL_SERVER_ERROR
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 calls
typescript
// 客户端配置
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

功能对比

FeaturetRPCRESTGraphQL
Type SafetyFull (TypeScript)Manual/codegenManual/codegen
Code GenerationNoneOptional (OpenAPI)Required
Learning CurveLowLowMedium/High
Client LibrariesTypeScript onlyAny languageAny language
API DocumentationTypeScript typesOpenAPI/SwaggerSchema/introspection
Public APIs❌ No✅ Yes✅ Yes
Flexible Queries❌ Fixed❌ Fixed✅ Yes
OverfetchingMinimalCommonNone
CachingReact QueryHTTP cachingComplex
Real-timeWebSocketSSE/WebSocketSubscriptions
File UploadsWorkaroundsNativeComplex
功能tRPCRESTGraphQL
类型安全完整(TypeScript)手动/代码生成手动/代码生成
代码生成无需可选(OpenAPI)必须
学习曲线中/高
客户端库仅TypeScript任意语言任意语言
API文档TypeScript类型OpenAPI/SwaggerSchema/自省
公共API❌ 不支持✅ 支持✅ 支持
灵活查询❌ 固定❌ 固定✅ 支持
过度获取极少常见
缓存React QueryHTTP缓存复杂
实时能力WebSocketSSE/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

渐进式迁移策略

  1. Add tRPC alongside REST: Don't rewrite everything at once
  2. New features in tRPC: Start with new endpoints
  3. Migrate high-value endpoints: Focus on complex or frequently used APIs
  4. Keep public APIs in REST: Only migrate internal consumption
  1. 在REST旁添加tRPC:不要一次性重写所有代码
  2. 新功能使用tRPC:从新端点开始
  3. 迁移高价值端点:聚焦复杂或高频使用的API
  4. 公共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 schemas
server/
├── 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 schema

Performance Tips

性能优化技巧

  1. Use batching for multiple queries:
    typescript
    httpBatchLink({ url: '/api/trpc', maxBatchSize: 10 })
  2. Implement DataLoader for N+1 queries:
    typescript
    const userLoader = new DataLoader(batchLoadUsers);
  3. Cache expensive queries:
    typescript
    trpc.posts.list.useQuery(undefined, { staleTime: 5 * 60 * 1000 });
  4. 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 },
    });
  5. Use React Query's deduplication:
    typescript
    // Multiple components can call same query—React Query deduplicates
    const { data } = trpc.user.getMe.useQuery();
  1. 对多个查询使用批量处理:
    typescript
    httpBatchLink({ url: '/api/trpc', maxBatchSize: 10 })
  2. 使用DataLoader解决N+1查询问题:
    typescript
    const userLoader = new DataLoader(batchLoadUsers);
  3. 缓存开销大的查询:
    typescript
    trpc.posts.list.useQuery(undefined, { staleTime: 5 * 60 * 1000 });
  4. 优化数据库查询:
    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 },
    });
  5. 使用React Query的请求去重:
    typescript
    // 多个组件可调用同一查询——React Query会自动去重
    const { data } = trpc.user.getMe.useQuery();

Security Best Practices

安全最佳实践

  1. Always validate input with Zod
  2. Use middleware for authentication:
    typescript
    const protectedProcedure = t.procedure.use(isAuthed);
  3. Sanitize error messages in production
  4. Implement rate limiting
  5. Use HTTPS in production
  6. Set CORS properly:
    typescript
    createNextApiHandler({
      router: appRouter,
      createContext,
      onError: ({ error }) => {
        if (error.code === 'INTERNAL_SERVER_ERROR') {
          console.error('Internal error:', error);
        }
      },
    });
  1. 始终使用Zod验证输入
  2. 使用中间件处理认证:
    typescript
    const protectedProcedure = t.procedure.use(isAuthed);
  3. 生产环境中 sanitize 错误信息
  4. 实现限流
  5. 生产环境使用HTTPS
  6. 正确配置CORS:
    typescript
    createNextApiHandler({
      router: appRouter,
      createContext,
      onError: ({ error }) => {
        if (error.code === 'INTERNAL_SERVER_ERROR') {
          console.error('内部错误:', error);
        }
      },
    });

Type Safety Tips

类型安全技巧

  1. Export router type, not implementation:
    typescript
    export type AppRouter = typeof appRouter; // ✅
    // Don't export `appRouter` itself to client
  2. Use
    satisfies
    for better inference
    :
    typescript
    const input = {
      name: 'Alice',
      age: 30,
    } satisfies CreateUserInput;
  3. Avoid
    any
    in context
    :
    typescript
    // ❌ Bad
    ctx: { user: any }
    
    // ✅ Good
    ctx: { user: User | null }
  1. 导出路由类型,而非实现:
    typescript
    export type AppRouter = typeof appRouter; // ✅ 推荐
    // 不要向客户端导出`appRouter`本身
  2. 使用
    satisfies
    提升推断效果
    :
    typescript
    const input = {
      name: 'Alice',
      age: 30,
    } satisfies CreateUserInput;
  3. 避免在上下文中使用
    any
    :
    typescript
    // ❌ 不良实践
    ctx: { user: any }
    
    // ✅ 推荐
    ctx: { user: User | null }

Development Workflow

开发工作流

  1. Define schema first: Write Zod schemas before procedures
  2. Test procedures in isolation: Use
    createCaller
    for unit tests
  3. Use TypeScript strict mode: Catch type errors early
  4. Enable React Query DevTools:
    typescript
    import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
    
    <ReactQueryDevtools initialIsOpen={false} />
  1. 先定义schema:在编写过程前先写Zod schema
  2. 独立测试过程:使用
    createCaller
    进行单元测试
  3. 启用TypeScript严格模式:提前捕获类型错误
  4. 启用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(集成)