t3code 实战:T3 Stack 代码生成器如何加速全栈 CRUD 开发
最近有不少朋友问我 t3code 是什么说实话这个工具我在小团队里已经用了一年多最初只是想省掉重复的 CRUD 代码后来发现它几乎把 T3 Stack 全栈开发的最佳实践固化进了模板。这篇文章不打算做官方文档复读而是把我从选型、落地到踩坑的完整过程分享出来。t3code 本质上是一个基于 TypeScript 全栈技术栈的代码生成器名字有两层意思一层是 T3 StackNext.js、TypeScript、Tailwind CSS、tRPC的缩写另一层是“Three-tier code”的谐音强调三层架构的代码生成。它能根据你的数据模型描述一键生成数据库模型、API 服务、前端表单和列表页面帮你把项目从零到一的速度提升两三倍。如果你是独立开发者或者在一个想规范前后端代码风格的团队里负责基建这个工具值得了解一下。1. t3code 项目概述我为什么不手写 CRUD 了1.1 拆开 t3code 这个名字很多第一次听到 t3code 的人会误以为它跟某个特定框架绑定实际上它更像一套“约定优于配置”的工程化解决方案。主打的 t3 指的是社区里非常活跃的 T3 Stack也就是 Next.js、TypeScript、Tailwind CSS、tRPC 这四样组合。这套组合最大的特点是全部用 TypeScript 类型打通从数据库到后端 API 再到前端组件类型可以一路传到底不存在接口文档过期的问题。t3code 的 code 部分则体现为“代码生成”你只需要写一个简单的模型定义文件它就能帮你生成完整的数据库 schema、tRPC router、React 组件以及页面路由。我最早接触这类工具时也怀疑过生成代码会不会很死板、很难改。实际用下来发现t3code 生成的代码本身就是按最佳实践组织的而且所有文件都留了手写扩展的口子不会在你改完之后下次生成时被覆盖。这一点后面我会详细讲它是我敢在正式项目里使用的前提。1.2 它解决的真实痛点如果你写过三个以上的全栈项目应该能明显感觉到项目的重复劳动不在“业务逻辑”而在“胶水代码”。每新增一个数据表你都要做同样一系列事情写 Prisma model、定义 zod 校验 schema、写 tRPC 的 validators 和 router、写前端获取数据的 hooks、再写一个列表页和一个表单页还要接好查询参数和分页。这套流程熟练之后虽然不难但极其消耗时间而且最容易在细节上出 bug。比如某个字段忘记加入 zod 校验前端传了空字符串后端直接 500查半天才发现是校验漏了。t3code 把这一整套流程自动化了。你在一个 model.t3.ts 文件里声明业务结构比如“用户有三花字段姓名、邮箱、角色”运行一条命令它就会在正确的位置生成所有配套代码。我接手过的项目里一个普通的后台管理模块差不多有三十多个文件用 t3code 生成后需要手动写的只剩真正的业务规则比如某个字段变更后要同步调用另一个接口。这样维护成本大幅降低新人上手也快因为他们看到的代码结构是统一的。1.3 适用人群与典型场景先说适合用的人。第一类是独立开发者或者小团队人少时间紧想快速验证产品t3code 能帮你把后台管理、内容模型这类基础功能在一两个小时内搞定。第二类是团队里负责工程化的人想统一项目结构让所有模块都遵循同一种分层方式t3code 的模板是可配置的。第三类是正在学 T3 Stack 的初学者一遍又一遍拆生成代码比看十遍文档都管用。不适合的也有。如果你的项目有大量非常规的数据关系、复杂的权限模型或者需要完全脱离 Next.js 的纯函数式后端那 t3code 可能帮不上太多。它擅长的是“标准 CRUD 基础业务逻辑”的快速搭建而不是一套能生成任何应用的万能机器。理解这一点很重要我见过有人把生成器当银弹结果遇到复杂需求反而比手写还痛苦。2. 核心架构与技术选型为什么选 T3 全家桶2.1 T3 Stack 四件套在 t3code 里的角色T3 Stack 的四件套不是随便凑的它们在 t3code 里有明确分工。Next.js 负责前端页面和 API 路由它是 React 生态里最成熟的全栈框架之一支持 App Router 和 Server Components配合 TypeScript 的类型推导非常顺滑。TypeScript 是整个体系的地基所有代码都基于类型驱动tRPC 能在前后端之间自动推导类型就是靠它实现了“类型安全”的远程调用。Tailwind CSS 负责样式生成出来的前端组件默认带了一套简洁的样式系统你不喜欢可以换成自己的设计体系因为生成代码里样式类都是内联在组件上的。tRPC 是这四件里最容易被小看的一个。传统 REST API 要手动维护路由、响应类型、错误处理而 tRPC 让前端像调用本地函数一样调用后端方法。t3code 在生成 tRPC router 时会自动为每个模型创建标准的 CRUD 操作getAll、getById、create、update、delete同时自动套上 zod 校验和登录鉴权。这听上去很简单但自己手写容易漏掉“校验失败时返回错误信息”、“未登录时抛出 unauthorized”这些边界情况生成器反而更稳定。2.2 t3code 的模块生成逻辑梳理t3code 的内部设计其实就是一个“可插拔生成器”的集合。它把每个业务模块拆成五层数据模型层、校验层、服务层、接口层、界面层。你在 model.t3.ts 里定义的每一个实体生成器都会分别在这五层生成对应文件。数据模型层是 Prisma schema负责表和字段定义。校验层是 zod schema负责类型校验和默认值。服务层是 tRPC router 中的业务函数负责实际操作数据库。接口层是 Next.js 的 API 路由负责桥接 tRPC 与前端请求。界面层是 React 组件和页面负责展示和交互。这五层的依赖关系是单向的数据模型层被服务层依赖服务层被接口层依赖界面层只管调用接口层不会直接碰数据库。这样的分层能让你在改动某一层时不至于牵连其他层。为什么这么设计核心原因是可维护性。如果你把代码都堆在页面组件里一开始写得很爽后面每次改字段都要全局搜索所有引用。t3code 强制拆开之后虽然文件数量变多了但每个文件的职责非常明确出问题只需要进对应层排查。我在团队里推行这套结构时最大的阻力是有人觉得“太啰嗦”但上线两个迭代之后没人再提意见了因为改需求的效率明显提升了。2.3 目录结构和数据流设计t3code 生成的目录遵循 Next.js App Router 和 T3 Stack 的建议。拿一个典型的用户管理模块举例工程结构大致如下src/ app/ api/ trpc/ [trpc]/ route.ts admin/ users/ page.tsx # 用户列表页 new/page.tsx # 新建用户页 [id]/edit/page.tsx # 编辑用户页 components/ users/ UserForm.tsx UserTable.tsx server/ db/ schema.prisma api/ routers/ userRouter.ts auth.ts trpc/ router.ts caller.ts数据流是典型的“前端组件 → tRPC client → tRPC server → Prisma → 数据库”。你可以把它想象成一根管道TypeScript 类型沿着这根管道全程流动前端表单里有个字段拼错了编辑器立刻标红根本轮不到运行时才报错。t3code 在生成前端 hooks 时会为每个 tRPC procedure 生成对应的 useQuery、useMutation 封装页面组件只需要调用这些 hooks不需要关心 fetch、序列化、错误码这些东西。这套数据流对调试也友好。因为类型是共享的你可以在 server 端打印日志确认入参也可以在浏览器端直接看到前端调用函数的参数结构。我遇到的大部分 bug 在编译阶段就被 TypeScript 拦截了真正到运行时的问题基本都是业务规则不对而不是接口不通。3. 从零开始实操用 t3code 生成一个完整的用户管理模块3.1 环境准备与安装开始之前需要准备好基础环境。Node.js 版本建议 18 或 20包管理器不限npm、pnpm、yarn 都行我个人用 pnpm 比较多因为安装速度快、磁盘占用小。还需要数据库环境t3code 默认支持 SQLite、PostgreSQL、MySQL 三种数据库小项目我用 SQLite 起步正式部署切到 PostgreSQL。安装 t3code 本身非常简单它是一个命令行工具可以直接用 npm 全局安装npm install -g t3code如果你不想全局安装也可以在一个项目目录里用 npx 临时执行npx t3codelatest init安装完建议先跑t3code --version确认版本号。我遇到过有人装完直接运行命令报 “command not found”多半是 npm 全局路径没有配进 PATH检查一下npm config get prefix指定的目录是否在环境变量里。接下来新建一个项目目录初始化 t3code 项目骨架mkdir my-app cd my-app t3code initinit 命令会问你几个问题包括项目名称、包管理器、数据库类型、是否启用 NextAuth 鉴权。如果你现在不确定可以先用默认值后面配置文件里都能改。init 完成后项目目录里会出现一个t3code.config.ts文件它的作用类似脚手架配置中心包括生成路径、组件风格、权限校验开关等选项。3.2 初始化项目与配置数据源init 完成之后最重要的步骤是配置数据源。t3code 会读你环境变量里的数据库连接串但自己新建项目时得手动设置。在项目根目录创建.env文件比如选择 PostgreSQLDATABASE_URLpostgresql://postgres:passwordlocalhost:5432/my_app?schemapublic NEXTAUTH_SECRETyour-secret-here NEXTAUTH_URLhttp://localhost:3000如果你只想先把流程跑通SQLite 更省事DATABASE_URLfile:./dev.db接着执行t3code db:push这个命令会生成 Prisma client 并同步数据库结构。这里注意db:push适合开发环境它会直接根据 schema 修改数据库不会留迁移历史。如果项目要上生产建议在数据模型稳定后用t3code db:migrate生成正式的迁移文件而不是继续用db:push。我早期不太在意这件事直接在测试库上 push等到要部署生产环境时才发现没有迁移记录差点把同事的开发库搞乱。配置完数据源后启动 t3code 自带的开发服务器t3code dev正常运行后访问http://localhost:3000能看到一个欢迎页面表示基础环境已经 OK。3.3 定义实体并生成 CRUD 代码核心步骤来了。在项目根目录新建一个schema/models.t3.ts文件我来演示一个用户模型的定义方式顺便包含一个关联模型import { defineModel } from t3code; defineModel(User, { fields: { name: { type: string, label: 姓名, required: true }, email: { type: string, label: 邮箱, required: true, unique: true }, role: { type: enum, label: 角色, values: [ADMIN, USER], default: USER }, posts: { type: relation, model: Post, relation: UserPosts }, }, options: { auth: { protect: true, roles: [ADMIN] }, searchable: [name, email], }, }); defineModel(Post, { fields: { title: { type: string, label: 标题, required: true }, content: { type: text, label: 正文 }, published: { type: boolean, label: 已发布, default: false }, author: { type: relation, model: User, relation: UserPosts }, }, });字段类型映射是 t3code 比较讲究的地方。string会生成 Prisma 的String、zod 的z.string()、表单里的 input 框text生成String但表单里用 textareaboolean对应 checkedenum对应 Prisma enum 和下拉选择框。定义完成之后运行t3code generate你会看到终端里刷出一堆“✓ generated”提示。打开src/server/db/schema.prisma能看到 User 和 Post 两个 model 已经生成。打开src/server/api/routers/每个模型对应一个 router 文件里面包含完整的 CRUD procedure。前端组件和页面也一起生成好了可以直接去浏览器里体验新建列表。3.4 接入前端页面与 API 路由生成出来的页面在src/app/admin/users下但你自己的应用导航里不一定有入口所以需要手动挂到菜单或主页面里。t3code 不会帮你把所有路由自动加进导航这是有意设计的因为导航结构跟产品设计强相关工具替你做决定反而碍事。我一般会在主页面加一个链接或者直接访问/admin/users看看效果。生成后的列表页在顶部有“新建”按钮每个用户记录右侧有“编辑”“删除”按钮。打开浏览器控制台会发现它调用的接口正是 tRPC 提供的/api/trpc/user.getAll。这里具体过程是 tRPC client 发起请求服务端解析 procedure 名经过输入校验后将结果序列化返回。前端拿到的数据自动带上了类型。如果你想手动调整某处逻辑比如列表默认按创建时间倒序而不是按 id 倒序直接打开userRouter.ts找到getAll方法里的orderBy字段改掉就行。下次再跑t3code generate它不会覆盖你手动改的文件除非你明确指定要重新生成。t3code 通过每个生成文件顶部的注释标记来判断是否覆盖这个机制让我很放心能让机器处理重复模板让手工处理业务差异。3.5 运行与联调验证当所有代码生成好后我把重点放在联调上。常见的验证步骤是先检查数据库中是否创建了表如果用 SQLite打开prisma/dev.db如果用 PostgreSQL用psql查一下表结构。然后跑一遍新建流程注意字段校验是否生效。比如我在模型里给 name 设了required: true前端表单提交时 tRPC 服务端会再次用 zod 校验一遍不留空名字的机会。如果项目开了鉴权还需要确认登录用户是否能访问受保护的接口。t3code 默认会在auth配置开启时给非公开 procedure 套上requireAuth中间件。我测试时用一个没有登录的浏览器窗口访问接口应该能收到UNAUTHORIZED错误。这种前后端同时校验的机制非常实用防止有人绕过前端的按钮隐藏直接 curl 接口。最后我要提的是生成完代码一定要纳入 Git 版本管理。t3code 的模型定义文件models.t3.ts是源头生成代码属于产物。产物可以提交到仓库也可以在 CI 里构建时重新生成但源头文件才是唯一的变更记录。团队协作时不要只改生成代码而忘了改模型定义否则下一次generate会把你的手工修改清理掉。这算是我踩过的最大一个坑现在我们会把“先改 model 再 generate”写成团队规范。4. 实战中遇到的问题与排查技巧4.1 数据库连接失败的常见原因t3code 生成的项目第一次跑数据库相关命令时最常见的报错就是Cant reach database server。这个问题的诱因很多但大多数情况是.env文件没有被加载。Next.js 环境变量加载机制要求变量必须在进程启动前注入如果你改完.env没有重启 dev server连接串还是旧的。我的建议是每次改完.env都强制重启一下t3code dev。另一种常见原因是本地数据库没启动。用 PostgreSQL 的朋友经常遇到端口被占用或者服务没起执行pg_isready确认一下状态。MySQL 则要确认字符集和权限。SQLite 最简单但也容易踩绝对路径的坑如果你在其他目录运行命令相对路径可能不对最好在.env里写完整的绝对路径或者在根目录统一执行命令。还有一类问题跟代理或者防火墙相关但目前国内网络环境访问数据库服务一般不走额外代理这里不展开。重点是看日志里的具体错误码比如 PostgreSQL 的28P01是密码错误3D000是数据库不存在先定位是哪一层的问题再动手。4.2 Prisma 迁移冲突处理团队开发里多人改 schema 是家常便饭t3code 生成的模型最终都会落到schema.prisma所以迁移冲突迟早会碰到。我遇到的一个典型场景是同事 A 在本地加了一个字段并生成迁移同事 B 同时改动了同一个 model结果 B 拉代码后跑t3code db:migrate提示迁移历史与数据库状态不一致。这个时候不要在本地直接db:migrate reset因为会清掉所有数据。正确做法是把当前的数据库状态和迁移历史对齐先看一下_prisma_migrations表对照已有迁移文件的名称把没有应用的迁移文件处理掉。如果两个人的改动重叠最好手动调整 model 文件把双方都需要的字段合并然后删掉冲突的迁移文件重新生成一个混合迁移。t3code 虽然没有自动解决 git 冲突的能力但它的模型定义文件比较短冲突解决起来通常比在schema.prisma里处理要简单。我会在项目里规定多人开发时一个迭代周期内尽量集中在同一个 model 上并且每天先拉取最新代码再跑 generate能有效减少这种问题。4.3 tRPC 类型不更新怎么办T3 Stack 的类型推断是它最大的优势但也是新手最常困惑的一点明明改完了userRouter.ts里的返回值前端调用处的类型还是旧的。这主要是因为 Next.js 开发服务器和 TypeScript watcher 有缓存tRPC 的类型是通过RouterOutput等工具类型从服务端推断出来的需要前端代码重新编译才能拿到新类型。我常用的解决方案是依次尝试三步第一步按CtrlShiftR强制刷新浏览器页面很多情况下只是热更新没有把类型缓存清掉。第二步重启 t3code dev让 TypeScript 重新编译整个项目。第三步如果还不行删除.next目录后重新t3code dev。这三个步骤能解决 95% 的类型不刷新问题基本不用去改 tsconfig 的incremental设置。还有一种情况是你在 server 端引用了ctx.session或者prisma字段但 tRPC 的 context 类型没有扩展编辑器会一直报红。这时候需要检查src/trpc/router.ts里是否把createTRPCContext的返回类型正确暴露。t3code 生成的初始化项目一般没问题但如果你手动调整过 context就得留意类型需不需要同步更新。4.4 权限系统集成时的坑为每个生成的模块配置权限是 t3code 的高级用法但也是最容易出问题的地方。首先要注意模型定义中auth.protect: true只代表这个模块需要登录不代表会自动按用户角色过滤数据。比如你想让普通用户只看到自己的帖子这属于业务规则生成器不会帮你猜你得在 router 的getAll方法里手动加where条件。另一个坑是角色判断的时机。t3code 自带一个简单的 ACL 中间件会检查session.user.role是否在允许列表里。但很多人把ADMIN写进了session里的逻辑有问题比如 NextAuth 的 JWT session 默认不更新新修改的角色字段。如果你改了用户角色但 session 里旧值还在权限判断一直走不准。解决办法是在 session 回调里强制从数据库读取最新角色或者让用户重新登录刷新。我在实际项目里更推荐把权限分层处理粗粒度的模块访问用 t3code 生成的requireAuth和requireRole中间件细粒度的数据范围控制自己写一个可复用的 query 条件函数。这样既有生成器的速度又不丢失灵活性。权限这块别指望工具能全帮你搞定它只负责把门收好房间里的东西怎么摆还得自己设计。5. 二次开发与团队落地经验5.1 自定义代码模板的目录约定t3code 的默认模板虽然好用但每个团队都有自己的组件库和代码规范改模板是很自然的需求。它支持通过t3code.config.ts指定自定义模板目录我习惯在项目里建一个.t3code/templates文件夹结构跟默认模板保持一致。例如你想让所有列表页都统一用公司封装的BizTable组件而不是默认的UserTable就可以复制默认模板里的list-page.tsx.ejs文件改掉引用组件名称。模板引擎支持变量替换modelName、fields、routePath都会被自动注入。改完之后运行t3code generate --template custom生成的所有页面就会走你的自定义模板。这里要提醒的是模板改动会影响所有后续生成的模块所以改之前一定先做好版本备份。我见过同事改模板改了一半导致生成代码语法错误还好有 Git 可以回滚。自定义模板的维护价值是长期的团队风格越统一后续代码审查越省力。5.2 通过配置化驱动生成除了改模板t3code 还支持在t3code.config.ts中定义很多生成开关。比如你可以开启softDeletes选项所有模型都会生成deletedAt字段和对应的逻辑删除逻辑开启auditFields所有模型自动加createdAt、updatedAt字段开启pagination列表接口会多返回total和pageSize。我一般会根据项目性质来开这些开关。对业务后台逻辑删除和审计字段几乎是刚需对数据要求严格的金融类项目反而需要物理删除避免审计追踪出问题。配置化的好处是不用改代码只改配置项就能影响所有模块的生成行为。但别贪多宁可需要的时候手动加字段也不要让所有模型都背上用不到的字段生成出来的代码冗余会影响阅读体验。我还有一个心得把t3code.config.ts当作团队的编码规范文档来维护新成员入职后看这个文件就能理解项目的工程标准。我现在会花时间写注释解释每个开关为什么打开、什么场景下不适合用这比写在 wiki 里有效得多。5.3 在 CI/CD 里自动生成代码的实践最后聊聊把 t3code 集成到 CI/CD。我在团队里推行的方式是在代码变更后由 CI 自动执行t3code generate然后检查生成的代码是否与仓库内已提交的代码一致。如果发现差异就认为上次 generate 后的产物没有同步提交自动让流水线失败。这一步能确保“模型定义文件”和“生成代码”永远不会脱节。具体实现可以在 CI 脚本里加两个步骤npx t3code generate --check git diff --exit-code如果git diff输出了内容说明有生成代码没提交流水线就会红。这个机制看起来简单但真正解决了多人协作时最隐蔽的问题有人改了 model 文件但没有重新生成手工改动接口层代码后续其他人拉取代码时跑 generate覆盖掉了他的修改。对于正式生产构建我不会在构建时重新运行t3code generate因为生成过程可能会引入环境依赖差异。更稳妥的方法是直接将生成代码连同源代码一起构建CI 只负责根据models.t3.ts校验生成的正确性。这样既保留了生成器的效率又保证了构建环境的稳定。这里有个小技巧值得分享在 CI 中使用t3code generate --check前最好固定 t3code 版本比如安装到devDependencies里并锁定版本号。因为不同版本的生成器可能产生不同的格式细节哪怕一个空行的变化都会让 diff 检查失败。把依赖版本固定配合 lockfile就能避免这种无谓的噪音。做技术基建就是这样解决一个痛点之后会发现背后还有一连串隐患。t3code 帮助我把团队从重复劳动中解放出来但真正让效率持续提升的是那些围绕生成器建立的规范和流程。从模型定义到 CI 校验每加一层约束后续维护的摩擦就少一分。如果你也在为全栈项目的重复代码头疼不妨从一个小模块开始试起先跑通一条生成链路再慢慢扩大使用范围。这种从点到面的推进方式比一开始就推全量改造稳妥得多。