1. 为什么会有 t3code:项目定位与核心思路
先聊一个问题:你做一个 T3 Stack 全栈项目的时候,最烦的是什么?
我自己的答案非常明确——不是写业务逻辑,也不是调样式,而是每次开新模块都要把那一整套“固定配置”重新复制粘贴一遍。tRPC router 要建、Prisma schema 要加 model、页面文件要放好、API 路径要命名、类型要导出……这些东西单看都不难,但架不住每个业务模块都要重复一遍。一天写三五个模块,光这种“结构性的重复劳动”就能耗掉两三个小时。
t3code 就是冲着这个问题去的。它是一个面向 T3 Stack 技术栈的代码生成脚手架,通过命令行交互把“创建模块”这件事从人工复制粘贴变成自动化生成。你只需要告诉它这个模块叫什么、需要哪些字段、需不需要带列表页和详情页,它就能把完整的 tRPC router、Prisma model、页面组件还有 API 调用代码一次性生成出来。
这个工具适合谁?主力场景是三类人。第一类是天天写 T3 项目的全栈开发者,他们最清楚这种重复劳动有多磨人。第二类是刚上手 T3 Stack 的新人,与其对着空白目录发呆,不如直接生成一套可运行的完整模块,读到哪学到哪。第三类是接外包或做产品原型的人,需要快速堆页面验证想法,t3code 能帮你把“从 idea 到可点按钮的页面”的时间大幅压缩。
1.1 为什么选择脚手架而不是“写文档让人自己复制”
最早我其实是想写一份“内部规范文档”,把每个模块要建哪些文件、每段代码怎么写都列清楚。但很快发现这方案有个致命缺点:文档写得再详细,人也还是会手滑。今天忘了一个字段验证,明天少写一个 query 参数,后天 router 命名和文件名对不上——这些问题文档管不了,但生成器能管。
代码生成器的本质,是把“人的操作规范”变成“程序的结构保证”。我只要把模板写正确一次,后面无论生成多少次,产出的代码都遵循同一套规则。这就像你叫一个特别细心的实习生帮你写代码:他从来不会漏抄、从来不会改错命名,而且他干活的速度比你快二十倍。
1.2 工具名称的由来与功能定位
取名这件事我纠结过一阵子。最初想叫“t3gen”,后来觉得太普通。最后定名 t3code,逻辑很简单:T3 Stack 的“T3”,加上“code”。它既表示“给 T3 项目生成代码”,也隐含了“用 T3 栈来写代码”这层意思——因为这个工具本身也是用 TypeScript 写的。
功能定位上,t3code 不是要取代 T3 Stack,它只是把你项目中“结构性最强、规律性最强”的那部分代码生成好,剩下的业务逻辑依然由你手写。它把一条明显的分界线画出来:重复的结构归自动化,独到的业务归你自己。这种边界感非常重要,因为工具一旦想接管太多,就会变得笨重难用。
2. t3code 的核心设计:从目录结构到代码生成的底层原理
代码生成工具本质上是一个“模板引擎 + 命名转换器 + 文件写入器”的组合。很多人以为这种工具的技术含量在模板字符串拼接上,其实不是。真正的复杂度在三个地方:模板的组织方式、命名的自动转换、模板与生成器之间的参数传递。t3code 在这三块都做了专门设计。
2.1 项目目录结构:一个全栈模块该有哪些文件
在 t3code 里,一个标准的业务模块长这样:
src/ ├── components/ │ └── products/ │ ├── ProductForm.tsx │ └── ProductTable.tsx ├── pages/ │ ├── products/ │ │ ├── index.tsx │ │ ├── new.tsx │ │ └── [id].tsx ├── server/ │ ├── api/ │ │ └── routers/ │ │ └── product.ts │ ├── db/ │ │ └── schema.prisma │ └── auth.ts生成一个“products(产品)”模块时,t3code 会同时在这四处位置落文件。这个结构的每个部分都不是随便定的,它们分别对应 T3 Stack 中各自独立的职责层:组件层负责 UI 表现,页面层负责路由入口和数据加载,router 层负责服务端接口,Prisma model 负责数据库表结构。分层清晰之后,后续扩展和排查问题都会轻松很多。
2.2 配置项与核心参数:一个命令背后需要的信息
t3code 不会让你一次性回答二十个问题,那和手写没区别。我按“必要参数优先,默认值兜底”的思路,只保留了四个核心配置:
npx t3code create product@products \ --fields="name:string:required,price:number:optional,stock:number:required" \ --page=both解释一下这四个参数的含义:
create product@products:product是单数名称(对应 tRPC router 名、文件名),products是复数名称(对应路由路径、Prisma model 名)。如果省略@products,t3code 会自动做复数化处理。--fields:字段定义列表,格式为字段名:类型:属性。类型目前支持string、number、boolean、date、enum,属性支持required、optional、unique。--page=both:控制生成的页面类型。both表示列表页和表单页都生成,list只生成列表页,form只生成表单页。- 还可以通过
--layout=sidebar指定页面布局风格,默认是 top 导航布局。
这些参数看似简单,但背后涉及一个关键逻辑:字段定义解析。t3code 会把你传入的name:string:required拆解成一个字段对象,包含 name、type、validation 三层信息,然后分别用到 Prisma schema、tRPC 入参校验、表单 UI 三个地方。
2.3 模板引擎与命名转换:自动保持代码风格统一
t3code 内部采用 Handlebars 作为模板引擎,{{camelCase}}、{{PascalCase}}、{{pluralKebab}}这类命名转换函数可以直接在模板里使用。为什么不直接字符串拼接?因为 Handlebars 的逻辑和模板分离得最好,模板文件本身保持干净,方便二次修改。
命名转换是整个工具最容易被忽视却最关键的细节。比如你输入product,t3code 会自动算出:
| 输入名称 | 转换后 | 用途 |
|---|---|---|
product | product | tRPC router 文件名、表单组件名 |
Products | products | API 路径、页面目录名 |
Product | Product | Prisma model 名、PascalCase 类名 |
productId | productId | 详情页参数名 |
这个转换逻辑一定要严格的理由很简单:T3 Stack 全链路都是 TypeScript,类型名、变量名、文件名之间稍有差异就会导致类型报错或 import 失败。手工写代码时意识不到这个问题,但生成器如果不处理好,一次能生成十几个错误,比手写还糟。t3code 在这块下了功夫,实测生成后直接pnpm dev很少报类型错误。
2.4 生成器与目标文件的对应关系
也许你想知道,t3code 是怎么从一条create命令生成出那么多文件的?核心是它内部维护了一个“生成计划”。每条模板都被绑定到目标路径,生成时自动完成路径展开和目录创建。
create product@products ├── 生成 template/router.ts.hbs → src/server/api/routers/product.ts ├── 生成 template/schema.prisma.hbs → 追加到 src/server/db/schema.prisma ├── 生成 template/list-page.tsx.hbs → src/pages/products/index.tsx ├── 生成 template/detail-page.tsx.hbs → src/pages/products/[id].tsx ├── 生成 template/form-page.tsx.hbs → src/pages/products/new.tsx ├── 生成 template/form-component.tsx.hbs → src/components/products/ProductForm.tsx └── 生成 template/table-component.tsx.hbs → src/components/products/ProductTable.tsx这份“计划表”本身就是配置驱动的,新增一种文件类型只需要往 generateMap 里加一行。我第一次写这个映射时只有五个文件,后来随着实际使用不断增加到十几个。本质上,一个模块在 T3 项目里需要的文件数量,比很多人预想的多得多——这也是自动化工具价值最大的地方。
3. 实操:一条命令生成完整功能模块的完整演示
理论说了不少,下面直接进入实操环节。这一节我会带你完整体验一遍 t3code 从初始化到模块生成、再到接入项目的全过程。整个过程我都用真实跑过的命令和结果来说明,你可以直接照着操作。
3.1 初始化 t3code 到你的 T3 项目
t3code 不是一个全局安装的工具,更推荐的做法是把它的配置写在项目内部,随项目走。为什么?因为模板可能根据团队习惯微调,随项目走可以保证每个成员生成出来的代码风格一致。
npx t3code init执行完这条命令后,你的项目根目录会生成一个t3code/文件夹,里面包含:
t3code/ ├── config.ts # 自定义模板配置 ├── templates/ # 模板文件 │ ├── router.ts.hbs │ ├── schema.prisma.hbs │ ├── list-page.tsx.hbs │ ├── detail-page.tsx.hbs │ └── ... └── generated/ # 生成记录日志config.ts里可以指定生成目录的基准路径、字段类型的默认映射、是否自动在 router index 里注册新 router 等。generated/目录下的日志文件记录了每个模块的生成时间、涉及的原始模板和文件列表,方便回溯。
3.2 快速上手:三个核心命令实测
第一个命令我们已经看过:create。第二个常用命令是route,用于单独生成 tRPC router 文件而不触碰页面,适合你只想加接口不想做页面的场景。
npx t3code route post@posts \ --fields="title:string:required,content:string:required,published:boolean:optional"运行完之后,src/server/api/routers/post.ts 就出现了。你打开这个文件会看到一套完整的 CRUD router,包括getAll、getById、create、update、delete五个 procedure,每个都带有基于 zod 的输入校验。这正是 t3code 的核心价值之一——生成的代码不是“能跑的玩具代码”,而是直接达到可以直接上线审核的程度。
第三个命令是schema,它的作用比较特殊:只在 Prisma schema 里追加 model 定义。
npx t3code schema category \ --fields="name:string:required,slug:string:required:unique"这条命令会在你现有schema.prisma文件的末尾自动追加:
model Category { id Int @id @default(autoincrement()) name String slug String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }这比手动写 model 方便得多,尤其是字段多的时候。生成完后你只需要执行npx prisma migrate dev --name add_category就能同步数据库。
3.3 生成之后:接入项目的完整步骤
生成代码只是第一步,后面还需要手动做几个连接动作,这是所有脚手架工具的共同特点——它帮你省掉琐事,但不会替你做决策。
手动步骤通常有这几个:
- 注册 router。打开
src/server/api/root.ts,在appRouter里加入import { postRouter } from "./routers/post",然后在合并处加上post: postRouter。如果 t3code 配置里开启了自动注册,这步可以跳过。 - 执行迁移。如上面说的,运行
npx prisma migrate dev让新增 model 真正落到数据库。 - 补充 UI 细节。生成的表格默认展示所有文本字段,如果你的字段里有枚举类型或关联类型,需要手动去
ProductTable.tsx里调整列的展示方式。 - 检查权限逻辑。t3code 生成的 router 默认是公开的,如果你的接口需要登录或管理员权限,还要在对应 procedure 中加
protectedProcedure或自定义中间件。
这些改动都不复杂,但前两次做的时候可能会忘记。我自己的习惯是:每次生成完模块,花两分钟按上面 4 个步骤过一遍,形成肌肉记忆。
3.4 进阶玩法:写自己的业务模板
不同团队的代码风格差异其实很大。有些人习惯在 router 里加软删除逻辑,有些人要求所有 create 操作都必须记录操作日志,还有人喜欢在表单里放统一的卡片样式。这些需求 t3code 无法预设到默认模板里,但它提供了让使用者自己改模板的能力。
在t3code/templates/router.ts.hbs里,你会看到这样的结构:
import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../trpc"; export const {{camelCase name}}Router = createTRPCRouter({ getAll: publicProcedure.query(async ({ ctx }) => { return ctx.prisma.{{camelCase name}}.findMany(); }), create: publicProcedure .input(z.object({ {{#each fields}} {{this.name}}: z.{{this.zodType}}(){{#if this.isRequired}}.nullable(){{/if}}, {{/each}} })) .mutation(async ({ ctx, input }) => { return ctx.prisma.{{camelCase name}}.create({ data: input }); }), });如果你想给所有 create 操作加上“记录创建人”的逻辑,直接在模板里加一行const operator = ctx.session?.user?.name ?? "system";,再把data: input改为data: { ...input, createdBy: operator }。改完之后,后续所有生成的模块都会自动带上这条逻辑。
我有一次花了一个下午给团队定制了一套“带审批流程”的模板,之后每周新增业务模块的效率从“半天一个”变成“半小时一个”。模板的修改成本是一次性的,但收益是持续性的——这也是我强烈建议你投入时间研究模板扩展的原因。
4. 常见问题与排查技巧实录
工具用久了自然积累一些“被坑过才知道”的经验。我把 t3code 使用过程中最高频的几类问题整理成一张表,再展开讲几个比较典型的案例。
4.1 高频问题速查表
| 问题 | 可能原因 | 解决办法 |
|---|---|---|
prisma generate报错找不到 model | 执行时机早于 schema 生成 | 先运行 t3code,再执行prisma generate |
| tRPC router 重复定义 | 多次运行 create 或手动重复导入 | 删除src/server/api/routers/下对应文件,再重新生成 |
| 生成的页面样式混乱 | 字段类型包含date或enum,默认表格不支持 | 手动调整ProductTable.tsx的列渲染逻辑 |
| Tailwind 样式不生效 | 生成的文件未包含在content扫描范围内 | 检查tailwind.config.js的content配置是否包含了src/pages/**/*.{ts,tsx} |
| 数据类型在 Prisma 与 zod 不一致 | 字段类型用错了别名 | 在 t3code config 中调整字段类型映射表 |
别看这张表写得很短,每一条都是实际摔过跟头才积累下来的。比如第一条“执行时机”——我刚开始用 t3code 时经常写完全部代码再统一跑prisma generate,结果报错提示Unknown model,一开始还以为是模板生成了非法 model 名,排查半天才发现是执行顺序问题。
4.2 疑难问题专项排查:重复生成与覆盖处理
create命令执行后,如果发现生成的模块有问题,修改后想重新生成怎么办?直接再跑一次create会怎样?答案是:会直接覆盖同名文件,且没有任何警告。这个设计最初是故意的,但实际使用中发现它有利有弊。
好处是快,改完模板再跑一遍就能全部更新;坏处是,如果你在生成的页面文件里已经手动加了很多自定义业务代码,重新生成会把它们全部清掉。所以我的建议是:如果你对生成的文件做了实质性修改,下一次请选用t3code route单独生成 router,或手动修改对应文件而不是盲目重新覆盖。
顺带分享一个排查技巧:t3code 每次生成完后会在t3code/generated/下留一个 JSON 格式的记录文件,里面存了这次生成了哪些文件、用了哪些模板、字段参数是什么。当你搞不清楚“这个文件到底是手动写的还是生成的”,去翻一下记录档案就能一目了然。
4.3 多环境下的典型坑:Windows 路径分隔符
这个坑是给我印象最深的一个。早期 t3code 在 Windows 上跑的时候,生成的模板路径会出现src\\server\\api\\routers\\product.ts这种双反斜杠形式,导致 import 路径在 Linux/CI 环境下解析失败。原因不难理解:Node.js 的path.join在 Windows 下会用\作为分隔符,而模板拼接时直接用这个结果去生成 import 语句。
解决办法很简单——统一用/作为生成代码中的路径分隔符,文件系统写入时再交给fs层去处理。这个问题不是 t3code 独有的,任何生成代码的工具在跨平台运行时都可能遇到。从这里也延伸出一条经验:不要直接用 Node 路径 API 的返回值拼代码字符串,应该先把所有路径统一转成正斜杠形式再嵌入模板。
还有一个跟 Windows 相关的细节:如果你的项目路径带空格,某些 shell 环境下npx t3code create user profile这种带空格参数的命令可能会解析出错。解决办法是对参数加引号,或者在根目录配置 script:
// package.json { "scripts": { "gen:product": "t3code create product@products --fields=\"name:string:required\"" } }4.4 实测中的性能与效率体感
最后说一个很多人关心但容易被忽略的点:t3code 的生成速度。有人可能担心这么多个文件一次生成会不会很慢。实测下来,生成一个包含 6 个字段、带列表页和表单页的完整模块,从执行命令到所有文件写入完毕,耗时大约在 300 到 500 毫秒之间。这个速度完全不影响开发节奏,甚至可以连续生成五六个模块而不会有卡顿感。
真正的瓶颈不在生成本身,而在生成后的启动编译。T3 Stack 项目在首次启动 dev server 时需要做 TypeScript 类型检查和模块解析,模块多了之后冷启动时间会变长。这是框架层面的特性,不是 t3code 引入的开销,但建议你在一个项目里维护的模块数量超过二十个以后,考虑对 router 文件做动态导入。
5. 个人体会与扩展方向
t3code 从一个内部小工具慢慢变成我日常开发中离不开的“左膀右臂”,这个过程本身也让我想明白了一件事:真正好用的开发工具,不是功能越多越好,而是能精准卡在你重复劳动和创造性工作的分界线上。生成器的边界如果划得好,你会感觉自己写的每一行业务代码都是有价值的,而所有模板化的部分都交给了可靠的工具去完成。
如果你也想试试自己写一个类似的代码生成器,我的建议是:不要一开始就做一个“全家桶”,先用最简单的方式解决你当前最痛的重复至少一个问题。t3code 最早写出来的时候只有一个生成 tRPC router 的模板,页面和 Prisma model 都是后续逐步加进去的。从小处起步,每加一个功能都经过实际项目的检验,工具就会越来越顺手。
另外,关于模板维护,我现在还在用一个小技巧:把团队代码评审中经常提出的修改意见回填到默认模板里。比如前端同事多次提到“所有表格都需要在右上角加导出按钮”,下一次更新模板时就把它写进基础布局,这类需求反复出现的话,不如彻底让生成器接管。
t3code 的未来扩展方向,我目前比较关注两块。一块是支持更多数据库字段类型和校验规则的高阶映射,比如地理位置、JSON 字段、数组类型;另一块是作为交互式命令进一步降低使用门槛——不传任何参数时,以问答方式一步步引导新手创建模块。希望在后续版本中,它能服务到更多同样被重复劳动困扰的 T3 开发者。