t3code这个项目名我在看到的第一眼就冒出一堆联想T3就是那套以TypeScript为核心的全栈组合拳——Next.js、tRPC、Tailwind、Prisma。前阵子正好用这套栈给团队搭了一个内部工具从零到上线跑了三个月踩过的坑比想象中多得多。这篇就把我基于t3code这个项目做的完整拆解、架构决策、实操记录、以及各种翻车现场整理出来给正准备上手T3 Stack或想重构一个全栈项目的人一份能直接抄的作业。1. 项目概述与T3 Stack选型思路1.1 t3code到底是什么简单来说t3code是一个以TypeScript为绝对主力的全栈应用代码库。它把前端界面、后端API、数据库访问、类型定义、部署配置全部收敛进一套代码里用同一个语言、同一套类型系统贯穿到底。如果你接触过传统的前后端分离开发大概率经历过这种场景后端写一套接口文档前端照着文档联调字段名稍微改一下前端就得翻半天代码找哪里报错。t3code这种模式就是为了干掉这一整类问题。它适合三类人一是想快速搭建一个内部管理后台或中型Web应用的开发者二是对TypeScript类型安全有执念、受够了运行时才发现字段错误的人三是想学习现代全栈最佳实践的前端工程师。我的建议是如果你已经有React和TypeScript基础用它练手非常合适如果你是纯后端背景想了解前端生态也能从中看到一套完整的现代工程化样板。1.2 为什么锁定T3 Stack这套组合选T3 Stack而不是其他方案核心逻辑就一句话用一套类型系统把从数据库到按钮的所有环节焊死。传统全栈项目在前后端接口处是断裂的后端返回的数据结构前端只能靠猜tRPC解决了这个断裂点。它不像REST那样需要手动定义接口路径和类型而是直接把后端函数暴露给前端调用类型自动推导前端调用的方法和参数都是类型安全的。create-t3-app脚手架默认集成的是Next.js TypeScript tRPC Prisma Tailwind这套组合各自分工明确Next.js负责页面渲染和路由支持SSR服务端渲染和RSCSEO和首屏体验都不错tRPC替代传统REST/GraphQL负责前后端的数据调用通道类型直接共享Prisma负责数据库建模和迁移schema即类型写一次模型自动生成全套TypeScript类型Tailwind CSS负责样式原子类方式让UI开发效率极高。这套组合看起来工具多但学习曲线其实不算陡。最爽的是你改一个Prisma模型保存后tRPC的输入输出类型和前端页面推断全部同步更新中间没有任何手动同步类型定义的动作。我实际用下来平均一个简单CRUD功能从建模到页面跑通时间能压缩到二十分钟以内。2. 项目架构设计与数据流规划2.1 目录结构与分层设计t3code项目的目录结构沿用了create-t3-app的标准布局但我在落地时做了一些调整更贴合中型项目治理src/ ├── pages/ # Next.js页面路由 │ ├── api/ # 服务端API入口含tRPC挂载 │ ├── dashboard/ # 业务页面 │ └── index.tsx ├── server/ │ ├── api/ │ │ ├── routers/ # tRPC子路由 │ │ └── root.ts # tRPC根路由聚合 │ ├── db.ts # Prisma客户端实例 │ └── auth.ts # 鉴权配置 ├── trpc/ │ ├── server.ts # 服务端tRPC初始化 │ ├── client.ts # 客户端tRPC封装 │ └── react.tsx # React Hooks封装 └── styles/这个结构的关键在于把tRPC的server端逻辑全部收在server/api目录下页面只依赖trpc/client部分的类型。我习惯把业务字段的校验、权限判断都放在router内部完成页面组件不直接操作数据库。这样做的理由很实在当项目从几个路由扩张到二三十个路由时如果每个页面都直接操作Prisma权限逻辑很快就会失控。2.2 类型安全的数据流页面到数据库一次打通看t3code的数据流比传统架构要清晰得多。以前一个典型的REST请求流程是前端调用fetch(/api/user)后端用Express写路由处理然后手写接口类型文档前端再手写对应的TS interface。这个链条中间每一步都可能出错尤其是字段改名后忘改类型。t3code里这个流程变成了一条直线。页面组件里直接调用trpc.user.getById.useQuery({ id })这个查询的类型直接来自于后端router的返回类型。后端router处理逻辑通过Prisma查询数据库Prisma根据schema生成精确类型。链路中的任何一环类型变化都会在编译期暴露而不是上线后才发现数据不对。另外在数据流设计上要特别重视的一点是输入校验必须在服务端router层做不能只依赖前端表单验证。我见过很多T3项目把zod校验只放在前端react-hook-form里结果绕过前端直接调API就各种报错。tRPC天然支持zod推荐在input里就直接把类型和校验规则声明清楚服务端不接受任何未经验证的数据。3. 核心实现tRPC路由与Prisma建模实战3.1 用tRPC写第一个业务路由t3code里最核心的代码就是各种router。以用户模块为例完整代码如下逻辑非常直白import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ../trpc; export const userRouter createTRPCRouter({ // 公开接口获取用户列表 list: publicProcedure .input( z.object({ page: z.number().min(1).default(1), pageSize: z.number().min(1).max(100).default(20), }) ) .query(async ({ ctx }) { const skip (input.page - 1) * input.pageSize; const users await ctx.prisma.user.findMany({ orderBy: { createdAt: desc }, skip, take: input.pageSize, }); return users; }), // 受保护接口获取当前用户 me: protectedProcedure.query(async ({ ctx }) { return ctx.prisma.user.findUnique({ where: { id: ctx.session.user.id }, }); }), // 修改用户资料 update: protectedProcedure .input( z.object({ name: z.string().min(1).max(50), bio: z.string().max(200).optional(), }) ) .mutation(async ({ ctx, input }) { return ctx.prisma.user.update({ where: { id: ctx.session.user.id }, data: { name: input.name, bio: input.bio }, }); }), });因为用的是publicProcedure和protectedProcedure权限语义在定义路由的时候就写清楚了。前端调用trpc.user.me.useQuery()会自动带上会话身份服务端通过ctx.session拿到用户信息不需要手动传token。这是tRPC相对REST一个很大的效率提升点。3.2 Prisma模型设计与迁移要点t3code里的Prisma schema是数据架构的源点。我建议开始写业务代码之前先把核心模型的关系设计清楚因为中途改模型涉及迁移和数据修正成本比想象中高。以典型的内容管理应用为例model User { id String id default(cuid()) name String email String unique posts Post[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Post { id String id default(cuid()) title String content String author User relation(fields: [authorId], references: [id]) authorId String tags Tag[] createdAt DateTime default(now()) updatedAt DateTime updatedAt }字段类型选择上有一个容易踩的坑如果是多租户或数据量大的场景自增Int主键在分布式环境下会撞车优于直接用cuid()避免在多个实例写入时出现问题。另外凡是需要做范围查询的字段如createdAt记得加索引Prisma默认不会给普通字段建索引数据量上来后慢查询会非常明显。执行迁移的命令是npx prisma migrate dev它会生成迁移SQL并自动重新生成Prisma Client。这一步要提醒的是迁移脚本一旦提交到git就不能轻易改名或删除生产环境的迁移历史必须和本地保持完全一致否则会直接导致线上库表结构错乱。3.3 鉴权与中间件protected procedure怎么用t3code里的鉴权封装其实并不复杂核心是createTRPCContext里把session塞进去然后在createTRPCProtectedRouter里校验。标准的trpc.ts文件核心代码大概这个样子import { TRPCError, initTRPC } from trpc/server; import { getServerAuthSession } from /server/auth; export const createTRPCContext async (opts: { headers: Headers }) { const session await getServerAuthSession(); return { ...opts, session }; }; const t initTRPC.contexttypeof createTRPCContext().create(); export const createTRPCRouter t.router; export const publicProcedure t.procedure; const enforceUserIsAuthed t.middleware(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: ctx.session } }); }); export const protectedProcedure t.procedure.use(enforceUserIsAuthed);这里有个细节值得留意getServerAuthSession必须在服务端调用不能把它塞到浏览器端的逻辑里。tRPC的server代码只跑在Node端因此session读取是安全的这也是它相比纯REST接口少写很多鉴权代码的原因。4. 前端体验Tailwind样式与状态管理细节4.1 Tailwind 组件库的组合方案Tailwind在t3code里的定位是纯原子化样式工具。刚开始用的时候很多人会不适应把一堆className堆在标签上的写法觉得一旦改样式就要翻很久。实际用久了就会发现Tailwind的真正优势是消除了CSS命名冲突和样式权重撕扯。在团队协作时不用再为名字取什么这个类被复用了几次操心所有样式都在视口内所见即所得。当然纯用Tailwind原子类做复杂交互组件还是很费劲的。我的实践是Tab、Modal这类基础组件尽量和headless组件库组合如Radix UI或Headless UI因为它们只提供交互行为不提供样式和Tailwind完全互补。这样既能保持样式一致性又不需要自己用JS重写可访问性逻辑键盘导航、焦点管理这些自己做真的容易漏。4.2 React Query缓存t3code里的数据请求策略tRPC本身就建立在React Query之上这算是个隐藏福利。t3code里大量用useQuery和useMutation缓存、重试、失效策略这些开箱即用。但这里有许多值得注意的细节不要把频繁变化的数据也缓存太久比如实时在线状态默认的staleTime会导致界面数据看起来更新不及时。mutation成功后的缓存失效要显式声明例如新增文章之后需要让列表页的缓存实效。正确操作是trpc.post.list.useQuery返回的refetch在useUtils中调用post.list.invalidate()。**利用好optimistic update**修改状态时先立即更新本地缓存失败再回滚体验直逼原生应用。但这类操作建议只用在信任度高的内部工具中避免数据一致性导致复杂问题。我实际开发t3code时踩过最典型的一个坑是列表请求和详情请求各自独立缓存更新详情后列表页不同步。后来对mutation统一加了invalidate逻辑问题才彻底解决。建议在项目初期就建立mutation成功必清理相关Query的约定不要在页面组件里到处写手动refetch。5. 部署上线与性能优化记录5.1 Vercel部署的注意事项t3code部署我首选Vercel原因很直接Next.js Vercel的适配最顺滑vercel build自动识别框架环境变量配置一次后基本就不用管了。但有几个点不提前处理上线当天就会很难受。数据库连接串需要区分环境开发库和生产库的URL必须严格隔离。我见过有人因为在Vercel环境变量里误写了本地数据库地址导致线上页面直接白屏的。Prisma在Vercel上构建时会执行prisma generate但不会自动执行迁移。所以迁移流程务必在CI/CD里手动控制而不是靠构建时自动跑否则每次部署都执行一次迁移数据库锁竞争会让团队腰酸背痛。还有一个细节是Serverless函数的体积。Vercel会把每个API路由拆成单独Function如果Prisma不看觉醒生成二进制文件会导致函数体积恶化、冷启动变慢。建议用prisma generate --no-hints并配合prisma/client的二进制引擎配置尽量控制在合理范围内。5.2 我踩过的性能坑性能问题集中在两个地方服务端渲染的性能和数据库查询的性能。第一在页面组件里避免直接写大量同步查询。虽然Next.js的getServerSideProps可以做SSR数据获取但t3code大量使用tRPC如果每个页面都等一串查询全部返回再渲染首屏延迟会明显增加。我后来改用RRFReact Server Fetch的方式让细粒度的数据小块各自加载、各自流式输出首屏体验明显更顺。第二谨慎使用findMany不带字段裁剪。Prisma的API会返回模型所有标量字段如果把不需要的字段全查回来尤其是大文本字段网络负载和内存开销都很可观。所有列表接口都会用select或omit把不需要的字段去掉这种小习惯对大流量非常有帮助。第三RSC里不要盲目把prisma实例作为依赖传给子组件Prisma实例内部状态可能会导致连接数异常。RSC和Server Actions的规范还在持续演进我的原则是保持数据获取逻辑尽可能集中在服务端路由不要在组件树里层层传递数据库实例。6. 常见问题排查与避坑实录6.1 tRPC类型不更新怎么处理这是高频问题。tRPC的客户端类型是根据服务端router推导的但你改了router代码之后有时前端却完全没有感知还是旧类型。原因通常是Next.js的dev server没有完整重启tRPC的类型只会在编译时刷新缓存时间久了就会出现服务端已经改了前端还报错的错觉。最有效的处理路径删除.next目录缓存的类型产物重启dev server。如果还不行查tsconfig.json里是否把src/trpc相关的路径配置写错了。另外注意createTRPCNext里类型参数必须是Router的根类型可以用inferRouterInputs/Outputs显式抽取需要的输入输出类型避免深层次泛型推导失败。6.2 Prisma连不上本地数据库t3code里最常见的是连接本地PostgreSQL或SQLite时报错。如果用的是SQLite第一次migrate dev后要确认prisma/dev.db文件确实生成了。如果用的是Docker或本地PostgreSQL先排查DATABASE_URL里的用户名密码是否正确。注意Prisma要求连接串里的schema部分不要轻易省略否则不同环境schema可能不匹配。我最常犯的错是修改schema.prisma后忘了跑npx prisma generate导致客户端没有新的模型方法。跑一次迁移或generate之后重启dev server就正常了。如果连接超时优先检查防火墙和Docker端口映射。6.3 构建期遇到的TS类型报错t3code是严格TS模式构建时任何类型错误都会直接fail。我遇到比较多的是三个类型问题trpc客户端没有在_app.tsx里正确配置withTRPC包裹导致所有useQuery的类型推导不了。react-query和tanstack/react-query版本混用tRPC v10要求tanstack/react-query的版本必须匹配。我后来直接固定版本号不再用latest避免生态版本漂移引发的类型冲突。zod的input类型与服务端output类型不匹配。如果用了zod的transform要注意tRPC的推断结果是变换后的类型而不是input字面量类型。类型报错的排查思路很实用先读报错信息里到底指向哪个文件哪个type再去查对应router的返回绝大多数类型问题都是因为某个字段没在prisma模型里存在或在router里没有返回。6.4 经验速查表场景处理建议常见误区tRPC类型不同步删除.next缓存、重启dev server以为是编辑器缓存问题没用Prisma模型改了没生效先migrate dev再generate只改了schema不重新生成clientmutation后列表不刷新在mutation成功后invalidate()相关query手动refetch导致逻辑混乱部署生产数据库单独设置DATABASE_URL把dev数据库地址写进生产环境构建类型报错固定trpc/*和tanstack/*版本用latest导致版本不一致收尾分享最后分享一个我自己在t3code项目上养成的习惯所有tRPC router的返回类型都设计成最小可用。我见过很多人习惯直接把Prisma返回的完整模型吐给前端图省事但后面一旦加字段就要同步改前端样式相当痛苦。改成只返回页面真正需要的字段后接口的稳定性明显提高前端也不容易收到意外的大对象。如果你刚开始用T3 Stack建议也从这个习惯开始——类型安全不只是编译器帮你找错更是帮你思考数据边界的那条线。踩过几次坑之后你会发现t3code最大的价值不是那一堆脚手架代码而是它用一整套一致的工程语言告诉你全栈开发可以这么干净。
阅读完成 · 觉得有帮助?