T3 Stack全栈实战:从TypeScript到tRPC的类型安全开发 1. 从零认识t3code这个全栈技术组合到底想解决什么问题1.1 一个名字背后的技术拼图先给新手把概念捋清楚。t3code这个名字我第一次看到的时候也琢磨了一阵子。严格来说它不是一个框架也不是一个编程语言而是指用 T3 Stack 这套技术组合去落地真实项目时的工程化实践。T3 Stack 是近两年在全栈开发圈子里热度非常高的技术选型核心成员包括Next.js 做前端和 API 层TypeScript 做全链路类型约束Tailwind CSS 处理样式tRPC 负责前后端接口调用Prisma 操作数据库NextAuth 处理认证。六个工具各司其职组合起来之后你写代码最直观的感受就是从数据库表结构到前端组件类型安全几乎是全程贯穿的。这套组合最初由 create-t3-app 这个脚手架项目带火作者 Theo 在社区里推广的核心理念是“类型安全是第一优先级”。我理解这句话的含义是当你的项目足够大、参与的人足够多时绝大部分 bug 其实都出在数据形状对不上——后端返回的字段写错了、前端拿到了 undefined、数据库里存进去的类型和接口定义不一致。T3 Stack 想做的就是把这些运行时才暴露的问题尽量提前到编译阶段就暴露出来。所以如果你把 t3code 拆开看其实就是在说用 T3 这套全栈技术来写真实项目代码。它既适合那些想在 Next.js 里做全栈开发的中级前端工程师也适合小团队快速搭建一个类型安全的内部工具或产品原型。我自己是从传统前后端分离模式切过来的最大的体会是以前接口文档要维护两份前端联调靠 Postman 拷来拷去现在 tRPC 把类型直接从后端推到前端接口文档这件事基本可以扔掉了。1.2 为什么全栈框架这么多偏偏要选这套组合你可能会问Next.js 自己就能写 API 路由为什么还要再加一层 tRPCExpress 或者 Nest.js 做后端成熟稳定为什么要在 Next.js 里搞全栈这些问题我在刚开始接触 T3 的时候也反复琢磨过。先说前端框架。Next.js 在 React 生态里的地位已经不用多说了SSR、静态生成、文件路由、边缘函数这些能力开箱即用。但它本质上还是以页面渲染为核心的框架API 路由虽然能写后端逻辑却没有任何类型上的魔法你在前端 fetch 一个接口返回值永远是一个 any。这时候 tRPC 的价值就出来了它让你在 Next.js 的 API 路由上写服务端函数然后通过 TypeScript 的类型推导直接把函数的入参和出参类型“传递”到前端调用侧。前端调用后端就像调用一个本地异步函数不需要生成 SDK、不需要手写类型定义、不需要维护 API 文档。再说数据库层。Prisma 这个 ORM 工具在类型安全上的表现是通过 schema 文件定义模型然后自动生成客户端代码所有的查询和写入操作都有完整的类型提示。更关键的是Prisma 和 tRPC 加在一起能形成一个从数据库到 UI 的类型闭环数据库表结构由 schema 决定Prisma 生成类型tRPC router 里的输入输出引用这些类型前端通过 tRPC hooks 拿到完全一致的类型。数据的流动路径是直的类型像水流一样沿着管子贯穿到底中间没有任何断点。这套组合当然也有学习成本。TypeScript 泛型、tRPC 的 procedure 概念、Prisma 的 schema 约定每一项都需要花点时间去熟悉。但一旦掌握之后开发效率确实提升得很明显尤其是做那种中小型全栈应用的时候几乎不需要折腾复杂的工程化配置一个脚手架就能跑起来一个五脏俱全的项目。2. 环境准备与项目初始化5分钟创建你的第一个 T3 全栈应用2.1 动手前的完整环境清单与选择先把需要的工具列一下确保大家能一步到位工具版本要求作用Node.js18.x 及以上运行时环境T3 项目构建依赖npm 或 pnpm新版即可包管理器我建议用 pnpm安装速度快、磁盘占用小Git任意较新版本代码管理非必须但强烈推荐数据库本地 SQLite 或远程 PostgreSQL开发阶段用 SQLite 足够生产环境建议 PostgreSQLVS Code任意版本编辑器需要装 ESLint 和 Tailwind 插件Node.js 版本这块要注意T3 项目对版本有硬性要求太老的 16.x 版本会在安装依赖时报引擎错误。我遇到过同事用 nvm 切换版本导致 dev 服务起不来的情况后来统一用项目根目录的.nvmrc文件锁定了 Node 18 LTS团队里每个人都切到这个版本才消停。数据库的选择上新手阶段强烈建议直接用 SQLite。零配置、单文件、不需要单独启动数据库服务对于学习和本地开发来说是最舒服的。等你真到了部署上线的阶段再切换到 PostgreSQL 也不迟Prisma 的 schema 迁移机制能让这个过程非常平滑。后面我会专门讲一下数据库切换时要注意的事项。包管理器这里有一个经验之谈如果你和团队协作尽量统一使用同一个包管理器。T3 脚手架会自动根据你使用的工具生成本地锁文件npm 生成 package-lock.json、pnpm 生成 pnpm-lock.yaml 和 .npmrc 配置。混用的话依赖版本可能出现细微差异这也是很多玄学问题的根源。2.2 交互式配置从命令行到运行起来的完整流程创建项目非常简单打开终端进入你要存放项目的目录执行npm create t3-applatest然后它会以交互式问答的形式问你一系列问题下面是各个选项的解释和我自己的推荐项目名称和安装目录输入t3code会自动创建一个名对应的文件夹。TypeScript 还是 JavaScript直接选 TypeScript。T3 整套方案的类型安全哲学就是建立在 TS 之上的选 JS 等于自废武功。是否启用 ESLint / Prettier全部选 Yes。代码规范这种东西项目一开始就应该立好后面再加标准大家习惯已经养成了很难改。Next.js 的 App Router 还是 Pages Router新项目直接选 App Router这是 Next.js 官方现在主推的架构客户端组件和服务端组件边界清晰数据获取也被统一到 Server Components 模型下。tRPC、Prisma、NextAuth、Tailwind 要不要一起装我的建议是核心全选。既然选择 T3 这套组合就要体验它完整的能力。尤其是 NextAuth一开始就接好认证体系后面做权限控制会省非常多的事。导入别名使用默认的 ~/路径**直接确认即可。配置完成后进入项目目录启动开发服务cd t3code npm install npm run dev终端输出Ready in xx ms之后打开http://localhost:3000就能看到项目默认页面。需要提醒的是脚手架初始化过程中会自动执行一次prisma db push这类数据库同步动作所以它会提前把 SQLite 文件生成好整个过程基本不需要你额外操作数据库命令。2.3 初始化后的目录结构解读哪些文件是最重要的跑起来之后我们把核心目录结构过一遍。create-t3-app生成的项目虽然骨架简单但每个关键文件存在的位置直接影响后续开发效率├── prisma/ │ └── schema.prisma # 数据模型定义所有业务表的来源 ├── src/ │ ├── pages/ │ │ ├── api/ │ │ │ └── trpc/ │ │ │ └── [trpc].ts # tRPC API 路由入口 │ │ └── _app.tsx # 全局 Provider 挂载不跨页面端的不需要 │ ├── server/ │ │ ├── api/ │ │ │ └── routers/ │ │ │ └── post.ts # 业务路由示例 │ │ ├── db.ts # Prisma 客户端实例 │ │ ├── auth.ts # NextAuth 配置如果启用 │ │ └── api/trpc.ts # tRPC 根路由创建与上下文 │ ├── styles/ │ │ └── globals.css # 全局样式 │ └── env.js / env.mjs # 环境变量校验基于 zod我最想强调的几个点src/server/api/trpc.ts是 tRPC 的核心入口这里定义了 publicProcedure 和 protectedProcedure以及 context 对象。context 里默认包含了db和session也就是说你在任何 procedure 处理函数内部都可以直接读数据库、拿当前登录用户信息不需要在路由里手动初始化 Prisma 或解析 session。src/server/db.ts文件里的 Prisma 客户端是全局单例。这个文件看起来很简单但背后隐藏了一个 Next.js 开发模式下热更新导致客户端实例重复创建的性能坑。脚手架已经帮你处理好了 globalThis 缓存的问题新手不要轻易去改这块逻辑。prisma/schema.prisma是数据库模型定义的唯一来源。你会看到脚手架预置了一个Post模型展示用户发帖这个场景。后续开发新功能的所有表都从这个文件开始定义。src/env.js用 zod 做环境变量的运行时校验这个设计很优雅。如果部署时某个环境变量忘记填应用启动时会直接报错告诉你哪个变量缺失而不是等运行到某个接口才炸。了解完这些你就已经知道 t3code 这个项目的基本盘了。下面我们进入核心问题tRPC 到底是怎么工作的。3. tRPC 核心原理为什么前端能像调用本地函数一样调用后端3.1 tRPC 与 REST/GraphQL 的本质差异对从传统后端开发过来的开发者来说理解 tRPC 可能需要先打破一个惯性思维。REST 的世界里前端和后端的交流是通过 URL HTTP 方法 JSON 报文完成的。你要先定义接口、写文档、约定错误码然后在后端处理请求前端还得自己封装一套请求层。图省事的做法是用 Postman 导出的代码片段或者用 OpenAPI 生成类型定义type-safe 版。但不管怎么做前后端之间的沟通成本始终存在因为 URL 和 JSON 是“无类型”的类型安全是人为添加的一层外套不是语言层面的保证。GraphQL 做了一部分改进用 schema 定义 API 的形状类型可以生成客户端 SDK。但 GraphQL 有自己的复杂度解析器、缓存策略、resolver 拆分、n1 问题一个功能齐全的 GraphQL 服务的成本比 REST 高出不少。tRPC 的思路完全不一样它不定义接口文档不生成 SDK不做运行时协议。它直接利用 TypeScript 的泛型推导能力让你在服务端定义的函数签名参数类型和返回类型被自动推送到前端。前端引入api对象后调用一个后端函数就像调用一个 import 进来的异步函数一样。// 前端组件里 const { data } api.post.getAll.useQuery(); // 这个 data 的类型就是后端 getAll 函数返回的类型 // 而且是完全自动推导的不需要 any不需要手动声明接口这段代码里没有 URL没有字符串没有 fetch。如果后端函数签名变了比如返回对象里加了一个字段、删了一个字段前端代码会立刻出现 TypeScript 编译错误。这就是 tRPC 最大的价值当类型成为接口时编译错误就是联调错误。3.2 从 Router 到 ProceduretRPC 的完整工作链路上手 tRPC 需要理解几个概念Router路由集合、Procedure一个具体接口、Middleware中间件、Context上下文。一个 Router 就是若干 Procedure 的集合。打开脚手架预置的post.tsimport { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure, } from ~/server/api/trpc; export const postRouter createTRPCRouter({ hello: publicProcedure .input(z.object({ text: z.string() })) .query(({ input }) { return { greeting: Hello ${input.text}, }; }), getSecretMessage: protectedProcedure.query(({ ctx }) { return { message: you can now see this secret message!, }; }), });publicProcedure表示所有人都能调用protectedProcedure表示必须要登录才能调用。每个 Procedure 都有三种类型queryGET、mutationPOST/PUT/DELETE、subscriptionWebSocket 实时推送。query 适合读取mutation 适合写入。Procedure 的输入校验是另一个亮点。input方法接收一个 zod schema然后 tRPC 会在运行时做校验同时把校验后的类型推给前端。前端调用时如果参数缺了某个字段编译阶段就会报错不会有“接口请求成功但后端得到 undefined”这种问题。这种校验同时发生在前后端等于一道双重保险。链路完整走一遍是这样浏览器页面发起调用 - Next.js 的/api/trpc路由接收请求 - 根据请求里的路径参数比如[post, hello]找到对应的 router 和 procedure - 经过 zod 校验 - 创建上下文 context这里会读取 session 和 db 实例 - 执行 procedure 的处理函数 - 结果序列化返回 - 前端拿到类型安全的 data。3.3 实操体验快速配置根 Router 并注册到 API 路由前面看到的 postRouter 最终要被聚合进根 Router。脚手架生成的trpc.ts文件里会有export const appRouter createTRPCRouter({ post: postRouter, }); export type AppRouter typeof appRouter;然后 API 路由文件[trpc].ts里把这个appRouter挂载到 Next.js 的路径上import { fetchRequestHandler } from trpc/server/adapters/fetch; import { appRouter, createTRPCContext } from ~/server/api/trpc; const handler (req: NextRequest) fetchRequestHandler({ endpoint: /api/trpc, req, router: appRouter, createContext: () createTRPCContext({ req }), }); export { handler as GET, handler as POST };这个文件把所有 tRPC 请求都收口到同一个 API 路径下。注意这里用的是 fetch adapter适配 Next.js 的 Route Handler。前端页面组件里通过~/trpc/react里生成的 hooks 调用即可。整个架构清晰干净。我自己的经验新手最容易把 tRPC 想复杂其实可以把 Router 理解为后端 ControllerProcedure 理解为具体的 Actionzod 是参数校验器Context 是在处理请求之前注入的全局数据。想清楚这四个概念后面写业务就是机械式堆叠。4. 实操落地用 t3code 写一个可复用的待办事项应用4.1 数据模型设计与 Prisma Schema 定义理论讲再多不如动手做点东西。我拿一个非常典型的场景——待办事项Todo应用来完整演示一遍 t3code 的开发流程。麻雀虽小五脏俱全它能覆盖数据库建模、CRUD 接口、前端交互、认证权限四个核心环节。第一步在prisma/schema.prisma里定义 Todo 模型。脚手架自带了 Post 模型我们新增一个model Todo { id String id default(cuid()) title String completed Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt userId String user User relation(fields: [userId], references: [id], onDelete: Cascade) } model User { id String id default(cuid()) name String? email String? unique emailVerified DateTime? image String? accounts Account[] sessions Session[] todos Todo[] }这里做一个设计决策每个用户只允许操作自己的 Todo不能看到别人的数据。所以模型里加了userId外键。Prisma 定义关系的语法很直观relation声明关联字段onDelete: Cascade表示删了用户之后自动删除他的所有 Todo避免孤儿数据。定义完模型后执行npx prisma db push这条命令会把 schema 同步到本地 SQLite 数据库。开发阶段用 db push 很方便不需要生成迁移文件。但如果项目要部署到生产环境建议用npx prisma migrate dev --name init_todo生成正式迁移记录这个以后细说。4.2 编写 Todo 的 CRUDRouter 层面的核心逻辑在src/server/api/routers/todo.ts里创建 todoRouter完整代码如下import { z } from zod; import { createTRPCRouter, protectedProcedure, } from ~/server/api/trpc; export const todoRouter createTRPCRouter({ // 获取当前用户的所有待办 getAll: protectedProcedure.query(({ ctx }) { return ctx.db.todo.findMany({ where: { userId: ctx.session.user.id }, orderBy: { createdAt: desc }, }); }), // 创建新的待办 create: protectedProcedure .input(z.object({ title: z.string().min(1).max(100) })) .mutation(async ({ ctx, input }) { return ctx.db.todo.create({ data: { title: input.title, userId: ctx.session.user.id, }, }); }), // 切换完成状态 toggle: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const todo await ctx.db.todo.findUnique({ where: { id: input.id }, include: { user: true }, // 这里可以控制关联数据的读取粒度 }); if (!todo || todo.userId ! ctx.session.user.id) { throw new Error(Todo not found or not permitted); } return ctx.db.todo.update({ where: { id: input.id }, data: { completed: !todo.completed }, }); }), // 删除待办 delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const todo await ctx.db.todo.findUnique({ where: { id: input.id }, }); if (!todo || todo.userId ! ctx.session.user.id) { throw new Error(Todo not found or not permitted); } await ctx.db.todo.delete({ where: { id: input.id } }); return { success: true }; }), });这份代码有几个细节值得展开第一个是ctx.session.user.id。protectedProcedure 中context 已经注入了当前登录用户的 session 对象直接从中取用户 id 即可无需再去数据库查一遍 User 表。这个设计让权限控制变得简单每个 Procedure 处理函数天然知道当前请求是谁发起的。第二个是权限校验逻辑。切状态和删除操作都先查一次数据库比对todo.userId和ctx.session.user.id。这是典型的防御式编码防止用户直接在请求里改参考值。虽然 tRPC 有类型校验但这种跨行的所有权校验必须在业务代码里做掉。第三个是 zod 校验规则。title: z.string().min(1).max(100)设置了标题的合法性边界。前端如果传了空字符串或者超过 100 个字符的标题请求直接被拦截不会进入数据库层。之后把这个 router 注册到根 router 中和 postRouter 平级export const appRouter createTRPCRouter({ post: postRouter, todo: todoRouter, });4.3 前端页面接入从 useQuery 到乐观更新后端逻辑就绪后前端页面可以直接调用。在src/pages/index.tsx中import { api } from ~/utils/api; export default function Home() { const utils api.useUtils(); const { data: todos, isLoading } api.todo.getAll.useQuery(); const createTodo api.todo.create.useMutation({ onSuccess: () { utils.todo.getAll.invalidate(); // 创建成功后重新拉取列表 }, }); const toggleTodo api.todo.toggle.useMutation({ onMutate: async (variables) { // 乐观更新先改本地 UI再请求后端 await utils.todo.getAll.cancel(); const prevTodos utils.todo.getAll.getData(); utils.todo.getAll.setData(undefined, (old) old?.map((todo) todo.id variables.id ? { ...todo, completed: !todo.completed } : todo, ), ); return { prevTodos }; }, onError: (_err, _vars, context) { // 失败时回滚 if (context?.prevTodos) { utils.todo.getAll.setData(undefined, context.prevTodos); } }, }); // 渲染逻辑... }注意到 tRPC hooks 的命名规律api.todo.getAll.useQuery()对应 todo router 里的 getAll queryapi.todo.create.useMutation()对应 create mutation。这种“按 name 自动生成 hook”的设计让前后端方法名保持绝对一致维护起来非常清晰。乐观更新这块很多人觉得难理解其实机制不复杂用户点击切换完成状态时先把 UI 上的数据改掉让界面看起来马上有了反馈然后再等等后端。如果成功了就什么也不做如果失败了就用 onError 里的 context 把数据回滚到之前的版本。这样操作的手感会比传统“请求完了再刷新整个列表”流畅很多。还有一个我特别想讲的点invalidate。创建成功之后调用utils.todo.getAll.invalidate()这个动作和 React Query 里的 invalidateQueries 含义一样标记缓存失效触发重新查询。在 t3code 里 this 一步是隐式协议的一部分如果你的列表数据在多处展示这一个调用能保证所有引用这个 query 的地方都刷新数据。5. 从本地到线上T3 项目的部署与常见坑位5.1 数据库迁移从 SQLite 平滑过渡到 PostgreSQL本地开发用完 SQLite部署到线上想要稳定一般会换 PostgreSQL。这个过程首要步骤是修改环境变量然后一样顺畅。核心是 Prisma 的 schema 不需要大改只需要调整 datasource providerdatasource db { provider postgresql url env(DATABASE_URL) }SQLite 和 PostgreSQL 在字段类型上有细微差异。比如 SQLite 里的 Boolean 和 DateTime 在 PostgreSQL 里也是对应的但 SQLite 没有原生枚举类型如果你用了enum在 schema 里迁移时要检查一下。实际操作流程# 1. 修改 .env 文件把 DATABASE_URL 换成 PostgreSQL 连接字符串 # 2. 重新生成客户端 npx prisma generate # 3. 执行迁移开发/预发布环境 npx prisma migrate deploy # 4. 同步种子数据可选 npx prisma db seed我遇到的典型坑本地 SQLite 生成的 API 数据主键是自增数字切到 PostgreSQL 后如果你已经用了 cuid 默认值就没问题。但如果你在 SQLite 里用了 Auto Increment 而且是手动指定的整数 ID切换之后必须清理 history 表否则外键关联会错乱。另一个坑环境变量里的 DATABASE_URL 在云平台后台上线前必须填对。很多部署失败的原因不在代码而是 DATABASE_URL 填成了本地 127.0.0.1 的地址或者漏了 SSL 参数。PostgreSQL 托管服务商一般会给你一个带?sslmoderequire的连接串别把这个后缀弄丢了。5.2 构建产物与发布最常见的三个翻车现场T3 项目的部署方式非常灵活Vercel 是最简单的选项。由于 Next.js 本身就出自 Vercel后端 API 和前端都是同一套运行时交付时只需要一个平台配置就够了。这是 T3 另一个吸引人的地方部署到 Vercel 不需要单独布置后端服务也不需要配置跨域策略。发布前必做的是本地构建验证npm run build实际开发中的三个经典问题我在不同项目上都遇到或听说过第一个问题是 NextAuth 环境变量不对。NEXTAUTH_SECRET是 NextAuth 用于加密会话 token 的密钥本地开发时脚手架可能自动生成了一个但部署时如果不重新设置每次重启服务的 session 都会失效用户会被频繁登出。而且如果放到多个实例上secret 不一致会造成“登录失败”这种莫名问题。第二个问题是 Prisma 生成的 client 没有在部署机上重新生成。Vercel 部署时如果忽略.env变更可能拿到的是本地开发环境的 client连到了错误的数据库甚至报 P1001 连接错误。解决办法是在 package.json 的 build 脚本前加一步build: prisma generate next build第三个问题是 tRPC 的批处理行为。默认情况下同一个页面在首屏如果有多个 query 并发请求tRPC 会把它们合并成一个 HTTP 请求这是它的自动批处理优化。但如果你在部署后有边缘函数或自定义 server 配置某些环境下批处理可能失效出现接口响应变慢。这时可以去 router 配置中把batching关闭或者按需调整请求负载策略不至于伤筋动骨。5.3 性能优化缓存、加载状态和 bundle 体积控制全栈模式有时会被误解为“性能一定更好”严格说不是。T3 项目像任何 Next.js 项目一样需要在 Data Fetching 的时机上做选择。在 App Router 模式下服务端组件可以直接调用 tRPC 的 server caller把数据在服务端就准备好然后塞给客户端组件。这种方式既能做到首屏数据不闪烁也能减少传到浏览器的 JS 体积。具体地讲如果你想节省 bundle 体积空间tRPC 的server caller只是一个普通对象不会像 hooks 那样把整个 react-query 相关逻辑带进前端。以我的经验对展示型页面如用户个人主页、知识库文章详情页使用服务端数据流可以显著减小首屏 JS 尺寸。而在需要多种交互响应的页面如开口表单、管理面板上使用 hooks 反而体验更好因为可以做乐观更新和缓存失效。还有一个很小但很有用的原则要避免在 query 里面把不需要的字段查出来然后不展示。Prisma 默认支持字段选择right-click 到 schema 再手写select的取舍是每个 T3 项目都会遇到的问题。缓存是另一方面。因为数据源是同一个 Prisma 层你可以启用swr: true设置。比如一个 Todo 列表用户完成了某条操作把它标记为 completed但数据不会立刻重挂配合 optimistic update 就能做到几乎秒开响应在 tRPC 中使用useQuery(..., { staleTime: 1000 * 60 })能避免不必要的请求重复尤其是那种高频访问的统计信息。6. 常见问题与排查技巧实录我踩过的坑和绕坑指南6.1 高频问题速查表我把这段时间里使用 t3code 前后端一体化架构时经常收到的开发同学提问整理成一个表格方便你直接查阅现象根因解决方案前端调用 tRPC 返回 undefined但没有 TS 错误页面和服务端注册的 router 不一致检查是否有多个appRouter入口确保 API handler 引入的是同一个 routerprisma db push 报错 P1012 或 P2002schema 存在格式错误或唯一约束冲突先npx prisma format再逐条检查 unique 字段的重复数据NextAuth 一直跳转到登录页面数据库 Session 版本与库表不一致确认 NextAuth 的 session strategy 是否正确及required中间件路径配置tRPC mutation 成功后数据列表不刷新没有调用 invalidate 或调用路径不对/写错 key打印utils.todo.getAll.getQueryData()检查对是否缓存在同一 key 下type error in create-t3-appCannot use import statement outside a modulepackage.json 缺少type: module检查 jest/vitest 配置文件或改用tsx执行脚本部署到 Vercel 后 API 返回 503服务端拿不到数据库连接在环境变量里检查 DATABASE_URL加directUrl到 Vercel 的 Functions 选项好几个问题表面看是 Node 版本或者网络原因实际排查到最后都是环境变量或缓存 key 这类小问题。所以我现在遇到报错不会随便怀疑框架而是先枚举环境变量清单再确认查询 key 有没有写错最后才翻框架源码。6.2 实战中的独家避坑清单说几条常规文档里不会明确写出来的经验。第一条不要在 getStaticProps 或服务端组件里直接使用 api hooks。tRPC 的 hooks 依赖 React Query 的 QueryClientProvider这些 hooks 只能在客户端组件边界内使用。服务端要用 tRPC 数据应该通过createCaller创建服务端调用器或直接调用 router 的处理函数。我见过有人非要在服务端组件里useQuery结果页面一直空白控制台报 provider 缺失。第二条注意 tRPC 路径规则和文件名的大小写约定。Router 的 key 一般用小驼峰命名比如postRouter字段名为post前端调用api.post.getAll。如果哪天你看到api.userProfile.getProfile但代码里没有对应 key最大可能性是后端在 router 注册时字段名写错或空值了。第三条不要把敏感逻辑放在 client component 里。React Query 的缓存数据是可以被浏览器扩展、DevTools 和任何人打开内存查看的如果你的 procedure 返回了没有过滤的敏感字段如用户手机号它的类型约束并不能阻止别人查看网络请求的原始响应。还是要在后端控制返回字段的详细程度。第四条关于 Prisma 的扩展查询比如分页与统计。业务逐步变多时简单的findMany不够用。句法上 Prisma 提供了很好的过滤和排序链式调用能力比如实现分页用skip和take统计完成状态用条件where: { completed: true }的count。别等到页面卡了才去优化 SQL直接把include、select写得精细一些会比事后做 SQL analyse 痛快得多。第五条部署后验证 tRPC 端点是否正常。可以在浏览器里直接访问你的域名/api/trpc/todo.getAll?input{}看 JSON 返回App Router 下可能需要 POST 或 HEAD。这个方法比看前端界面更快定位到是前端还是后端的锅。6.3 开发流程里的实用心得最后我把使用 t3code 这套全栈实践几个月后的体会浓缩成几条经验给正在考虑引入这套技术栈的朋友先学 TypeScript再学 T3。TS 泛型不熟的话看 tRPC 的类型定义会像看天书。起码要理解infer、泛型函数、typeof这几个概念。从一个小需求开始练手。不要一上来就幻想做一个大型电商平台。用 Todo、记账本这种小工具跑通全链路之后许多设计模式自然就理解了。不要再写 API 文档了。团队里应该有一套约定改完后端函数签名前端编译报错就是文档更新。如果还有人习惯用 Word 写接口文档在 t3code 里属于冗余管理成本。留意依赖的升级节奏。Next.js 和 tRPC 都在快速迭代大版本升级时注意 changelog尤其是 procedure 的配置方式在不同大版本之间可能有 breaking change。我自己的体会是t3code 并不是银弹。它适合快速开发、团队规模小、对类型安全要求高的项目。但如果你想做那种需要复杂可查询接口、或多系统间大规模服务集成的项目还是老老实实用 REST/GraphQL。选择工具永远是看场景适合自己的才是最优解。如果你正在从零开始尝试这套技术栈我的建议是别犹豫直接照这篇文章里的流程走一遍。第一天就完成”创建项目 建表 写一个 router 前端调用“这个小循环感受到类型安全从后端一路贯穿到前端的爽感之后你自然会有动力继续深挖下去。