tRPC 深度教程:端到端类型安全的全栈开发

January, 10th 2026 19 min read Markdown
tRPC 深度教程:端到端类型安全的全栈开发

日期:2026-01-10 预计阅读时间:30 分钟


目录

  1. 什么是 tRPC?
  2. 核心概念
  3. 快速开始
  4. 深入理解
  5. 高级用法
  6. 最佳实践
  7. 常见问题
  8. 总结

一、什么是 tRPC?

1.1 问题背景

在传统的全栈开发中,前后端类型不一致是一个常见的痛点:

typescript
12345678910
// 后端 API (Express/Next.js)
app.get('/api/users/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);
  res.json(user);
});

// 前端调用
const response = await fetch('/api/users/123');
const user = await response.json(); // user: any 😱
console.log(user.name); // 可能 undefined,运行时才知道

问题

  • 前端不知道后端返回什么类型
  • 后端改了字段,前端不会报错(运行时才炸)
  • 需要手动维护两套类型定义

1.2 tRPC 的解决方案

tRPC = TypeScript Remote Procedure Call

让你像调用本地函数一样调用服务器函数,并且前后端类型完全同步

typescript
123456789101112
// 后端定义
export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      return await db.getUser(input.id);
    }),
});

// 前端调用(自动类型推断)
const user = await trpc.user.getById.query({ id: 123 });
console.log(user.name); // ✅ TypeScript 知道 user 有 name 属性

核心价值

  1. 端到端类型安全:后端改了,前端立刻报错
  2. 自动类型推断:无需手动定义前端类型
  3. 运行时验证:Zod 自动验证输入参数
  4. 自动序列化:Date/BigInt 等特殊类型自动处理

二、核心概念

2.1 Procedure(过程)

Procedure = 一个可远程调用的函数

tRPC 有两种 Procedure:

  • Query:只读操作(GET 请求)
  • Mutation:写入操作(POST/PUT/DELETE 请求)
typescript
123456789101112131415
// Query:查询数据(幂等)
export const todoRouter = router({
  list: publicProcedure.query(async () => {
    return await db.todos.findMany();
  }),
});

// Mutation:修改数据(非幂等)
export const todoRouter = router({
  create: publicProcedure
    .input(z.object({ title: z.string() }))
    .mutation(async ({ input }) => {
      return await db.todos.create({ data: input });
    }),
});

2.2 Router(路由器)

Router = 一组 Procedures 的集合

typescript
123456789101112131415161718192021
// 定义多个 Router
const userRouter = router({
  list: publicProcedure.query(...),
  getById: publicProcedure.query(...),
  create: publicProcedure.mutation(...),
});

const postRouter = router({
  list: publicProcedure.query(...),
  create: publicProcedure.mutation(...),
});

// 组合成根 Router
export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

// 前端调用
trpc.user.list.useQuery();
trpc.post.create.useMutation();

2.3 Context(上下文)

Context = 每个 Procedure 都能访问的共享数据

typescript
1234567891011121314151617181920
// 定义 Context
const createContext = async (opts: CreateNextContextOptions) => {
  const session = await getSession(opts.req);

  return {
    session,        // 当前会话
    db,             // 数据库连接
    headers: opts.req.headers,
  };
};

// 在 Procedure 中使用
export const userRouter = router({
  getMe: publicProcedure.query(async ({ ctx }) => {
    if (!ctx.session) throw new Error('Unauthorized');
    return await ctx.db.users.findUnique({
      where: { id: ctx.session.userId },
    });
  }),
});

2.4 Middleware(中间件)

Middleware = 在 Procedure 执行前后执行的逻辑

typescript
123456789101112131415161718192021222324252627
// 定义认证中间件
const isAuthenticated = t.middleware(async ({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      session: ctx.session, // 确保 session 存在
    },
  });
});

// 创建需要认证的 Procedure
const protectedProcedure = t.procedure.use(isAuthenticated);

// 使用
export const userRouter = router({
  updateProfile: protectedProcedure
    .input(z.object({ name: z.string() }))
    .mutation(async ({ input, ctx }) => {
      // ctx.session 一定存在(中间件已检查)
      return await db.users.update({
        where: { id: ctx.session.userId },
        data: input,
      });
    }),
});

2.5 Input/Output Schema(Zod 验证)

Input Schema = 定义并验证输入参数

typescript
12345678910111213141516171819202122232425262728
// 定义 Schema
const createPostSchema = z.object({
  title: z.string().min(1, '标题不能为空'),
  content: z.string().min(10, '内容至少 10 个字符'),
  published: z.boolean().default(false),
  tags: z.array(z.string()).max(5, '最多 5 个标签'),
});

// 使用
export const postRouter = router({
  create: publicProcedure
    .input(createPostSchema)
    .mutation(async ({ input }) => {
      // input 类型自动推断为:
      // { title: string, content: string, published: boolean, tags: string[] }

      return await db.posts.create({ data: input });
    }),
});

// 前端调用
trpc.post.create.mutate({
  title: '我的第一篇文章',
  content: '这是内容...',
  tags: ['技术', 'TypeScript'],
});
// ✅ TypeScript 自动检查类型
// ✅ Zod 运行时验证(如果 title 为空会报错)

三、快速开始

3.1 安装依赖

bash
1
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod superjson

3.2 后端:初始化 tRPC

typescript
1234567891011121314151617181920212223242526272829303132333435363738
// src/server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';

// 1. 定义 Context 类型
type Context = {
  userId?: string;
};

// 2. 创建 Context
export const createContext = async (opts: { req: Request }) => {
  const token = opts.req.headers.get('authorization');
  const userId = token ? await verifyToken(token) : undefined;

  return { userId };
};

// 3. 初始化 tRPC
const t = initTRPC.context<Context>().create({
  transformer: superjson, // 自动处理 Date/BigInt
  errorFormatter({ shape }) {
    return shape;
  },
});

// 4. 导出工具函数
export const router = t.router;
export const publicProcedure = t.procedure;

// 5. 创建认证中间件
const isAuthenticated = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({ ctx: { userId: ctx.userId } });
});

export const protectedProcedure = t.procedure.use(isAuthenticated);

3.3 后端:定义 Router

typescript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
// src/server/routers/todo.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const todoRouter = router({
  // 查询所有 todo
  list: publicProcedure.query(async () => {
    return await db.todos.findMany();
  }),

  // 根据 ID 查询
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      return await db.todos.findUnique({
        where: { id: input.id },
      });
    }),

  // 创建 todo(需要登录)
  create: protectedProcedure
    .input(z.object({
      title: z.string().min(1),
      completed: z.boolean().default(false),
    }))
    .mutation(async ({ input, ctx }) => {
      return await db.todos.create({
        data: {
          ...input,
          userId: ctx.userId,
        },
      });
    }),

  // 更新 todo
  update: protectedProcedure
    .input(z.object({
      id: z.number(),
      title: z.string().optional(),
      completed: z.boolean().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      return await db.todos.update({
        where: { id: input.id, userId: ctx.userId },
        data: input,
      });
    }),

  // 删除 todo
  delete: protectedProcedure
    .input(z.object({ id: z.number() }))
    .mutation(async ({ input, ctx }) => {
      return await db.todos.delete({
        where: { id: input.id, userId: ctx.userId },
      });
    }),
});
typescript
123456789
// src/server/routers/_app.ts
import { router } from '../trpc';
import { todoRouter } from './todo';

export const appRouter = router({
  todo: todoRouter,
});

export type AppRouter = typeof appRouter; // ⚠️ 重要:导出类型

3.4 后端:HTTP 适配器(Next.js)

typescript
1234567891011121314
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/trpc';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext,
  });

export { handler as GET, handler as POST };

3.5 前端:配置客户端

typescript
12345
// src/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();
typescript
12345678910111213141516171819202122232425262728293031323334
// src/app/_providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import superjson from 'superjson';
import { trpc } from '@/utils/trpc';

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
          transformer: superjson, // 必须与后端一致
          headers() {
            const token = localStorage.getItem('token');
            return token ? { authorization: `Bearer ${token}` } : {};
          },
        }),
      ],
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

3.6 前端:使用 tRPC

typescript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// src/app/todos/page.tsx
'use client';

import { trpc } from '@/utils/trpc';

export default function TodosPage() {
  // 查询列表
  const { data: todos, isLoading } = trpc.todo.list.useQuery();

  // 创建 todo
  const utils = trpc.useUtils();
  const createMutation = trpc.todo.create.useMutation({
    onSuccess: () => {
      utils.todo.list.invalidate(); // 刷新列表
    },
  });

  // 更新 todo
  const updateMutation = trpc.todo.update.useMutation({
    onSuccess: () => {
      utils.todo.list.invalidate();
    },
  });

  // 删除 todo
  const deleteMutation = trpc.todo.delete.useMutation({
    onSuccess: () => {
      utils.todo.list.invalidate();
    },
  });

  const handleCreate = () => {
    createMutation.mutate({
      title: '新任务',
      completed: false,
    });
  };

  const handleToggle = (id: number, completed: boolean) => {
    updateMutation.mutate({
      id,
      completed: !completed,
    });
  };

  const handleDelete = (id: number) => {
    deleteMutation.mutate({ id });
  };

  if (isLoading) return <div>加载中...</div>;

  return (
    <div>
      <button onClick={handleCreate}>新建任务</button>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggle(todo.id, todo.completed)}
            />
            <span>{todo.title}</span>
            <button onClick={() => handleDelete(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

四、深入理解

4.1 类型推断原理

typescript
123456789101112131415161718192021222324252627
// 1. 后端定义
export const appRouter = router({
  todo: router({
    getById: publicProcedure
      .input(z.object({ id: z.number() }))
      .query(async ({ input }) => {
        return { id: input.id, title: '示例任务', completed: false };
      }),
  }),
});

// TypeScript 自动推断 appRouter 的类型:
type AppRouter = {
  todo: {
    getById: {
      input: { id: number };
      output: { id: number; title: string; completed: boolean };
    };
  };
};

// 2. 前端创建客户端
export const trpc = createTRPCReact<AppRouter>();

// 3. 调用时自动推断
const todo = await trpc.todo.getById.query({ id: 123 });
// todo: { id: number; title: string; completed: boolean }

4.2 HTTP 请求格式

typescript
12345678910111213141516171819202122232425262728
// tRPC 底层仍然是 HTTP,只是格式不同

// RESTful 请求:
POST /api/todos
Content-Type: application/json
{ "title": "买菜", "completed": false }

// tRPC 请求(不使用 batch):
POST /api/trpc/todo.create
Content-Type: application/json
{
  "json": { "title": "买菜", "completed": false },
  "meta": { "values": {} } // superjson 元数据
}

// tRPC 批量请求(使用 httpBatchLink):
POST /api/trpc
Content-Type: application/json
{
  "0": {
    "json": { "id": 123 },
    "meta": { "values": {} }
  },
  "1": {
    "json": { "title": "买菜" },
    "meta": { "values": {} }
  }
}

4.3 superjson 序列化

问题:JavaScript 的 JSON.stringify 无法处理特殊类型。

typescript
1234567
// 普通 JSON 的问题
const data = { createdAt: new Date('2025-01-01') };
JSON.stringify(data);
// → '{"createdAt":"2025-01-01T00:00:00.000Z"}' (字符串)

JSON.parse('{"createdAt":"2025-01-01T00:00:00.000Z"}');
// → { createdAt: "2025-01-01T00:00:00.000Z" } (仍是字符串,不是 Date)

superjson 的解决方案

typescript
123456789
import superjson from 'superjson';

const data = { createdAt: new Date('2025-01-01') };
const serialized = superjson.stringify(data);
// → '{"json":{"createdAt":"2025-01-01T00:00:00.000Z"},"meta":{"values":{"createdAt":["Date"]}}}'

const deserialized = superjson.parse(serialized);
// → { createdAt: Date 对象 } ✅
console.log(deserialized.createdAt instanceof Date); // true

支持的类型

  • Date
  • undefined
  • BigInt
  • Map / Set
  • RegExp
  • Error
  • 循环引用

4.4 错误处理

typescript
12345678910111213141516171819202122232425
// 后端抛出错误
export const todoRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      const todo = await db.todos.findUnique({ where: { id: input.id } });

      if (!todo) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: '任务不存在',
        });
      }

      return todo;
    }),
});

// 前端捕获错误
const { data, error } = trpc.todo.getById.useQuery({ id: 999 });

if (error) {
  console.log(error.message); // "任务不存在"
  console.log(error.data?.code); // "NOT_FOUND"
}

tRPC 错误码

  • UNAUTHORIZED - 401
  • FORBIDDEN - 403
  • NOT_FOUND - 404
  • TIMEOUT - 408
  • CONFLICT - 409
  • INTERNAL_SERVER_ERROR - 500
  • BAD_REQUEST - 400

五、高级用法

5.1 订阅(Subscription)

typescript
1234567891011121314151617181920212223
// 后端:定义订阅
export const todoRouter = router({
  onUpdate: publicProcedure.subscription(async function* () {
    const eventEmitter = new EventEmitter();

    // 监听数据库变化
    db.todos.on('update', (todo) => {
      eventEmitter.emit('update', todo);
    });

    // 推送数据
    for await (const data of eventEmitter) {
      yield data;
    }
  }),
});

// 前端:订阅数据
trpc.todo.onUpdate.useSubscription(undefined, {
  onData(todo) {
    console.log('收到更新:', todo);
  },
});

5.2 服务端调用(SSR)

typescript
123456789101112131415
// Next.js App Router 中使用
import { createCaller } from '@/server/trpc';

export default async function TodosPage() {
  const caller = createCaller({});
  const todos = await caller.todo.list(); // 服务端直接调用

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

5.3 条件查询

typescript
12345
// 根据条件决定是否查询
const { data } = trpc.todo.getById.useQuery(
  { id: todoId },
  { enabled: !!todoId } // 只有 todoId 存在时才查询
);

5.4 乐观更新

typescript
123456789101112131415161718192021222324
const updateMutation = trpc.todo.update.useMutation({
  onMutate: async (newTodo) => {
    // 取消正在进行的查询
    await utils.todo.list.cancel();

    // 保存旧数据(用于回滚)
    const previousTodos = utils.todo.list.getData();

    // 乐观更新
    utils.todo.list.setData(undefined, (old) =>
      old?.map((t) => (t.id === newTodo.id ? { ...t, ...newTodo } : t))
    );

    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // 回滚
    utils.todo.list.setData(undefined, context?.previousTodos);
  },
  onSettled: () => {
    // 重新获取数据
    utils.todo.list.invalidate();
  },
});

5.5 中间件链

typescript
1234567891011121314151617181920212223
// 定义多个中间件
const timing = t.middleware(async ({ next, path }) => {
  const start = Date.now();
  const result = await next();
  console.log(`${path} took ${Date.now() - start}ms`);
  return result;
});

const logging = t.middleware(async ({ next, path, input }) => {
  console.log(`Calling ${path} with input:`, input);
  return next();
});

const isAuthenticated = t.middleware(async ({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { userId: ctx.userId } });
});

// 组合中间件
const protectedProcedure = t.procedure
  .use(timing)
  .use(logging)
  .use(isAuthenticated);

六、最佳实践

6.1 Router 组织结构

plaintext
1234567891011121314
src/server/
├── trpc.ts              # tRPC 初始化
├── routers/
│   ├── _app.ts          # 根 router
│   ├── user.ts          # 用户相关
│   ├── post.ts          # 文章相关
│   ├── comment.ts       # 评论相关
│   └── admin/           # 管理员模块
│       ├── index.ts
│       ├── user.ts
│       └── analytics.ts
└── utils/
    ├── auth.ts          # 认证工具
    └── validation.ts    # 验证工具

6.2 Zod Schema 复用

typescript
123456789101112131415161718192021
// shared/schemas/user.ts
export const userSchema = z.object({
  id: z.number(),
  email: z.string().email(),
  name: z.string().min(1),
  createdAt: z.date(),
});

export const createUserSchema = userSchema.omit({ id: true, createdAt: true });
export const updateUserSchema = userSchema.partial().required({ id: true });

// 使用
export const userRouter = router({
  create: publicProcedure
    .input(createUserSchema)
    .mutation(async ({ input }) => { ... }),

  update: publicProcedure
    .input(updateUserSchema)
    .mutation(async ({ input }) => { ... }),
});

6.3 Context 类型安全

typescript
123456789101112131415161718192021222324252627
// 定义多种 Context
type BaseContext = {
  db: Database;
};

type AuthenticatedContext = BaseContext & {
  userId: string;
};

// 中间件保证类型
const isAuthenticated = t.middleware(async ({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });

  return next({
    ctx: ctx as AuthenticatedContext, // 类型断言
  });
});

// 使用
const protectedProcedure = t.procedure.use(isAuthenticated);

export const userRouter = router({
  getMe: protectedProcedure.query(async ({ ctx }) => {
    // ctx.userId 类型为 string(不是 string | undefined)
    return await ctx.db.users.findUnique({ where: { id: ctx.userId } });
  }),
});

6.4 错误处理统一化

typescript
1234567891011121314151617181920212223242526272829
// utils/errors.ts
export class AppError extends TRPCError {
  constructor(message: string, code: TRPC_ERROR_CODE_KEY = 'INTERNAL_SERVER_ERROR') {
    super({ code, message });
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 'NOT_FOUND');
  }
}

export class ValidationError extends AppError {
  constructor(message: string) {
    super(message, 'BAD_REQUEST');
  }
}

// 使用
export const todoRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      const todo = await db.todos.findUnique({ where: { id: input.id } });
      if (!todo) throw new NotFoundError('Todo');
      return todo;
    }),
});

6.5 性能优化

typescript
12345678910111213141516171819202122232425262728293031
// 1. 使用 dataloader 避免 N+1 查询
import DataLoader from 'dataloader';

const createContext = async () => {
  const userLoader = new DataLoader(async (ids: number[]) => {
    const users = await db.users.findMany({ where: { id: { in: ids } } });
    return ids.map((id) => users.find((u) => u.id === id));
  });

  return { userLoader };
};

// 2. 使用 httpBatchLink 批量请求
const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: '/api/trpc',
      maxURLLength: 2083, // 避免 URL 过长
    }),
  ],
});

// 3. 前端缓存配置
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 分钟内不重新请求
      cacheTime: 5 * 60 * 1000, // 缓存 5 分钟
    },
  },
});

七、常见问题

Q1: tRPC 支持文件上传吗?

A: 不直接支持。需要保留 RESTful 端点。

typescript
123456789
// REST API 上传文件
POST /api/upload
Content-Type: multipart/form-data

// tRPC 只传文件 URL
trpc.post.create.mutate({
  title: '文章标题',
  coverImage: 'https://cdn.example.com/image.jpg', // 上传后的 URL
});

Q2: 如何调试 tRPC 请求?

A: 浏览器 DevTools Network 面板。

plaintext
12345678910111213141516
请求路径:/api/trpc/todo.list
请求体:
{
  "json": {},
  "meta": { "values": {} }
}

响应:
{
  "result": {
    "data": {
      "json": [ ... ],
      "meta": { "values": {} }
    }
  }
}

Q3: tRPC 与 GraphQL 的区别?

维度tRPCGraphQL
类型系统TypeScriptSchema 定义语言
代码生成不需要需要
学习曲线低(纯 TS)高(新语言)
灵活性低(固定结构)高(查询任意字段)
生态新兴成熟
适用场景全栈 TS 项目大型 API + 多端

Q4: 生产环境性能如何?

superjson 性能开销

  • 序列化:比 JSON 慢 50%
  • 反序列化:比 JSON 慢 30%

建议

  • 小数据量(少于 1000 条):可忽略
  • 大数据量(超过 5000 条):考虑 RESTful

Q5: 如何处理 CORS?

typescript
1234567891011
// Next.js App Router 中设置 CORS
export async function OPTIONS() {
  return new Response(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

Q6: 外部系统如何调用 tRPC API?

不推荐。tRPC 设计用于内部 API,外部调用应该用 RESTful。

混合方案

typescript
12345
// 内部调用:tRPC
trpc.todo.list.useQuery();

// 外部调用:RESTful
GET /api/todos

Q7: tRPC 支持 WebSocket 吗?

支持,但需要配置 wsLink

typescript
123456789
import { wsLink } from '@trpc/client';

const trpcClient = trpc.createClient({
  links: [
    wsLink({
      url: 'ws://localhost:3000/api/trpc',
    }),
  ],
});

Q8: 如何测试 tRPC API?

typescript
1234567891011121314
import { createCaller } from '@/server/trpc';

describe('todoRouter', () => {
  it('should create todo', async () => {
    const caller = createCaller({ userId: '123' });

    const todo = await caller.todo.create({
      title: '测试任务',
      completed: false,
    });

    expect(todo.title).toBe('测试任务');
  });
});

八、总结

8.1 tRPC 的核心价值

  1. 端到端类型安全

    • 后端改了,前端立刻知道
    • 编译时发现错误,不是运行时
  2. 自动化

    • 类型自动推断
    • 序列化自动处理
    • 错误处理统一
  3. 开发体验

    • IDE 自动补全
    • 重构友好
    • 减少手动代码

8.2 适用场景

适合使用 tRPC

  • 全栈 TypeScript 项目
  • 同一个 monorepo
  • 内部 API(不对外)
  • 快速迭代

不适合使用 tRPC

  • 公开 API(外部调用)
  • 大数据量传输(超过 5000 条)
  • 文件上传
  • 非 TypeScript 客户端

8.3 学习路径

  1. 第 1 天:理解核心概念(Procedure/Router/Context)
  2. 第 2 天:搭建完整的 Demo(Todo App)
  3. 第 3 天:学习高级用法(Middleware/Subscription)
  4. 第 4 天:实践项目集成

8.4 参考资源


最后更新:2026-01-10 作者:AI Assistant License: MIT


附录:完整示例代码

A. 后端完整代码

typescript
12345678910111213141516171819202122232425
// src/server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';

type Context = { userId?: string };

export const createContext = async (opts: { req: Request }) => {
  const token = opts.req.headers.get('authorization');
  const userId = token ? await verifyToken(token) : undefined;
  return { userId };
};

const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

export const router = t.router;
export const publicProcedure = t.procedure;

const isAuthenticated = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { userId: ctx.userId } });
});

export const protectedProcedure = t.procedure.use(isAuthenticated);
typescript
1234567891011121314151617181920
// src/server/routers/todo.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const todoRouter = router({
  list: publicProcedure.query(async () => {
    return await db.todos.findMany();
  }),

  create: protectedProcedure
    .input(z.object({
      title: z.string().min(1),
      completed: z.boolean().default(false),
    }))
    .mutation(async ({ input, ctx }) => {
      return await db.todos.create({
        data: { ...input, userId: ctx.userId },
      });
    }),
});

B. 前端完整代码

typescript
12345
// src/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();
typescript
12345678910111213141516171819202122
// src/app/todos/page.tsx
'use client';

import { trpc } from '@/utils/trpc';

export default function TodosPage() {
  const { data: todos } = trpc.todo.list.useQuery();
  const createMutation = trpc.todo.create.useMutation();

  return (
    <div>
      <button onClick={() => createMutation.mutate({ title: '新任务' })}>
        新建
      </button>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

Happy Coding! 🚀