t3code这个代号是我最近一个全栈工程的实际项目名。你如果刷到这个名字先别懵它既不是一个开源库也不是某个框架的新版本而是我在 T3 技术栈下从零搭建的一套完整应用的代号。T3 这个词在 TypeScript 社区里已经不算新鲜但很多人对它的理解还停留在Next.js tRPC这个层面真正把它跑通、跑稳、跑到生产环境中间其实有不少坑。这篇就把 t3code 这个项目从架构选型、类型设计、后端路由到前端接入、部署上线的完整链路拆开来讲适合正在学习全栈开发、或者准备用 T3 技术栈接自己项目的朋友参考。1. 项目整体设计t3code 为什么选这套技术栈1.1 T3 技术栈到底是什么简单说T3 技术栈是一套固执己见的全栈方案核心成员是四个Next.js 负责前端框架和 API 层TypeScript 负责全链路类型约束Tailwind CSS 负责样式tRPC 负责前后端数据通信。实际项目中还会加上 Prisma 作为 ORM、Zod 作为校验器这全套组合在一起才构成我这次 t3code 项目的底座。选这套方案的原因很直接它是一个把类型安全贯彻到极致的组合。传统的前后端分离项目里前端调接口要手动写一份类型定义后端接口文档一变前端就得跟着改改漏了就是运行时才暴露的问题。T3 技术栈的思路完全不同——类型定义从数据库模型开始通过 Prisma 生成类型再通过 tRPC 把这套类型直接传到前端组件里中间几乎没有手工维护类型的成本。不过这里要提醒一点T3 不是一个开箱即用的框架它没有独立的运行时也不存在一个叫T3的包。它更像是一套最佳实践的集合。你完全可以只挑选其中的部分使用比如只上 Next.js TypeScript不加 tRPC。但既然叫 t3code我在项目里就把这套方案完整落地了这样才能真正体验不同类型安全机制之间的协同效果。1.2 类型安全解决的实际问题有人可能会问类型安全到底带来了什么实际价值我拿 t3code 里的一个实际场景说明。项目里有一个帖子列表功能传统模式下你要先写后端接口、定义返回结构再在前端单独声明一个接口类型。如果后端的字段名从postTitle改成了title前端不一定会立刻报错往往要等到页面运行时才发现数据渲染不出来。在 t3code 这样的 T3 工程里这种问题在编译阶段就会被拦截。因为前端调用的 tRPC 查询函数是直接从后端路由推导出来的后端类型改了前端的调用处会直接出现红色波浪线。我实测下来项目从开发到部署因为字段名不匹配导致的线上故障是零这在以前做前后端分离项目时几乎不敢想象。还有一层是开发体验的提升。tRPC 配合 TypeScript 的智能提示前端代码里可以点出来后端有哪些查询、每个查询需要什么参数、返回什么结构不需要频繁切到接口文档页面。这个体验类似后端函数直接在前端被调用虽然实际上走的还是 HTTP 请求但体感完全不同。1.3 为什么不用 GraphQL 或传统 REST在选型阶段我也认真比较过 GraphQL 和 REST。GraphQL 灵活但引入的复杂度很高需要定义 Schema、解析器、处理缓存和权限。对于 t3code 这样一个中大型应用来说GraphQL 这种过度设计反而会拖慢开发效率。REST 相对简单但问题在于类型不互通前端要手写类型接口多了以后维护成本很高。tRPC 的优点在于它是过程中间层而非协议层。它直接用 TypeScript 的函数签名作为接口定义没有 Schema 语言、没有代码生成步骤、不需要额外维护一份文档。缺点也有它绑定了 TypeScript 生态如果你的前后端不是同一套语言tRPC 就完全无法使用。好在 t3code 全栈都是 TypeScript这个限制不构成问题。2. 核心细节解析每个环节的关键决策2.1 Next.js App Router 的目录与服务端组件t3code 采用 Next.js 14 的 App Router 模式。目录结构上app目录下每个文件夹对应一个路由层级page.tsx是页面组件layout.tsx是布局组件api目录则用来放服务端接口。这套模式的核心理念是服务端优先默认情况下组件在服务端渲染只有显式标记use client的组件才进入客户端。这个决策的影响很大。T3 技术栈里的 tRPC 在早期版本中和客户端组件配合比较多但在 App Router 模式下更推荐的做法是让服务端组件直接调用 tRPC 查询客户端组件只负责交互部分。t3code 里的页面就分了两层服务端组件负责取数据、渲染主要内容客户端组件负责搜索框、筛选器这类需要交互的 UI。实际开发中有个常见误区把整个页面都标记为use client。这样做虽然能解决部分问题但会让大量原本可以在服务端执行的逻辑跑到客户端去首屏加载和 SEO 都会受影响。我个人的经验是尽量让数据获取逻辑留在服务端组件里只有真正需要useState、useEffect或事件处理的部分才抽到客户端组件。2.2 Prisma 数据模型设计数据层我用的是 Prisma PostgreSQL。Prisma 的 Schema 文件是数据模型的单一事实来源它定义了数据库表结构同时自动生成对应的 TypeScript 类型。t3code 的核心表结构大概是这样用户表、帖子表、评论表。设计时我总结出几个关键原则。第一是关系字段的命名要一致比如authorId和author要能对应上这样 Prisma 自动生成的关系类型不会有歧义。第二是尽量使用枚举类型而非字符串字段比如帖子状态用PostStatus枚举而不是status: String这样在代码层面就能限制非法值。第三是索引要提前设计特别是外键字段和经常作为查询条件的字段。Prisma 的一个优点是迁移流程比较成熟。开发中我改了多次 Schema每次都是prisma migrate dev生成迁移文件并同步数据库团队成员拉下代码后执行prisma migrate deploy就能对齐结构。比起手写 SQL 迁移这套流程省心很多。2.3 tRPC 路由与 Zod 校验t3code 的后端逻辑全部挂在 tRPC 路由上。这里的核心概念是 router 和 procedure。router 是路由集合procedure 是具体的接口函数。每个 procedure 可以定义输入校验、中间件和输出而 Zod 就是用来做输入校验的。我举个实际例子创建帖子的 API。输入需要标题、内容和标签用 Zod 定义一个postSchema规定标题至少 3 个字符、内容不能为空、标签是字符串数组。这样一旦前端传入非法数据tRPC 会在请求层直接拦截返回格式化的错误信息而不是让错误散落到业务代码里。中间件这块t3code 做了两个自定义中间件一个用于用户登录态校验另一个用于记录请求日志。中间件的实现思路是收到请求后先从 header 里读取 token解析出用户信息挂到 context 上后续的 procedure 里就可以通过ctx.user直接访问当前用户不再需要每个接口单独解析一遍 auth。这个机制在多个接口间复用得很好代码也干净。3. 实操过程t3code 完整搭建流程3.1 环境准备与初始化如果你也想复现 t3code 这个项目准备工作不算复杂。需要安装 Node.js 18、包管理器 pnpm、Docker用于本地跑 PostgreSQL。我建议用 Docker 起数据库因为干净、易清理不会污染宿主机环境。本地数据库连接串形如postgresql://USER:PASSWORDlocalhost:5432/t3code这个字串后面会反复用到。初始化项目用官方脚手架pnpm create t3-applatest脚手架会交互式询问需要包含哪些模块。我的选择是Next.js、TypeScript、Tailwind CSS、tRPC、Prisma、Zod。先把 NextAuth 也勾选上后面做登录认证会方便。初始化完成后项目结构就已经是完整的 monorepo 形态前后端代码都在同一个仓库里tRPC 的类型共享也在配置层面就打通了。这个脚手架节省了大量时间但有一点需要注意它生成的代码质量较高但也很重里面有不少模板代码。初学者建议先通读一遍生成的关键文件尤其是src/server/api/trpc.ts和src/trpc/server.ts理解它们是怎么把类型传到前端的。如果不理解就直接往上堆业务代码后面出包的时候会非常痛苦。3.2 定义数据模型与配置 Prisma初始化完成后先打开prisma/schema.prisma把业务表建出来。t3code 的帖子模型大概长这样model Post { id String id default(cuid()) title String content String tags String[] author User relation(fields: [authorId], references: [id]) authorId String status PostStatus default(DRAFT) createdAt DateTime default(now()) updatedAt DateTime updatedAt } model User { id String id default(cuid()) email String unique name String? posts Post[] }这里Post和User建立了一对多关系tags用了 PostgreSQL 的字符串数组类型。定义好之后执行pnpm prisma migrate dev --name init这个命令会生成迁移文件、同步数据库结构同时重新生成 Prisma Client 类型。之后在业务代码里import { prisma } from ~/server/db就可以直接操作数据库而且返回的类型本身就是完善的 TypeScript 类型。3.3 打通 tRPC 查询tRPC 的核心文件是src/server/api/trpc.ts它创建了createTRPCRouter和publicProcedure、protectedProcedure等基础构造器。t3code 的业务路由建在src/server/api/routers/post.ts。定义第一个查询import { z } from zod; import { createTRPCRouter, publicProcedure } from ../trpc; export const postRouter createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) { return ctx.prisma.post.findMany({ orderBy: { createdAt: desc }, include: { author: true }, }); }), create: publicProcedure .input(z.object({ title: z.string().min(3), content: z.string().min(1), tags: z.array(z.string()).default([]), })) .mutation(async ({ ctx, input }) { return ctx.prisma.post.create({ data: { ...input, authorId: user_001 }, }); }), });然后将postRouter合并到根路由src/server/api/root.ts中。这里要注意 tRPC 的 version 10 和 version 11 语法有些差异脚手架默认是 version 10API 比较稳定我建议新项目暂时停留在 v10等生态完全过渡再升级。3.4 前端接入服务端组件与客户端组件前端接入 tRPC 有两条路径。服务端组件里直接用 server callerimport { api } from ~/trpc/server; export default async function PostsPage() { const posts await api.post.list.query(); return PostList posts{posts} /; }客户端组件里则用trpc.post.list.useQuery()这个 hook 会走一遍 React Query 的缓存和请求生命周期。两种方式在 t3code 里搭配使用页面初始数据用服务端组件获取保证首屏不闪烁、SEO 友好后续的筛选、分页等操作走客户端 hook利用缓存提升交互响应速度。这个混合模式看起来简单实际有个容易踩的坑不要在客户端组件里直接导入服务端组件的代码。比如在客户端组件里import { api } from ~/trpc/server运行时会报错因为 server caller 依赖 Node 环境到了浏览器端就没有对应的执行环境了。正确做法是把服务端获取的数据作为 props 传给客户端组件或者用 tRPC 的客户端 hook 重新请求。4. 常见问题与排查技巧实录4.1 类型不同步前端找不到新加的查询这个问题在我开发 t3code 的过程中出现过好几次尤其是在新增 router 或修改 procedure 之后前端调用处会提示类型不存在。原因并不是代码写错而是 tRPC 的类型推导依赖 TypeScript 对 server 代码的增量编译有时候增量编译缓存没有及时更新。解决方案很粗暴但也有效重启 Next.js 开发服务器。如果重启还不行就删掉.next目录和node_modules/.cache再启动。平时我还会留意tsc --noEmit的输出只要类型检查通过基本说明 server 端的类型已经正确生成了。4.2 Prisma 在 Edge Runtime 下的连接问题Next.js 的某些功能会跑在 Edge Runtime 环境中而 Prisma Client 依赖 Node.js 的底层网络库在 Edge Runtime 下无法正常工作。t3code 最开始部署到 Vercel 时部分接口报了PrismaClient is not defined的错误排查后发现是某个服务端组件被推断为 Edge Runtime 了。解决办法是在next.config.js里显式指定需要运行在 Node Runtime 的路由const nextConfig { experimental: { serverComponentsExternalPackages: [prisma/client, prisma], }, };同时可以在需要数据库操作的路由文件里加export const runtime nodejs强制让该路由运行在 Node 环境中。这个问题一度很隐蔽因为本地开发环境一切正常只有到部署环境才会暴露建议项目一开始就确认清楚每个路由的运行环境。4.3 Zod 校验错误信息太生硬Zod 默认的错误信息是英文的而且形式比较机械比如字符串长度不足会返回String must contain at least 3 character(s)。在生产环境面向用户时这样的提示不够友好。我的做法是在每个字段的校验规则里自定义错误信息同时写一个统一错误格式化函数把 ZodError 转换成前端可以展示的键值对结构。比如const postSchema z.object({ title: z.string().min(3, 标题至少需要 3 个字符), content: z.string().min(1, 内容不能为空), });这样 tRPC 返回的错误信息前端可以直接展示不需要再做一层文案映射。4.4 tRPC context 在服务端组件里访问不到服务端组件调用 tRPC server caller 时context 是独立创建的不像客户端请求那样能从 HTTP header 读取 token。t3code 初期在做服务端组件里的用户态判断时遇到了这个问题服务端组件拿不到当前登录用户的信息。解决思路是为服务端组件单独创建一个 context 工厂把 Next.js 的cookies()和headers()读取到的信息注入进去。简单说服务端的认证信息来源是 cookie比如 session token而客户端请求的来源是 Authorization header两者的 context 构建逻辑不同需要分别实现。这个属于 T3 技术栈比较高级的用法除非你的项目里有服务端组件需要感知用户状态的场景否则可以先不考虑。5. 部署生产环境与性能优化5.1 数据库连接与迁移部署t3code 部署在 Vercel 上数据库用的是独立的 PostgreSQL 实例。这里有一个关键决策数据库连接串不能直接写在环境变量里给所有分支共用要用 Vercel 的 Environment Variables 功能按分支配置。生产环境的连接串和预览环境、本地环境必须分开否则预览部署的接口可能会误写生产数据库。线上迁移的操作也需要注意。Vercel 上不能跑prisma migrate dev正确流程是先在本地把迁移文件生成并提交到代码仓库在数据库指定的迁移目录执行prisma migrate deploy。后面每次发版前我先手动跑一下迁移再推送部署代码避免新代码和旧表结构不一致导致线上接口报错。5.2 性能优化缓存与请求策略t3code 上线后的性能优化主要做了三件事。第一是给列表查询加上staleTime让 React Query 在缓存有效期内不重复请求这个对点赞数、浏览量这类实时性要求低的数据效果明显。第二是对详情页启用 Next.js 的静态生成或增量静态再生成帖子详情不是实时变化的数据可以缓存一段时间。第三是注意 tRPC 的批量请求能力客户端可以同时发起多个查询React Query 会自动做请求去重和合并。这三项优化做完Lighthouse 的 Performance 分数从 60 多提升到了接近 85首屏内容更快出现服务端压力也明显下降。算下来性能优化的收益远比多加几台机器来得实在。6. 我做完 t3code 之后的几点体会整套 T3 技术栈跑完一遍之后我最大的感受是类型安全不是一种锦上添花的体验而是能直接改变开发节奏的东西。t3code 这个项目的代码量不算少从数据模型到接口、到页面组件全程几乎没有因为字段改名、参数漏传这类低级失误返工过这在以前写前后端分离项目时是不太可能的。当然T3 技术栈也不是万能的。它适合团队里所有人都是 TypeScript 的场景适合产品迭代速度快、接口频繁变动的项目。如果你的团队里有人只会写 JavaScript 或者后端是其他语言那整套方案的收益会大打折扣。最后再分享一个小技巧T3 项目的目录结构虽然由脚手架生成但不要为了整洁去任意搬动src/server/api下的文件。tRPC 的类型推导对绝对路径导入比较敏感随意改动导入路径会导致编辑器里明明能看到类型、编译却报错的情况出现。保持脚手架默认结构在这个基础上增加业务模块就好。
阅读完成 · 觉得有帮助?