1. t3code 到底是什么:从 T3 Stack 到自动生成
1.1 先从 T3 Stack 说起
t3code 这个名字,乍看像某个神秘代码仓库,其实它是近几年 TypeScript 全栈开发里的一个热门方向:以 T3 Stack(Next.js + TypeScript + Tailwind CSS + tRPC)为底座,把重复的全栈业务代码自动生成出来。我在实际项目里用了大概几个月,印象最深的是它把“写接口 → 联调 → 改类型”这一整串流程压缩成了“定义模型 → 生成代码 → 直接跑”,适合那些已经能写基础 Next.js 应用、但不想在 CRUD 上反复磨时间的开发者。
先解释一下背景。T3 Stack 是 Next.js 社区里一套非常成熟的组合拳,核心是 TypeScript 全栈类型安全:前端用 Next.js 做页面,后端直接用 Next.js 的 API 路由或 Server Component,数据库层用 Prisma 定义模型和迁移,前后端通信则交给 tRPC——它最大的特点就是不需要写 REST API 的文档和类型定义,客户端调服务端方法时,类型是自动推导的。这套组合对个人项目和中小型团队来说确实高效,但问题也随之而来:每开一个新项目,你都要把用户、文章、评论这些常见模块的增删改查重新写一遍,光初始化目录和搭 tRPC router 就要花掉不少时间。
t3code 就是来解决这个痛点的。你可以把它理解成一个“全栈脚手架增强器”:它先读取你定义的数据模型(一个类似 Prisma schema 的描述文件),然后帮你生成整套业务代码——数据库模型、查询字段类型、tRPC 路由、页面组件、表单逻辑、鉴权判断,全部一次性输出。和传统脚手架最大的区别是,它生成的是“有业务逻辑、能直接改的活代码”,不是堆了一堆 hello world 的静态模板。
1.2 t3code 解决的三个痛点
先说痛点一:类型不同步。手工写 API 的时候,后端改了字段名,前端调用处往往要等运行时报错才能发现。但基于 t3code 生成的代码,前后端类型全部来自同一份 schema 文件,字段在模型里删了,前端组件里的对应属性直接编译报错,上线前就能堵住问题。
痛点二是重复劳动。我实测下来,一套标准的用户管理模块,包含数据库 model、zod 输入校验、四个 CRUD 接口、列表页和编辑页表单,手写大概要 600 到 800 行代码。用 t3code 定义好模型后,生成时间基本在十秒内,而且结构统一,团队里的新人看了生成代码也能快速搞清楚项目约定。
痛点三是架构漂移。不少人做项目是从“先能跑”开始的,写到一半发现鉴权逻辑散落在各个页面里、数据请求方式五花八门。t3code 的好处是它强制你按“模型驱动”的方式来组织代码,目录结构统一、数据传输链路规范,后面加功能时心里有底。
不过这工具也不是万能药。它擅长的是“典型业务模块”的生成——只要你的需求是围绕数据的增删改查,它效率极高;但如果你的业务里有复杂的状态机流转、大量自定义前端交互逻辑,那生成代码只能帮你搭好地基,剩下的还得自己写。我给它定位是“高速公路的前半段”,不是终点,但能让你把力气花在真正有价值的地方。
2. 为什么用 t3code:类型安全这条主线的价值
2.1 类型安全不是玄学,是省时间的工具
很多朋友听到“类型安全”就头大,觉得是 TypeScript 党在制造优越感。我用生成器类工具做得多了,反而觉得类型安全本质上是“编译器帮你做需求追踪”:当你把一个字段从name改成nickname,全项目所有引用这个字段的地方会被标红,而不是等测试环境跑挂了再一个一个接口排查。这个价值在单人项目里是“少改几处”,在多人协作项目里就变成“不用频繁问队友接口返回什么字段了”。
t3code 在这一点的处理很聪明。它生成的前端类型不是单独拎出来的types.ts,而是直接从 tRPC 路由的返回值里推导出来的。也就是说,你在数据库模型里给User加一个avatar字段,生成器会同步更新 Prisma schema、tRPC 路由的返回类型、前端页面的 prop 类型三处。你在编辑器里同时打开这三个文件,能看到它们的变化是“连锁”的,改模型文件的时候,另外两处的类型引用会跟着报错或恢复,这种联动体验让“改一个字段导致前端挂掉”这种事基本绝迹。
这里要说明一点:类型安全真正的价值不在于“写代码的时候有提示”,而在于“重构的时候有兜底”。真正用起来之后,你会发现自己越来越敢改代码了——因为编译器会告诉你哪些地方忘了改,而不是靠回忆或者全局搜索。这也是我坚持在项目里使用这类工具的核心原因。
2.2 t3code 的生成策略:schema 驱动 vs 模板驱动
生成代码的工具大致分两派:模板驱动和 schema 驱动。模板驱动说白了就是给你一堆预设好的文件,你复制到项目里再手改,常见的脚手架大多是这种;schema 驱动则是你先定义数据结构的“真相”,由工具根据这个真相生成所有关联代码。t3code 走的是后者,而且比我想象得更彻底。
举例来说,你在 schema 里定义了Post模型,指定了标题是字符串、内容是富文本、作者关联User、创建时间是默认自动生成。t3code 生成的不只是数据库表,它还会生成对应的 zod 校验规则——比如标题最少 3 个字符、最多 100 个字符,这些规则直接用在表单校验和后端入参校验上,一套规则两头用。前端生成一个“新建文章”页面时,表单字段类型、校验提示、提交逻辑全都配好了;后端生成一个 tRPC 的createPost方法,入参会先过 zod 校验再写库。你不再需要“前端一套校验、后端一套校验”地重复维护。
这种策略的前期投入比普通模板高一点——你得先学会 schema 怎么写,但它把“改了模型以后所有相关代码一起变”这件事做到了极致。我自己的体验是,用了半个月之后,写新模块从“复制粘贴旧代码改名字”变成了“写几行模型描述,让工具生成,然后专注写业务逻辑”,心里轻松很多。
2.3 什么时候该用、什么时候不该用
我把 t3code 的使用场景分成三类,方便大家判断:
推荐场景是典型 CRUD。比如后台管理的用户、角色、订单、文章管理,结构常规、操作单一、字段直接映射表单,这类场景是生成器的舒适区,能省掉大量机械劳动。比较推荐给孩子团队内统一使用,因为生成的代码风格一致,新人上手成本低,代码评审时也不用纠结“这家写了个自定义 hook,那家直接在组件里 useEffect 请求数据”这种风格问题。
谨慎使用的场景是业务逻辑重、页面交互复杂的模块。比如审批流里有动态节点、拖拽排序、条件分支,这些逻辑很难用模型描述,生成的代码只是“壳”,核心还要自己写。这种情况下我的建议是:先用 t3code 生成数据层和基础接口,前端页面只保留列表和详情,复杂交互页单独手写,不要把生成结果“硬融”进去,否则改起来很痛。
不该用的场景其实也明确:一是你的项目有非常特殊的架构要求,比如不用 Next.js 的目录结构、数据源不是 SQL 而是第三方 API;二是项目已经成型且没有使用 tRPC/T3 Stack,引入生成器等于推翻重来。工具是服务项目的,不是反过来,这点想清楚就不会做出错误决策。
3. 上手实操:用 t3code 快速搭建一个带鉴权的全栈应用
3.1 环境准备与项目初始化
先交代一下基础环境。我本机是 macOS + Node.js 18+,包管理器用 pnpm,数据库选的是 PostgreSQL。不同工具在不同环境的坑,下一节详细说,这里直接给一个稳固的组合:Node 18 或 20 LTS 版本 + pnpm 8 + PostgreSQL 15。
初始化命令很简单,在项目目录外执行:
npx t3code init my-app cd my-app pnpm install这个命令会创建一个标准的 T3 Stack 项目结构,同时带上 t3code 的配置文件和模板目录。装好后可以先跑一下pnpm dev,页面能开、说明基础没问题,然后我们开始处理数据模型。
注意:如果你公司网络下载 npm 包很慢,建议先配好镜像源,因为后续安装依赖的量并不小。另外 Node 版本不要用太旧的,t3code 依赖的现代语法特性在 Node 16 上会出问题,我一开始就是在这上面浪费了半小时。
3.2 定义数据模型:示例与语法讲解
t3code 的数据模型文件默认生成在schema.t3(不同版本也可能叫t3code.config.ts,里面用类型标注来描述模型),本质上是 Prisma schema 再加了一些生成注解。我们先定义一个最基础的用户模型:
model User { id String @id @default(cuid()) email String @unique name String? role String @default("user") posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String createdAt DateTime @default(now()) }这一段和普通 Prisma 模型没有任何区别,关键在后面的生成注解。举例来说,如果你想给User生成一个完整的管理界面,可以这样扩展:
model User { id String @id @default(cuid()) email String @unique name String? role String @default("user") posts Post[] @@t3.generate("crud", { label: "用户管理", icon: "User", fields: { email: { show: true, searchable: true }, name: { show: true, editable: true }, role: { editable: true, input: "select", options: ["user", "admin", "moderator"] } }, auth: { role: "admin" } }) }我解释一下这些注解的含义:@@t3.generate是告诉 t3code 要为这个模型生成代码,crud表明生成完整增删改查;label用于菜单显示;fields里的配置决定某个字段在列表页是否显示、是否能搜索、编辑时候用文本框还是下拉;auth是访问控制,这里规定只有admin角色可以进入用户管理页面。这些都是我在真实项目里常用到的配置,理解之后就不需要太多文档查询。
3.3 执行生成命令
模型写好后,运行生成命令:
npx t3code generate你会看到命令行输出每个模型对应的生成结果,比如 “User: router created, pages created, schema updated”。生成完成后,去看项目结构,你会发现在src/server/api/routers下多了一个userRouter.ts,里面有完整的create、update、delete、getAll、getById五个 tRPC 方法;src/pages下多了用户列表页和编辑页,带有表格、搜索框、分页和表单;prisma/schema.prisma也被更新了,新增了刚才定义的模型。
生成后先不要急着改代码,我的习惯是先把 Prisma 迁移跑起来:
npx prisma migrate dev --name init这一步会把数据库表建好。然后启动开发服务器,打开浏览器访问/admin/users,你就能看到完整的管理界面——列表展示邮箱、姓名、角色,支持搜索和新增。整个过程从初始化到页面跑通,熟练之后十分钟内能完成,这速度在以前手写是不可想象的。
3.4 跑起来看效果与后续操作
页面能跑通之后,核心的“看到效果”环节就完成了。我建议你此时打开 t3code 生成的代码逐行读一遍,重点关注三个文件:tRPC router 的输入输出校验、页面里用的trpc.user.getAll.useQuery()的调用方式、以及EditUserForm组件里的表单校验规则。读代码不是浪费时间,能帮你理解生成器的产出逻辑,后面手动改代码时才不会一头雾水。
如果你要做的模块带子表关系,比如“每个用户可以有多篇文章”,我建议在模型里把Post也加上@@t3.generate注解,并把authorId标记为来自当前登录用户的关联字段:
@@t3.generate("crud", { fields: { authorId: { type: "relation", relationTo: "User", default: "currentUser" } } })这样生成出来的“新建文章”页面会自动填充当前登录用户作为作者,不需要你在代码里手动塞用户 ID。这个技巧在博客后台、订单管理等场景里非常实用,实测能少写不少胶水代码。
4. 核心配置拆解:路由、鉴权与目录约定
4.1 生成结果的目录结构
了解 t3code 生成的目录,对你后续手动扩展业务很有必要。标准结构大致是这样:
src/ ├── pages/ │ ├── admin/ │ │ ├── users/ │ │ │ ├── index.tsx // 用户列表页 │ │ │ └── [id].tsx // 用户编辑页 │ │ └── posts/ │ │ ├── index.tsx │ │ └── [id].tsx ├── server/ │ ├── api/ │ │ └── routers/ │ │ ├── userRouter.ts // 用户 CRUD 路由 │ │ └── postRouter.ts // 文章 CRUD 路由 │ └── db/ │ └── schema.prisma // 数据库模型 ├── components/ │ ├── admin/ │ │ ├── DataTable.tsx // 通用数据表格 │ │ └── FormField.tsx // 通用表单控件 └── types/ └── t3code.ts // 生成器的类型声明这个结构和我手工搭建的 T3 Stack 项目几乎一致,只是多了admin目录和t3code.ts类型声明文件。使用 t3code 之后,你的团队可以形成一个共识:后台管理页面统一放在pages/admin,业务路由统一注册在routers目录,代码结构的一致性完全不用靠 code review 时苦口婆心地强调,因为生成代码本身就是规范。
4.2 常用配置项解析
如果你上手后想深入定制,重点看这几个配置,它们出现的频率最高。第一个是auth字段,它决定了访问权限:
auth: { role: "admin", // 指定角色 or: ["admin", "moderator"], // 多角色任意一个 }生成器会自动在页面上加上权限判断组件,在 tRPC router 里加上requireRole中间件。你不需要额外写鉴权逻辑,只需在用户登录时把role字段正确存入 session。
第二个是fields里的input选项,决定表单控件类型。text是单行文本、textarea是多行文本、select是下拉、number是数字输入、date是日期选择器。像“状态”这种枚举值字段,我习惯用select配合options配置,生成的表单就是下拉框,不用手写 HTML<select>的选项列表。
第三个是searchable和showInTable选项,控制列表页展示哪些列、哪些字段参与搜索。字段很多时建议只把核心字段设为可搜索,避免生成的 SQL 查询在数据量大时变慢。我自己处理过一个用户表有 20 多个字段的模型,只在列表里展示了邮箱、姓名、角色三个字段,页面干净,查询也快。
4.3 自定义模板:让生成器长成你的形状
t3code 有一个高阶玩法:模板自定义。默认模板生成的页面风格是偏简洁的通用后台风格——白底、表格、左侧菜单。如果你的公司有设计规范,希望在表单控件上加 “必填红星” 或特定样式的按钮,默认模板就不够用了。好在 t3code 把模板文件暴露在t3code-templates目录下,用的是 Handlebars 语法:
<div class="form-group"> <label class="form-label"> {{field.label}} {{#if field.required}}<span class="required-star">*</span>{{/if}} </label> {{{field.render}}} </div>你只改这个模板再执行npx t3code generate,整个项目的表单控件样式都会跟着变。团队有设计规范的话,第一次拿到项目先花费半天时间把模板调成团队风格,之后所有生成页面天然符合规范。这个投入非常值得,相当于把你的开发规范固化到了工具层,而不是靠团队成员自觉。
注意:改模板之前建议备份原文件。版本升级时模板可能会变化,直接覆盖会让你提交的自定义模板失效。我是把自定义模板放到 vendor 目录下的 git 子模块,升级时对比差异再合并。
5. 常见问题与排查经验
5.1 Prisma 或数据库相关报错
实际项目中遇到最多的就是数据库生成问题。症状一般是执行npx prisma migrate dev后报类似 “Environment variable not found: DATABASE_URL”。t3code 初始化项目时会在.env里生成一个默认的DATABASE_URL,但默认值通常是postgresql://user:password@localhost:5432/db,不会自动填你的实际账号密码。你需要到.env文件里改成真实值,这个坑基本人人都会踩一次。
还有一类是模型关系写错导致的迁移失败。比如我在一个模型里定义了两个User的外键字段(createdById和updatedById),Prisma 要求必须显式命名这两个 relation:
createdBy User @relation("CreatedPosts", fields: [createdById], references: [id]) updatedBy User @relation("UpdatedPosts", fields: [updatedById], references: [id])如果少了@relation名称,Prisma 会报 “The relation field must be defined” 的错误。遇到这种情况不用慌,对照 Prisma 官方文档中的关系配置,把每个外键字段的 relation 名字补齐即可。
5.2 tRPC 路由与类型不同步
使用一段时间后,你可能会遇到“明明数据库里有这个字段,前端就是不显示”的情况。这个问题的根因通常是 t3code 生成的类型缓存没有刷新。tRPC 的类型推导基于RouterOutputs类型,如果你改了 router 里的返回值,但编辑器里 import 的还是旧的类型,就会出现“运行时数据有、编辑器里报类型错”或者反过来“编辑器里有类型、运行时数据为空”。
排查步骤很简单:第一步,确认npx t3code generate执行成功且输出没有报错;第二步,杀掉 dev server 重新跑pnpm dev,因为 tRPC 的类型在 dev 模式启动时会被重新生成;第三步,如果还不行,删除node_modules/.cache和.next目录后再重启。我踩过一次这样的坑之后,现在每次生成完代码都习惯性重启 dev server,省了很多不必要的纠结。
5.3 生成后的“手改边界”与版本升级
用熟练之后,你必然遇到一个问题:生成的代码不够满足业务需求,想手改。我的经验是遵循“改模板而非改产物”的原则。比如列表页需要一个批量删除按钮,默认模板没有,你应该先搜索模板里列表页的实现,加上批量删除逻辑,然后重新生成,而不是在已经生成的index.tsx里去改。因为一旦你改了生成的产物,下次模型变更重新生成时,你的手改会被覆盖,而且是静默覆盖,容易埋雷。
版本升级也要留意。t3code 迭代速度不慢,我见过从 0.7 升到 0.8 后模板变量名变化的情况,旧项目重新生成会报找不到变量。我的做法是先跑一次npx t3code create-template把新版模板拉到一个临时目录,然后用diff对比新旧模板,逐一调整自己维护的自定义模板。说实话这过程有点繁琐,但比起直接升级导致全量页面样式崩掉,这种可控的方式稳妥得多。
5.4 前后端日期类型的时区坑
生成项目里如果频繁涉及日期范围查询,你会遇到一个隐蔽的 bug:前端传的日期字符串到后端变成DateTime后被当成 UTC,而页面显示时又不自觉加了 8 小时。t3code 默认生成的日期控件用的是<input type="date">,取值是yyyy-mm-dd字符串,Prisma 存到 PostgreSQL 后是带时区的timestamptz,前端Intl.DateTimeFormat一格式化,日期就多了一天。
我的解法是:在 zod 校验层把日期统一成 UTC 日期格式,再进数据库:
z.string() .regex(/^\d{4}-\d{2}-\d{2}$/) .transform((v) => new Date(`${v}T00:00:00.000Z`))这个 transform 把“当天零点零时零分 UTC”存进去,查出来再格式化时就不会出现跨天偏移。如果你只需要按天分组统计,这种处理足够干净。
结语与一点个人体会
最后分享一些我在真实项目中使用 t3code 的体会。任何代码生成器都解决不了“不清楚自己要做什么”的问题——工具能帮你把 CRUD 跑通,但业务逻辑、交互细节、异常处理这些仍然需要人来决策。我最舒服的用法是把它当作“项目骨架生成器”,所有数据模型敲定之后,先用它把完整的后台管理跑通,然后以生成代码为参考,把精力集中在一两个复杂的定制页面上。
还有一个建议是,团队使用时一定要统一 schema 的编写规范,尤其是注释和字段命名。因为 schema 就是团队成员之间的“活文档”,字段写清楚了,后面生成的代码才可读。我见过因为字段命名随意(a1、b2这种)导致模板变量名变得不可理解的情况,重新生成的代码基本没法看。
如果你的项目刚好是 T3 Stack 体系,又厌倦了反复重写相同的增删改查,不妨花一个下午试试 t3code。先把一个小模块跑通,感受一下“定义模型 → 生成 → 直接调试”的节奏,再决定要不要全量迁移。工具毕竟是工具,能解放你用于重复劳动的时间,但要把它用得顺手,还需要你在真实业务里反复调整模板和配置,找到适合自己的工作流。这也是所有高效率开发者的共同思路——让工具适应团队,而不是让团队迁就工具。