☰
t3code:从Prisma模型到全栈CRUD代码的自动生成工具
2026/10/27 7:06:43 网站建设 项目流程

说出来你可能不信,我做这个工具的直接原因,是某个周五晚上连续给第6个内部项目搭同一套CRUD接口。全栈开发干了几年,最耗神的从来不是那些需要想清楚的设计问题,反而是"复制一份user路由,把字段从name改成title"这种没有任何智力含量的机械劳动。那天我盯着屏幕问自己:如果这些都能自动生成出来,我为什么要手动复制十遍?

t3code就是这么来的。它的目标很简单:让一个TypeScript全栈项目从零到“能跑通”的时间压缩到三分钟以内。这里说的“能跑通”,不是create-t3-app那种给你一个空壳页面,而是直接把Prisma模型变成完整的后端RPC接口、前端调用代码和基础管理页面,整条链路都能动起来。这篇文章我把工具内部的实现思路、踩过的坑、设计取舍都拆开讲一遍,适合被重复样板代码折磨的全栈开发者、想在一天内出产品原型的独立开发者,以及时不时要交付内部系统的团队参考。

1. 我到底烦透了什么,才决定自己写一个

1.1 样板代码的本质问题

先说一个反直觉的结论:样板代码本身不可怕,可怕的是它需要跟着业务一起变。

一套CRUD接口看起来简单,但真落到代码里,至少涉及这么几层:数据库模型定义、输入校验逻辑、服务层业务代码、tRPC路由注册、前端API封装、页面表格与表单。这六层里任何一层字段变了,其余五层都得跟着同步改。手动同步的时候,漏掉任何一个文件,跑起来就是类型报错或者运行时才暴露的数据问题。

我最开始也用过市面上现成的代码生成插件,但它们的痛点在于:要么生成完就完事,和项目里的Prisma schema完全脱节;要么生成的代码封装太重,跑起来一堆抽象,出了bug根本不知道去哪改。我更想要的,是一个直接和我的数据模型绑定、生成出来就是我平时手写风格的代码的工具。

1.2 create-t3-app为什么只解决了一半的问题

T3 Stack社区里最常用的脚手架是create-t3-app,它最大的价值是把你从"怎么把Next.js、tRPC、Prisma、Tailwind拼在一起"的配置地狱里解放出来。跑完create-t3-app,你会得到一个结构干净、类型安全的空项目,里面甚至预置了简单的healthcheck路由。

但你冷静想一下,它解决的是项目的"出生"问题,没有解决"成长"问题。接下来你要做的第一件事,通常就是去Prisma schema里定义第一张业务表,然后手动为这张表写路由、写校验、写页面。这恰恰是机械劳动最密集的地方。第一次手动做这套流程还觉得新鲜,做到第五次就会产生一个很自然的念头:这中间完全可以把"模型定义"作为唯一事实来源,其他所有东西都由它推导生成。

1.3 t3code想补齐的那段路

我想补齐的,就是从schema到业务的最后一公里。

具体来说,当我写下一张Product表,t3code应该能自动推导出:对应的tRPC路由需要哪些输入参数、输出类型应该是什么、前端页面里表格应该展示哪些列、搜索框应该按哪些字段查、新增和编辑表单里每个控件应该用什么类型。它会把这些组装成一份可以直接run起来的完整代码,而不是零零散散的文件碎片。

这里的关键词是"推导"。不是把静态模板塞给你,而是读取Prisma schema里的模型、字段、类型、关系,结合内置约定生成对应的业务代码。这也是t3code和普通代码片段插件最大的区别:它输出的不是例子,而是真正符合你当前模型定义的可运行代码。

2. t3code到底做了什么:从设计目标说起

2.1 三个必须满足的硬性要求

这个工具一开始就给自己定了三条规矩,后来证明这些规矩直接决定了它的可用性。

第一条,生成出来的代码必须是"人样",就是像你自己手写、并且有一定工程素养的人会写出来的那种。我不想要那种加了一大堆抽象基类、继承链绕三层的生成框架。代码是给你改的,不是给你膜拜的,平铺直叙有时候是最好的。

第二条,生成动作必须可以反复执行。项目里给User表生成过接口之后,过两天我可能又在同一张表上加了一个字段,此时重新跑一次生成命令,不应该把我的手动改动冲掉,也不应该生成一份重复的路由定义。这个约束直接导致了后面第二章节里要讲的"幂等生成"策略。

第三条,生成结果不能锁死技术栈。这个工具默认生成的是Prisma + tRPC + React Query的组合,但模板引擎本身是分层的,换一层适配器就能生成其他风格。实际上我在用的时候也给它配过Drizzle ORM的变体,后面会提到。

2.2 命令设计与使用场景

t3code的交互方式遵循一个原则:让最常用的路径只需一条命令。

命令作用典型场景
t3code init在当前目录生成项目骨架,并自动安装依赖从空目录启动新项目
t3code model list读取prisma/schema.prisma,列出所有模型及统计信息确认工具正确解析了数据模型
t3code generate为所有模型生成RPC路由、API封装与页面文件日常开发的主力命令
t3code generate --model User只处理指定模型增量修改单张表
t3code sync根据schema模型增删情况,清理已废弃的生成文件模型改名或删除后

第一次拿到这些命令的开发者,通常的疑问是:那我自己手写的业务逻辑怎么办?答案很简单,t3code只维护它生成的文件头标注了"generated by t3code"的部分,这些文件默认放在src/generated目录。你自己的业务代码放在src/features等其他目录,两者物理隔离,通过导出合并。生成代码负责把数据通道铺好,你的业务代码在通道基础上做定制,互不干扰。

2.3 为什么取名叫t3code,而不是别的名字

灵感来源就是T3 Stack的命名习惯,同时这个工具又落点在"生成代码"这件事上。T3 Stack一直被社区称为"快速开发全栈应用的最短路径",但实际用下来你会发现最短路径只覆盖了项目骨架,业务代码依然要靠手写。t3code这个名字想传递的意思很直接:它把T3的路径进一步往业务方向延伸,让"从schema到业务代码"这段路成为自动化的一部分。

取名这件事的教训是:命名不要太玄乎。工具名字最好能让人一眼看出它解决什么问题,一看t3code就知道这工具和T3技术栈有关,而且核心动作是产出代码。

3. 实现细节里最关键的三个设计决策

3.1 为什么坚持"生成后不再覆盖"

这是这个工具设计上最重要,也最和直觉冲突的一个决策。

很多代码生成工具死掉,都是因为一个场景:开发者生成了一版代码,然后手动改了其中三处,这时候模型加了个字段,重新生成直接把手动改动覆盖了,气得想砸电脑。为了不覆盖,它们设计了各种复杂机制——区块标记、冲突合并、版本比对,但最终都会滑向不可预测。

t3code的处理方式比较"笨":生成的文件一旦存在于磁盘上,就不再次覆盖。重新运行generate命令时,工具会读取当前Prisma模型,对比目标文件是否已存在,如果存在就跳过,同时记录一条日志。那新增字段怎么办?通过diff命令生成一个增量补丁文件,你用编辑器合并到已有的生成文件里,或者手动改动那些生成的代码后,运行generate只生成缺失的新文件。

实测下来这个"笨"方案比花哨的冲突合并机制好用得多:你永远知道自己动过什么,生成器永远不会趁你不注意改掉你的代码。从工程角度看,生成器像个只负责铺路的工人,之后翻新路面是你自己的事。

3.2 模板引擎选型:字符串模板胜过AST重写

一开始我碰过用TypeScript编译器API直接做AST改造的路子:解析源文件、插入导入语句、添加路由定义、改写类型导出。这套方案原理上行得通,也最“优雅”,但是很快发现两个致命问题。

第一个问题是,AST改造的规则极度脆弱。Prisma模型稍微复杂一点——加了关系、加了默认值、加了复合唯一索引——AST变换规则就指数级膨胀,你永远在修边界情况。第二个问题是,生成出来代码的格式和项目里的其他代码风格很难保持一致,prettier跑一遍勉强能看,但可读性依然差。

后来我换成了最原始的字符串模板方案:用模板字面量拼接文件内容,再用prettier做一次格式化。代价是模板里会有一些长得难看的占位符循环,但收益巨大——模板的写法就是普通代码的写法,任何开发者都能读懂和修改模板本身,出了问题打日志看一眼就知道是哪段生成的,调试成本直线下降。对你而言,如果要抄这个方案,记住一条经验:模板里的可读性比生成逻辑的优雅程度重要得多。

3.3 命名空间与类型冲突:从源头上消灭脏数据

给多个模型同时生成路由时,最容易出问题的位置是tRPC路由命名空间。两个模型一个叫UserProfile,一个叫ProfileHistory,自动推断的短名称可能都落在profile附近,类型推导会变得一团乱麻。

t3code的解决办法是:每个模型生成的所有文件,统一使用模型名称的PascalCase作为前缀,比如UserProfileRouter、UserProfileApi、UserProfilePage,并且文件内部的类型命名也严格遵守"模型名+类型用途"的约定。这个设计看起来笨,但保证了全项目范围内类型和路由零冲突。

数据库关系里常见的命名冲突也被提前处理了。比如一个订单模型里有customer和merchant两个外键,分别指向用户表,还在generate阶段自动为它们生成带前缀的关联查询方法,customerUser和merchantUser,不会出现两个一模一样的user字段卡在一起。

4. 一次完整演示:三张表怎么从schema变成能跑的页面

4.1 准备环境

假设你有一台干净的Linux或macOS开发机,装了Node.js 18以上、pnpm和Docker。我们先创建一个演示项目:一个简单的博客后台,包含User(用户)、Post(文章)、Comment(评论)三张表,其中Post和User关联,Comment再和Post与User关联。

# 启动一个本地PostgreSQL docker run --name t3code-demo -e POSTGRES_PASSWORD=pass -p 5432:5432 -d postgres:16

然后在一个空目录里跑t3code init。这个命令会自动拉取一个经过定制的Next.js + TypeScript全栈模板,装上Prisma和tRPC,并把t3code自身的CLI挂到项目里。

mkdir t3code-blog && cd t3code-blog t3code init

如果你不想用Docker,用SQLite也一样,init的时候带上--db sqlite参数就行。我这次用PostgreSQL是为了让演示更贴近生产环境。

4.2 定义数据模型

打开prisma/schema.prisma,写入三张表:

model User { id String @id @default(cuid()) email String @unique name String posts Post[] comments Comment[] } model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) authorId String author User @relation(fields: [authorId], references: [id]) comments Comment[] } model Comment { id String @id @default(cuid()) body String authorId String author User @relation(fields: [authorId], references: [id]) postId String post Post @relation(fields: [postId], references: [id]) }

注意这里每个用户都有多个文章,文章又有多个评论,三层关系嵌套。t3code解析这个schema后,会在系统里构建出模型之间的关联图,生成的时候会参考这些关系来决定路由的嵌套查询能力。

4.3 跑生成命令,看它产出什么

t3code generate

这个命令执行几十秒后,默认的src/generated目录下会出现一整套文件:

src/generated/ users/ users.router.ts users.api.ts users.schema.ts users.table.tsx users.form.tsx users.page.tsx posts/ ... comments/ ...

每个模型目录下都是同构的六个文件。users.router.ts是tRPC服务端路由,包含list、getById、create、update、delete五个过程;users.schema.ts对应这些过程的输入校验规则,字段类型完全从Prisma模型推导而来,比如email自动匹配z.string().email();users.api.ts是前端调用封装,把所有RPC方法包装成React Query的hooks;users.table.tsx和users.form.tsx是列表表格和新增编辑表单;users.page.tsx把它们组装成一个完整页面。

t3code generate --model Post

如果后续只改了Post表结构,再跑上面这条命令,其他模型不受影响。增量生成的执行速度也会快很多。

4.4 把页面挂到路由上

生成的文件不会自动注册到Next.js路由表里,这是故意的:每个项目的导航结构差别太大,自动注册反而制造麻烦。你只需要在app/(dashboard)/posts/page.tsx里写一行导入:

export { default } from "@/generated/posts/posts.page";

tRPC根路由器那边也不需要手工修改文件,初始化的时候模板已经做了文件系统扫描式的路由合并,src/generated下的router文件会被自动汇总进根router。

4.5 端到端跑通验证

启动开发服务器:

pnpm dev

打开管理后台页面,这时候你会发现列表、分页、搜索、新增、编辑、删除功能都已经是可用状态。为了让展示更真实,我通过生成的创建表单往Post表里插了几条数据,界面上能即时看到记录出现。这个流程走下来,从干净的schema到有数据写入的完整页面,大约花了十几分钟,其中大部分时间是等待依赖安装。

再验证一下类型安全:去users.schema.ts里把email改成z.string().min(1),编辑器里立刻会提示前端API的类型不再吻合,TS类型系统在编译之前就把这处不一致拦下来了。这就是t3code"类型安全从schema一路传到底层组件"的直接体现。

5. 那些让我熬到凌晨的怪问题

5.1 Prisma标量类型和zod校验的映射鸿沟

第一个翻车现场是Prisma的Decimal类型。schema里字段类型写的是Decimal,数据库里存的是精确十进制数,可tRPC传输的时候默认走了JSON序列化,前端拿回来的是一个字符串包着对象的怪结构,表单里一填数字就报类型错误。

这个问题表面上看是简单映射,实际牵扯到整个校验链:Prisma类型怎么转换成zod类型、RPC输出序列化格式怎么保留精度、前端表格怎么格式化显示。最后我给出的方案是:所有数值字段默认映射成z.number(),同时在生成页面时针对Decimal字段自动挂一个自定义格式化组件。这个处理逻辑现在写进生成模板里了,但它让我意识到:任何代码生成器都逃不掉类型系统之间的"翻译"问题,这往往是调试成本最大的地方。

5.2 重复生成后的脏代码

我前面说生成器不覆盖已存在的文件,但版本更早期的t3code不是这个策略,它会直接重写文件。有一段时间,我每动一次schema,整个generated目录就会翻天覆地重写一遍,然后我在Vim里看到的diff是无尽的删除和重新插入,手动合并工作时心态直接炸了。

后来我换成"只在文件缺失时生成"策略,但又遇到了另一个问题:模型删掉了字段,旧文件里的残留代码不会被清理。于是加了sync命令,扫描当前schema里存在的所有模型,把已经不存在的模型目录整体标记为冗余,而不是直接删除——给你留一个确认期,确认无误后再手动清垃圾。

5.3 模板里的转义地狱

用TypeScript字符串模板写代码生成器,绕不开一个恶心体验:模板里要包含模板本身。比如我要生成一段包含${variable}插值的客户端API代码,生成器模板里就得写\${variable}。双重大括号、正则里的反斜杠、注解里的星号,各种符号叠在一起,模板文件的可读性会差到令人崩溃的程度。

我的处理经验有两条。第一,把模板文件拆成独立文件(.tpl.ts),而不是嵌在CLI的字符串变量里,这样编辑器能对模板文件单独做语法高亮,转义密度也降低不少。第二,凡是模板中超过一层嵌套的复杂片段,一律抽成独立的拼装函数,单元测试直接对着拼装结果断言。不这样做,生成器会在某个你完全没想到的角落里产出非法代码。

5.4 版本锁定:next、prisma和tRPC的三方夹击

代码生成器最怕依赖升级,因为模板会基于当前依赖版本的API编写。Next.js的App Router用法改过很多细节,tRPC从v10到v11改了初始化函数签名,Prisma从v5过渡到v6调整了部分类型推导行为。

一开始模板跟着最新版走,结果隔两周社区升级一个小版本,别人拉下来的模板就跑不起来了。现在的设计是把技术栈的具体版本直接固化在init生成的package.json里,模板和依赖版本绑定发布,升级模板后再单独跑一条t3code upgrade来统一推进。对使用者来说,这避免了"今天还能跑,明天全是类型错误"的尴尬。

6. 什么情况该用它,什么情况别硬用

6.1 我推荐的使用场景

如果你做的是内部后台、CRUD密集的管理系统、原型验证型产品,这类项目对业务字段形态的确定性要求低、对迭代速度要求高,t3code能把"从schema到页面"的周期压缩到分钟级。尤其是接踵而至的十几个内部需求,每个都要配五六张表的管理页面,这个工具几乎是为这种场景量身定做的。

独立开发者做side project也很值得试。你第一天把数据模型想清楚,跑一遍生成命令,剩下的时间和精力就可以全部投入在真正的差异化功能上,而不是在admin页面里填表格列名。

6.2 不建议硬用的场景

如果你的项目存在"复杂业务状态管理、强定制化的前端交互、大量跨表事务逻辑",这类场景下生成出来的平铺代码并不占优势。任何脚手架都替你解决不了业务规则的复杂程度,硬用生成器还得花更多时间改它生成的代码。

大型团队如果已经有自建的代码规范体系、代码评审中对命名风格吹毛求疵,我建议先把t3code的模板改成符合团队规范的形式再统一铺开。生成器有个天然优势:它是你团队编码规范的一次性具象化,定制一遍模板,所有人产出的一致性问题就少了一大半。但前提是你愿意花半天时间做模板定制。

6.3 一个被低估的使用方式:作为团队模板基准

最后分享一个我觉得被低估的用法。把t3code的模板仓库当作团队的"活文档",你想让团队成员写出什么风格的全栈代码,就把这个风格固化在生成模板里。新人入组,跑一次初始化,看到的就是标准写法的代码;审查代码时,凡是偏离生成约定的点,很容易发现问题。

这也是工具类项目比文档类方案强的地方:规范真正长在代码里,而不是躺在wiki里吃灰。

根据我自己的使用体验,这类代码生成器最重要的是先承认自己的边界:它不负责替你想清楚业务,只负责把已经想清楚的表单和字段搬运成代码。别把期望拔得太高,只把它当作一个永远不会疲倦的体力活外包工,你会发现它能省下的时间远超预期。如果你也想尝试同样的思路,建议从最让你烦躁的场景入手——生成一个最小的列表页,感受一下从schema到可点击页面的路径,剩下的自然就顺了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询