1. 项目缘起与核心定位第一次看到“t3code”这个名字我下意识以为是某个新出的低代码平台或者代码生成工具。翻了一圈社区讨论和零散的项目记录之后才慢慢拼出它的真实面貌——这是一个围绕T3 StackNext.js tRPC Tailwind CSS TypeScript构建的代码实践项目核心目标是把一套“类型安全、端到端可推导”的全栈开发范式真正落地成可复用的工程模板。说白了它不是某个具体产品而是一套开发方法论加脚手架的组合体。为什么这件事值得单独拿出来讲因为现在前端圈子里“全栈”这个词被用得太泛了。很多人所谓的全栈不过是前端写完页面再随手写两个接口类型对不上就any一把梭。而 t3code 想解决的恰恰是这个痛点让前后端之间的类型契约从编译期就锁死接口改一个字段前端立刻飘红根本不给运行时错误留机会。这套思路对于中小型团队、独立开发者、以及想从纯前端往全栈方向转的同学来说价值非常大。我接触这套东西大概是在两年前当时手上有个后台管理系统的项目接口文档和实际返回经常对不上联调阶段天天扯皮。后来把 tRPC 引入进来配合 Next.js 的 API 路由那种“改完接口前端自动感知”的体验确实回不去了。t3code 这个项目标题背后其实承载的就是这一类工程实践的沉淀。它适合谁适合已经会 React 和 TypeScript、想进一步提升工程化水平的开发者也适合团队技术负责人用来评估要不要在下一个项目里采用这套技术组合。2. 技术选型背后的取舍逻辑2.1 为什么是 T3 这套组合而不是别的先拆一下 T3 Stack 的四个核心件Next.js 负责全栈框架和路由tRPC 负责类型安全的接口层Tailwind CSS 负责样式TypeScript 负责整个类型系统。这四个东西单独拿出来都不新鲜但组合在一起产生的化学反应才是关键。Next.js 的 App Router 支持服务端组件和 Route Handler这意味着你可以在同一个项目里写前端页面和后端逻辑不用维护两个仓库。tRPC 则把接口定义变成了普通的 TypeScript 函数服务端写一个procedure客户端直接调用类型自动推导。Tailwind 的价值在于样式和组件逻辑放在一起减少上下文切换。TypeScript 是粘合剂把上面三个串成一条类型链。我试过用 REST OpenAPI 生成客户端代码的方案也试过 GraphQL 那套。REST 的问题是类型定义和实际实现容易脱节代码生成要额外跑一步CI 里经常忘记更新。GraphQL 类型是强了但 schema 和 resolver 之间还是有一层手写映射而且学习曲线陡。tRPC 的好处是没有中间层服务端函数的输入输出类型直接就是客户端的类型零代码生成零手动同步。注意tRPC 并不是银弹。如果你的接口需要被非 TypeScript 客户端消费比如移动端原生、第三方合作方那 tRPC 就不合适还是得走 REST 或 GraphQL。t3code 这套方案更适合前后端都是自己人、且都用 TypeScript 的场景。2.2 类型安全到底省了多少事举个我实际踩过的例子。之前有个用户列表接口服务端返回的字段叫createdAt前端某处写成了createAt因为都是字符串类型TypeScript 没报错测试也没覆盖到上线后那个时间一直显示空白。这种问题在 tRPC 体系下根本不会发生——字段名写错编辑器立刻标红编译直接失败。再比如接口参数变更。以前改一个查询参数得全局搜索哪里调用了这个接口一个个改。现在服务端改了input的 schema所有调用点自动报错改完才能编译通过。这种“编译器帮你做回归测试”的感觉用过就回不去了。从量化角度看我在一个中等规模项目里做过粗略统计引入 tRPC 之后联调阶段因为字段名、类型不匹配导致的问题减少了大概七成。剩下的三成主要是业务逻辑错误和边界条件那些本来就不是类型系统能解决的。2.3 项目结构怎么划分才不乱t3code 这类项目最容易犯的错就是目录结构混乱。我见过有人把所有 tRPC 路由塞在一个文件里写到两千行改一个接口要滚半天。合理的做法是按业务域拆分。一个我常用的结构是这样的server/api/routers/下面按模块建文件比如user.ts、post.ts、order.ts每个文件导出一个 router。然后在server/api/root.ts里合并成appRouter。客户端通过api.user.getById.useQuery()这样调用命名空间清晰。共享类型放在server/api/types/或者直接用 tRPC 推导出来的类型。千万不要手动写一份接口类型再手动同步那就失去 tRPC 的意义了。校验逻辑用 Zod 写在input里这样运行时校验和编译时类型是同一份定义不会出现“类型说没问题但运行时炸了”的情况。3. 核心环节的实操拆解3.1 环境搭建与依赖安装假设你从零开始第一步是初始化 Next.js 项目。我习惯用create-next-app选 TypeScript、ESLint、Tailwind、App Router 这几个选项。命令大概是这样npx create-next-applatest my-t3code-app --typescript --tailwind --eslint --app --src-dir装完之后进目录接着装 tRPC 相关依赖npm install trpc/server trpc/client trpc/react-query trpc/next tanstack/react-query zod superjson这里解释一下每个包的作用。trpc/server是服务端核心trpc/client是客户端核心trpc/react-query提供 React hooks 封装trpc/next是 Next.js 适配层。tanstack/react-query是 tRPC 底层依赖的数据请求库zod做运行时校验superjson解决 Date、Map 这类特殊类型的序列化问题。提示superjson这个包很多人会漏掉结果传 Date 对象的时候变成字符串前端拿到手还得手动转。加上它之后Date、BigInt、Map、Set 都能原样传输省心很多。3.2 服务端 router 的编写要点先建src/server/api/trpc.ts初始化 tRPC 实例配置 context 和 transformer。context 里通常放数据库连接、当前用户 session 这些东西。transformer 就填superjson。然后建src/server/api/routers/user.ts写第一个 routerimport { z } from zod; import { createTRPCRouter, publicProcedure } from ../trpc; export const userRouter createTRPCRouter({ getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input, ctx }) { const user await ctx.db.user.findUnique({ where: { id: input.id }, }); return user; }), });注意input用的是 Zod schema不是 TypeScript interface。这是关键区别——Zod 既提供编译时类型又提供运行时校验。如果客户端传了个数字 id服务端会直接抛校验错误不会带着脏数据往下走。在root.ts里合并import { createTRPCRouter } from ./trpc; import { userRouter } from ./routers/user; export const appRouter createTRPCRouter({ user: userRouter, }); export type AppRouter typeof appRouter;最后一行导出AppRouter类型客户端就靠它做类型推导。3.3 客户端调用的完整链路客户端这边先在src/trpc/react.tsx里创建 tRPC hooksimport { createTRPCReact } from trpc/react-query; import type { AppRouter } from /server/api/root; export const api createTRPCReactAppRouter();然后在组件里直接用const { data: user, isLoading } api.user.getById.useQuery({ id: 123 });这里id的类型会自动约束成 string传数字会报错。user的类型也会自动推导成服务端返回的类型编辑器里点进去能看到完整字段。整个过程没有任何手动类型声明全靠推导。我实测下来这套链路在 VSCode 里的响应速度很快改服务端代码后客户端类型提示几乎秒级更新。唯一需要注意的是 tsconfig 的strict要打开不然推导会打折扣。3.4 参数校验与错误处理Zod 的校验能力比很多人想的强。除了基础类型还能做字符串长度、数字范围、正则匹配、自定义 refine。比如.input(z.object({ email: z.string().email(), age: z.number().min(18).max(120), tags: z.array(z.string()).max(5), }))错误处理方面tRPC 提供了TRPCError可以指定错误码throw new TRPCError({ code: NOT_FOUND, message: 用户不存在, });客户端拿到错误后error.data.code就是NOT_FOUND可以根据错误码做不同 UI 处理。这比 REST 里靠 HTTP 状态码猜要清晰得多。注意不要在服务端把原始数据库错误直接抛给客户端那会泄露表结构信息。用 TRPCError 包一层对外只暴露必要的错误信息。4. 常见问题与排查实录4.1 类型推导失效怎么办最常见的情况是客户端api对象没有正确关联AppRouter类型。检查createTRPCReactAppRouter()里的泛型是不是导入了正确的类型。另一个坑是循环引用——如果服务端 router 里引用了客户端的类型会导致推导链断裂。保持服务端只依赖服务端代码客户端只依赖AppRouter类型不要交叉。还有一种情况是 tsconfig 的paths配置不对/server/api/root解析不到。检查baseUrl和paths是否匹配实际目录结构。4.2 请求报 500 但看不到具体错误开发环境下tRPC 默认会把错误信息返回给客户端。如果只看到笼统的 500先看服务端终端有没有打印堆栈。常见原因是数据库连接失败、Zod 校验抛错没被捕获、或者 context 初始化时出错。我遇到过一次是superjson没配置传 Date 的时候序列化失败报错信息很隐晦。后来在trpc.ts里加上transformer: superjson就好了。4.3 性能相关的几个坑tRPC 默认走 HTTP 批量请求多个 query 会合并成一个请求。这个特性在页面初始化时很有用但如果某个 query 依赖另一个 query 的结果就不能批量得用enabled选项控制执行时机。另一个坑是服务端组件里直接调用 tRPC 会绕过 React Query 缓存。如果需要在服务端预取数据用createServerSideHelpers或者直接在服务端调用 router 的函数不要走 HTTP。问题现象可能原因排查方向类型推导为 anyAppRouter 类型未正确导出检查 root.ts 的 export type请求 404路由路径不匹配确认 Next.js Route Handler 路径Date 变字符串未配置 superjson检查 transformer 配置批量请求失效query 之间有依赖用 enabled 控制执行顺序校验错误无提示Zod schema 未定义 message给字段加自定义错误信息4.4 独家避坑经验第一个经验不要把业务逻辑写在 router 里。router 只负责参数校验和调用 service 层真正的业务逻辑放到server/services/下面。这样单元测试好写router 也保持轻薄。第二个经验Zod schema 可以复用。表单校验和接口校验用同一份 schema前端表单提交前先校验一遍减少无效请求。我通常把 schema 放在shared/schemas/目录前后端都能 import。第三个经验开发阶段打开 tRPC 的 logger。在trpc.ts里配置loggerLink能看到每个请求的耗时和参数排查性能问题很方便。生产环境记得关掉不然日志量太大。第四个经验数据库查询用 Prisma 的话注意 N1 问题。tRPC 不会自动帮你优化查询该 include 的还是要 include。我见过有人在循环里调findUnique一百条数据打了一百个查询接口慢得离谱。5. 从 t3code 延伸出的工程思考5.1 类型安全只是起点t3code 这套东西解决的是类型层面的问题但工程化远不止类型。我后来在这个基础上加了几个东西用eslint-plugin-boundaries约束模块依赖方向防止 service 层反向依赖 router用vitest写 service 层单测router 层用集成测试用 GitHub Actions 做 CI每次 PR 跑类型检查和测试。这些加进来之后整个项目的稳定性上了一个台阶。类型安全让你在写代码时少犯错测试和 CI 让你在改代码时少犯错两者缺一不可。5.2 团队协作中的实际收益带过几个人的小团队之后我发现 t3code 这类方案对协作的改善很明显。新人接手项目打开编辑器就能看到所有接口的输入输出类型不用翻文档。接口变更时编译器会告诉所有受影响的地方不会漏改。代码 review 时类型层面的问题编译器已经拦住了reviewer 可以专注在业务逻辑和边界条件上。当然也有代价。tRPC 的学习曲线对纯前端来说有点陡需要理解 procedure、context、middleware 这些概念。我的做法是先让团队里一个人吃透写一份内部文档然后带着大家做一个小的内部工具练手跑通之后再上正式项目。5.3 什么场景不适合这套方案前面提过需要被非 TypeScript 客户端消费的接口不适合。另外如果项目非常小比如就几个页面加两个接口引入 tRPC 的收益可能抵不过配置成本。还有就是团队里如果有人强烈抵触 TypeScript强行推这套方案会引发很多摩擦不如先用 REST 过渡。我的判断标准是接口数量超过十个、前后端都是自己人、团队愿意投入时间学 TypeScript这三个条件满足两个以上就值得上 t3code 这套方案。否则可以先观望或者只引入其中一部分比如只用 Next.js Tailwind接口还是走 REST。5.4 后续可以怎么扩展这套基础搭好之后往上加东西很顺。加认证就在 context 里解析 token加权限就在 procedure 上加 middleware加缓存就在 service 层包一层 Redis。因为类型链路是通的每加一层都能保持类型安全。我最近在尝试把 tRPC 和 Server Actions 结合表单提交走 Server Action数据查询走 tRPC两者共享同一套 Zod schema。这样既利用了 Server Action 的渐进增强特性又保留了 tRPC 的类型推导优势。目前跑下来感觉不错等再稳定一些可以单独写一篇分享。最后分享一个小技巧如果你在犹豫要不要上 t3code 这套方案先花一个下午搭个 demo写两个接口跑通全流程。那种“改完服务端前端立刻知道”的体验比看十篇文章都有说服力。我自己就是这么入坑的到现在为止新项目只要条件允许我都会优先考虑这套组合。