正文开始
一次在技术社区逛帖子的时候,我无意中在一个 GitHub Trending 列表里看到了t3code这个项目。那时候团队正好接了一个中后台项目,技术栈锁定了 Next.js + TypeScript + tRPC + Prisma 这一套 T3 组合,数据模型一张张列出来,足足有四十多张表。也就是说,光 CRUD 这一层,我就要面对“每张表配一套 API、一套前端调用、几个页面”的重复劳动。这种活儿做起来毫无技术含量,但特别耗时间,还容易因为手滑把字段写错。t3code的定位恰好就是“从 Prisma schema 直接生成全套 tRPC API 和 React 页面代码”,这不就是我正缺的东西吗。于是我从拉取源码到跑通完整项目,前前后后折腾了一周,把关键流程和踩过的坑都整理在下面。如果你也在 T3 技术栈里被 CRUD 折磨,这篇文章应该能帮你少走不少弯路。
1. 为什么我会盯上这个新出现的代码生成器
1.1 项目背景:四十几张表的 CRUD 让人头大
先说下我遇到的实际场景。项目是一个典型的运营中后台系统,用户、角色、菜单、订单、结算、配置、日志,这类模块占了绝大多数。每个模块的本质都差不多:一张数据库表,一组增删改查接口,几个列表和表单页面。如果按传统方式手写,一张十字段左右的表,你得维护:
- Prisma schema 里的 model 定义
- tRPC router 里至少 5 个 procedure(list、detail、create、update、delete)
- 输入校验的 zod schema
- 前端调用的 React Query hook 或 tRPC hook
- 列表页、详情页、新增/编辑表单页
- 相关类型定义
一套下来差不多是 6 个文件,平均每个文件 80 到 150 行。四十多张表就意味着几千行的模板代码,而且每个开发写出来的风格还不一样。有的人喜欢在 router 里直接写业务逻辑,有的人习惯单独拆 service 层;有的人字段命名规范,有的人createdAt能写成create_time。代码评审的时候,一半时间都花在纠正这些没有技术含量的差异上。
我当时的想法是,一定要找一个能把这块自动化的方案,把人的精力释放出来,留给真正有挑战性的业务逻辑。
1.2 现有工具为什么没能完全解决我的问题
在遇到t3code之前,我先评估了一圈现有的方案。
第一个想到的是create-t3-app。它确实是 T3 技术栈项目初始化的标准答案,交互式问答选好模块,一条命令拉起来一个结构完整的项目。但它的边界也很清楚:只管把项目骨架搭好,之后的路由、页面、CRUD 都得自己写。它解决的是“项目怎么开始”的问题,而不是“业务代码怎么少写”的问题。
接着看了Plop。这是一个很灵活的文件生成工具,理念不错,但它的工作方式更像是“你告诉我模板长什么样,我帮你照着填”。也就是说,所有模板都要自己维护,包括目录结构、命名规则、字段渲染方式。团队里没有专人去维护一套高质量模板的话,上线几天后模板就会和实际业务脱节,最后大家还是回到手写。
还试过Amplication。功能确实强,支持可视化建模、生成完整的后端服务和前端代码。但问题是对我们这种中小型项目来说太重了,它需要独立跑一个服务,而且生成的代码体系比较自成一派,跟我们已有的代码风格融合成本高。
这些工具各有各的道理,但都和我“只想在现有 T3 项目里快速生成业务代码”的需求差了一口气。
1.3 t3code 打动我的关键设计:schema 驱动
t3code让我眼前一亮的地方在于它的核心思路:把 Prisma schema 当作唯一的事实来源,然后把“从模型到接口再到页面”的整条链路自动打通。
说白了,它预设了一条完整的产品逻辑:你先定义好数据结构,它会帮你把这套结构“翻译”成可以直接跑起来的前后端代码。这不只是省了敲键盘的时间,更重要的是让数据模型和业务代码始终保持同步。你改一个字段,重新跑一次生成,API、类型、页面就全部对齐了,不存在“数据库加了字段,前端拿到的还是旧类型”这种经典脱节问题。
它适合的场景也很明确:T3 技术栈、以 CRUD 为主的中后台项目、希望团队代码风格统一的团队,以及像我这样想快速验证产品原型的个人开发者。当然,如果你的业务里充满高度自定义的复杂逻辑,或者不是 T3 技术栈,那就另当别论了。
2. 快速上手:四条命令拉起一个可运行的前后端项目
2.1 环境准备:Node 版本与包管理器选择
动手之前先把环境确认好。t3code对运行时是有要求的,我用的 Node 20 LTS 版本,跟它的兼容性最好。如果你还在 Node 16 或者更老的环境,大概率会在安装依赖阶段就遇到语法报错,因为项目本身和它的依赖都用了比较新的 JavaScript 特性。
包管理器方面,我强烈建议用pnpm。原因有两个:一是t3code的依赖树比较深,pnpm 的硬链接机制安装速度明显更快;二是它在处理 monorepo 和 peerDependency 时的行为更严格,生成的 lockfile 在不同机器上更容易保持一致。当然你用 npm 也不是不行,只是我在实测中发现 npm 在某些版本组合下容易出现第五章节会讲到的依赖冲突问题。
确认好环境后,顺手检查一下 Node 版本:
node -v pnpm -v输出能看到 v20.x 和 8.x 以上,就可以继续了。
2.2 初始化命令:交互式问答里有哪些坑
执行创建命令:
pnpm create t3code@latest my-app这时候会进入一个交互式问答流程,和create-t3-app的风格很像,但它的问题更多,而且每一问都直接影响后续能不能顺利生成代码。我把自己当时的选择列出来供参考:
- 项目名:默认取目录名,我直接回车。
- 包管理器:选 pnpm。
- 是否启用 Prisma:选 yes。这是
t3code能不能干活的前提,没数据库模型它就没有输入。 - 数据库类型:按实际来,我选了 PostgreSQL,它生成的连接串和迁移命令会跟着变。
- 启用哪些模块:我勾了 tRPC、NextAuth、Tailwind。如果你的项目不需要鉴权,NextAuth 可以不勾,少一层配置少一些烦恼。
- 是否生成示例模型:我选了 yes,这样第一次跑通后可以直接拿示例模型验证生成链路,跑通了再删掉换成自己的模型。这个选项强烈建议选 yes,空手验证生成器是在给自己找麻烦。
- 是否立即执行第一次代码生成:选 yes。
整个过程大概两三分钟,结束后它会自动安装依赖并创建初始数据库迁移文件。这里有个小细节:它生成的.env文件里数据库连接串是占位符,我记得第一次跑的时候忘了改,导致后面 dev 启动时报数据库连不上。要是你也碰到,别慌,回到.env把DATABASE_URL改成真实的连接地址就行。
生成的目录结构大致是这样的:
my-app/ ├── prisma/ │ ├── schema.prisma │ └── migrations/ ├── src/ │ ├── server/ │ │ ├── api/ │ │ └── root.ts │ ├── pages/ │ ├── styles/ │ └── t3code.config.ts ├── .env └── package.json注意src/t3code.config.ts这个文件,这是整个生成器的控制中枢,后面章节会专门讲。
2.3 启动和验证:第一步就跑通全链路
初始化完成后,进入项目目录启动开发服务器:
cd my-app pnpm dev打开 http://localhost:3000,你会看到默认的欢迎页。如果初始化时选了示例模型,这时候页面上应该已经能看到一组由生成器产出的“示例数据管理”入口,包括简单的列表和新增功能。我验证时直接试了新增一条数据,提交后刷新列表,数据正常落库并回显,说明从数据库到 API 到页面的整条链路已经通了。
第一次跑通的感觉其实是有点震撼的——我没写过一行业务代码,一个完整的数据管理闭环就转起来了。不过在刚开始的兴奋劲过去后,我也意识到一个问题:光会用默认模板远远不够,真要在项目里落地,必须搞明白它的配置和生成原理,否则遇到需求变化就只能干瞪眼。
3. 真正值钱的部分:Schema 驱动的代码生成实战
3.1 为什么最终选择生成而非手写
这里想认真聊一下“为什么”。我见过不少团队对代码生成器有偏见,觉得生成出来的代码很“脏”,不如手写干净。但我的实测体验是:在 CRUD 密集的场景里,生成代码的工程质量反而比大部分手写代码更稳定。
拿一个十字段的 model 举例。手写方案里,你至少要保证 zod 校验、Prisma 查询、前端表单三者字段数量一致,多一个少一个都能跑,但跑到某个角落就会悄悄出问题。生成器不存在这个问题,因为所有代码都来自同一份 schema 定义,字段天然对齐。
这很像是做 UI 时用设计稿自动生成代码,而不是拿鼠标一点点对像素。前者只要设计稿更新,产物自动更新;后者你永远会碰到“设计稿改了,落地页没改”的尴尬。代码生成器把这个逻辑搬到了数据模型上,schema 就是设计稿,生成的 API 和页面就是落地页。
3.2 配置 t3code.config.ts:所有行为都在这里控制
项目的核心配置文件是src/t3code.config.ts。这个文件控制着生成器的一切行为,包括读哪份 schema、输出到哪个目录、生成哪些模块、代码用什么风格。我当时的配置大概是这样的:
import { defineConfig } from 't3code' export default defineConfig({ prismaSchemaPath: './prisma/schema.prisma', outputDir: './src/generated', generators: ['trpc', 'react-query', 'ui'], importStyle: 'relative', formatOnGenerate: true, overwrite: false, })逐个说下这些字段的含义:
prismaSchemaPath:告诉生成器去哪里找数据库模型定义。默认是./prisma/schema.prisma,一般不用改。outputDir:生成代码输出到哪个目录。建议固定在一个目录里,方便统一管理和在 gitignore 时做排除。generators:要生成哪些类型的代码。trpc是后端 API,react-query是前端数据请求 hook,ui是页面组件。importStyle:生成的 import 语句用相对路径还是别名。我用relative是因为别名在某些 tsconfig 配置下容易出问题,后面踩坑章节会细说。formatOnGenerate:生成之后自动格式化代码。这个必须开,否则生成的代码可能因为引号、分号风格不一致,导致和你项目里 prettier 的规则冲突,提交 diff 会非常难看。overwrite:已存在的文件是否覆盖。我一开始以为越界覆盖越好,后来发现把它设成false更合理。因为你会需要在生成结果上做少量手写定制,用overwrite: true每次生成都会把手工修改冲掉。
配置文件改好后,不需要重启 dev server,生成器每次运行时都会重新读取配置。
3.3 从 Prisma model 到完整 CRUD 页面的生成链路
配置文件就绪后,执行生成命令:
pnpm t3code generate为了说清楚它会做什么,我举一个最常见的User和Post关系模型作为例子。假设你的schema.prisma长这样:
model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }运行生成器之后,它会依次执行几条流水线:
- 解析
schema.prisma,提取每个 model 的字段名、类型、是否必填、是否有默认值、模型之间的关联关系。 - 根据 model 生成 tRPC router,核心部分是 CRUD procedures。生成的代码大概长这样:
import { z } from 'zod' import { createTRPCRouter, publicProcedure } from '~/server/trpc' import { prisma } from '~/server/db' const userInput = z.object({ email: z.string().email(), name: z.string().optional(), }) export const userRouter = createTRPCRouter({ list: publicProcedure .input(z.object({ page: z.number().default(1), pageSize: z.number().default(20) })) .query(async ({ input }) => { return prisma.user.findMany({ skip: (input.page - 1) * input.pageSize, take: input.pageSize, orderBy: { createdAt: 'desc' }, }) }), detail: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { return prisma.user.findUnique({ where: { id: input.id } }) }), create: publicProcedure.input(userInput).mutation(async ({ input }) => { return prisma.user.create({ data: input }) }), update: publicProcedure .input(userInput.extend({ id: z.number() })) .mutation(async ({ input }) => { const { id, ...data } = input return prisma.user.update({ where: { id }, data }) }), delete: publicProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { return prisma.user.delete({ where: { id: input.id } }) }), })看到这里你应该明白了,所谓生成器,本质上就是“替你把最样板化的代码填好,包括 zod 校验、分页参数、orderBy 写法、id 提取”。自己写十次可能有一两次忘加分页,生成器永远不会忘。
- 生成前端调用层。每个 model 会有一个对应的 hook 文件,比如
useUser.ts,里面包好 list、detail、create、update、delete 五种调用方法,并补上 useQuery 和 useMutation 的常用配置。 - 生成基础页面组件。包括列表页骨架、表格列定义、新增/编辑表单。这个部分生成的代码更多是起点而不是终点,因为真实业务的页面往往会有自定义列、自定义交互,你需要在生成结果上继续加工。
整个生成过程跑完,outputDir里会多出几个子目录。我一般会先快速扫一遍生成的代码,确认关系模型的嵌套查询有没有正确处理。像User下面有posts这种一对多关系,生成器会在 list 或者 detail 的 include 里自动带上关联数据,这个特性在默认配置下是开启的,不用额外操心。
跑通生成链路之后,我心里基本有数了:常规 CRUD 确实可以不需要手写。但软件工程里没有完美方案,接下来的实战阶段,我踩了不少坑,下面这部分我觉得才是这篇文章里最值得你收藏的内容。
4. 实测踩坑记录:三类典型问题与完整排查路径
4.1 问题一:pnpm 安装时的 peerDependency 冲突导致安装中断
先说最轻量但最容易让新手直接放弃的一个坑。
我用pnpm create t3code@latest初始化时,有一台机器安装依赖中途直接报错退出,错误信息大概是ERESOLVE could not resolve,后面跟着一大堆 peerDependency 冲突的说明。当时我第一反应是“这工具不行,包依赖都理不清”,差点就放弃了。
静下心来看错误信息,才意识到问题只出在 Node 版本和 tRPC 版本匹配上。我那一台开发机装的是 Node 18 的早期版本,而t3code默认拉取的是当时最新的 tRPC 版本,它对 Node 的@types/node和部分 API 有隐式要求,pnpm 的严格模式就把这种不满足直接当成错误抛出来,而不是像 npm 那样打一个 warning 继续装。
排查链条很简单:
- 先看报错里提到的是哪个包,定位到
@trpc/server和@trpc/client; - 再查
node_modules/.pnpm里实际安装的 tRPC 版本,发现和我项目里 Next.js 要求的版本存在偏差; - 修复方案有两个:要么把 Node 升级到 20,要么在
package.json里加pnpm.overrides强制统一 tRPC 版本。
我最后升级了 Node 解决了,因为 Node 20 本来就在计划内。如果你不想升 Node,可以在package.json里手动声明 overrides:
{ "pnpm": { "overrides": { "@trpc/server": "10.45.0", "@trpc/client": "10.45.0" } } }然后删除node_modules和 lockfile,重新安装。这里要说个教训:不要手动去改 tRPC 的版本号来消错误,不然后面你会发现类型定义里全是坑。优先升级 Node,实在不行再用 overrides 对齐。
4.2 问题二:Prisma Client 没有重新构建,API 运行时报错
这个问题是我在实际项目中加第一个自定义 model 时踩到的,现象非常隐蔽。
我当时是这么操作的:在schema.prisma里新增了一个Order模型,然后直接运行pnpm t3code generate。生成命令成功了,页面也能打开,但一到调用 order 相关 API 的时候,服务端就抛PrismaClient is not defined,或者更准确地说,是运行时找不到PrismaClient这个构造函数的定义。
排查过程是这样的:
- 先看编译日志,确认 t3code 生成的文件里确实有
import { PrismaClient } from '@prisma/client',说明代码层面没有问题。 - 再看运行时日志,发现报错发生在
server/db.ts里new PrismaClient()这一步。这说明@prisma/client包本身没有匹配到客户端代码。 - 到
node_modules/.prisma/client目录下检查,才发现里面根本没有Order模型对应的客户端方法。原因是 Prisma 的客户端代码并不是安装依赖时自动更新的,必须先跑prisma generate把 schema 编译进客户端。 - 最终解决就一条命令:
pnpm prisma generate跑完后重启 dev server,API 恢复正常。
这个问题背后的逻辑是:t3code负责生成业务代码,但它不会去执行 Prisma 的客户端编译步骤。也就是说,你改完schema.prisma后,动作序列应该是“改 schema → 跑prisma generate→ 跑t3code generate”。顺序反了或者漏了中间那步,就会出现代码有生成但运行时炸掉的情况。
后来我在package.json里加了这样一段脚本,把这个顺序固化下来:
{ "scripts": { "generate:all": "prisma generate && t3code generate" } }以后只敲一句pnpm generate:all,就不会再漏步骤了。
4.3 问题三:tRPC 类型在 IDE 里失踪,tsc 却可以通过
这是最让我抓狂的一个,因为它属于“薛定谔的报错”:命令行pnpm type-check直接通过,代码也能正常跑,但 VSCode 里所有import { type RouterOutputs } from '~/server/api/root'这类语句全都飘红,说不存在这个导出。
一开始我以为是自己 tsconfig 配置改坏了,反复对比初始化项目的 tsconfig,发现配置没有差异。又以为是 VSCode 的 TypeScript 服务卡了,重启了好几次也没用。
后来我认真对比了两种环境下的行为差异,才意识到问题的根源:tsc默认只检查tsconfig.json里include指定的范围,而生成器输出目录src/generated恰好没有被包含进去。所以命令行检查时压根没看那些文件,自然也不报错。但 VSCode 打开项目时加载的是整个工作区,它找到生成文件里引用的类型别名~/server/api/root,发现路径解析不到,就直接标红了。
解决办法有两条路:
- 把生成目录加进 tsconfig 的
include,保证 IDE 能定位到这些文件:
{ "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/generated/**/*.ts" ] }- 或者像我一样在
t3code.config.ts里把importStyle改为relative,让生成代码不用依赖路径别名。
两条路我最后都验证过,都能解决。但我的建议是两条同时做:加include保证 IDE 一定能看到生成产物;用相对路径保证未来的生成代码不依赖别名解析环境。这就像出门既带伞又带雨衣,双保险。
4.4 三类问题的共性:生成器类工具都有“产物刷新”难题
把这三个问题摆在一起看,它们其实有一个共同点:生成器工具的产物更新时机和环境依赖。安装依赖不解决版本冲突、schema 改动不更新 Prisma 客户端、IDE 不刷新类型缓存——本质上都是“产物”与“源头”不同步造成的。
我把排查思路整理成一张表,方便你以后遇到问题时快速定位:
| 问题 | 现象 | 根因 | 快速解决 | 预防手段 |
|---|---|---|---|---|
| 依赖冲突 | pnpm install 报 ERESOLVE | Node 版本与 tRPC 版本不匹配 | 升级 Node 20 或加 overrides | 初始化前检查 node -v |
| Prisma 客户端缺失 | 运行时报 PrismaClient is not defined | 改了 schema 没跑 prisma generate | 手动执行 pnpm prisma generate | 使用 generate:all 组合脚本 |
| 类型在 IDE 飘红 | tsc 通过但 VSCode 报错 | 生成目录未纳入 tsconfig include | 补 include 或改相对导入 | 双保险:include + relative |
说到底,这类问题不是t3code独有,任何代码生成工具都绕不开“产物刷新时机”这个课题。你只要心里有这个意识,排查起来就不会像无头苍蝇一样乱撞。
5. 把 t3code 纳入团队规范和 CI,避免生成器失控
5.1 提交还是不提交生成代码,我的选择
这是团队引入生成器时最容易吵起来的问题:生成出来的代码到底要不要提交到 git?
一种观点认为,生成代码属于产物,应该放进.gitignore,CI 构建时现跑现用,保证仓库里只维护“源文件”。另一种观点是必须提交,因为 IDE 的类型推导、评审时的代码阅读、不同人本地环境一致性问题,都得靠仓库里的代码兜底。
我实测之后的选择是:提交。原因有三条。
第一,类型体验。如果生成代码不进仓库,那每个开发第一次拉完代码,必须先跑一次t3code generate才能在 IDE 里看到完整的类型声明。新人入职第一步就因为没有生成产物而看到满屏红,这个体验太差了。
第二,评审可追踪。中后台项目的业务变更,很大比例实际上就是字段变更。如果不提交生成代码,评审者面对的就是一个简单的UPDATE: schema.prisma 增加 status 字段,看不到 API 和页面上的实际变化。提交了,diff 里就能直观看到字段影响范围。
第三,CI 一致性好。CI 环境每次都是干净安装,如果依赖安装速度不稳定,生成命令偶尔还会遇到动态版本拉取问题,不如直接把生成结果一起跑测试,稳定得多。
当然,提交不意味着“生成完无脑提交”。我的建议是给 generate 命令加一个--check模式。
5.2 CI 里做生成一致性检查,防止 dirty diff
t3code提供了一个--check模式,作用很简单:把当前目录里的生成结果重新生成一遍,然后和磁盘上已有的文件对比,如果发现不一致就返回非零退出码。我最早是在本地发现的这个用法,后来把它接到了 CI 里:
pnpm t3code generate --check配合 GitHub Actions 的 workflow,大概长这样:
name: codegen-check on: pull_request: jobs: generate-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm prisma generate - run: pnpm t3code generate --check这段代码的原理就是:如果开发者改完 schema 后忘记跑生成器,CI 会立刻报错,PR 根本进不了合并队列。这相当于给代码生成加了一道强制门禁,把“可能忘记”变成了“不可能漏掉”。
这里面有一个细节值得注意:CI 里一定要先prisma generate再跑t3code generate --check,和本地操作顺序保持一致。否则可能出现本地因为跑了prisma generate所以生成结果正常、但 CI 里因为没跑而报错的情况。
5.3 自定义模板:让生成的代码符合团队口味
默认模板能覆盖大部分场景,但团队总会有自己的一套规范。比如我们团队要求所有 API 都要包一层统一的响应格式,不能直接返回 Prisma 对象;又比如表单页需要默认带一个返回列表的按钮。
这些需求可以通过自定义模板解决。t3code的模板机制借鉴了常见生成器的做法:你在项目根目录建立t3code/templates目录,然后在里面放.hbs后缀的模板文件,里面用变量和条件块占位。变量跟你 schema 里的 model 和字段一一对应。
举个例子,如果我要让生成的 router 所有 list 接口都返回统一格式,模板可以写成:
export const {{modelName}}Router = createTRPCRouter({ list: publicProcedure .input(...) .query(async ({ input }) => { const items = await prisma.{{modelName}}.{{listMethod}}(...) return { success: true, data: items, total: items.length } }), })模板里用{{modelName}}引用模型名,用{{#each fields}}遍历字段。写好后在t3code.config.ts里指定templateDir指向这个目录即可。这里我不展开所有语法细节,你只需要知道有这个能力就行——真到需要那天,查文档会比我这篇更有针对性。
实际上,自定义模板是团队运用生成器的高级形态。初阶是直接用默认模板,中阶是加--check门禁,高阶才是沉淀自己的模板。走到高阶之后,生成器就不再只是省时间的工具,而是团队规范的执行者。
6. 横向对比:t3code、create-t3-app、Plop 和 Amplication 的取舍
6.1 四类工具的定位差异
我实际用过这四个工具之后,最大的感触是:它们的定位完全不一样,硬要分高低只会误导人。我做一个表格给你看清楚:
| 工具 | 核心定位 | 上手成本 | 代码生成深度 | 适合场景 |
|---|---|---|---|---|
| create-t3-app | 项目初始化脚手架 | 低 | 无业务代码生成 | 新项目从零起步 |
| Plop | 通用文件生成器 | 中 | 模板完全自己定,很灵活但费事 | 团队已有完整模板积累 |
| Amplication | 可视化全栈生成平台 | 高 | 能生成整个服务,但体系独立 | 大型复杂系统,愿意接受新体系 |
| t3code | T3 技术栈的 schema 驱动生成器 | 低 | 深,连 API 和页面一起生成 | T3 项目中 CRUD 密集场景 |
这里面最关键的区别在于“生成深度”。create-t3-app 生成完就是一个空壳,业务代码一行没有;Plop 只是帮你快速填充文件,逻辑全靠模板作者写;Amplication 能力强到能生成一整套服务,但生成的代码和自己团队手写部分很难无缝融合;t3code恰好卡在中间:它不做全局架构,只专注 T3 技术栈里的业务代码层,生成结果又是可以继续手写加工的普通 TypeScript 文件。
6.2 什么场景下我不推荐用 t3code
虽然我对这个工具体验整体很好,但它不是万能的。结合我的实践,下面这几类情况我不太推荐硬上:
- 非 T3 技术栈项目。如果你的项目不是 Next.js + tRPC + Prisma,
t3code基本帮不上忙,它的生成器写死了 tRPC router 和 React Query hook 的输出格式。 - 业务逻辑高度自定义。我前面反复强调,生成器解决的是 CRUD 样板问题。如果你的业务里大量存在复杂的审批流、多阶段状态机、精细的权限控制,生成出来的代码只能当起点,改造成本可能比手写还高。
- 团队规模特别小且项目只有三五张表。这种情况下,手写的成本本来就不高,引入一套生成工具反而要学配置、维护模板,性价比不划算。
- 无法接受生成目录进入版本控制。如果你有强烈的洁癖,不允许任何“非人手写”的代码进仓库,那
t3code对你来说就是负担。
我的建议是:在一个存量 T3 项目中先找一个小模块试点,跑两个星期,体验一下 schema 变更到页面更新的完整闭环,再来决定是不是全面铺开。不要第一天就在核心业务上上强度,给自己留点反悔空间。
6.3 选型时最容易忽略的一个问题:退出成本
最后聊一个选型时容易被忽略的点:退出成本。
用生成器最怕的不是生成能力弱,而是用了三个月后你想走却走不掉。怎么理解?如果你的业务代码大量依赖生成结果,而生成结果又不是人手写的,一旦工具停止维护或者你决定放弃,摆在你面前的就是几千行“不是人写的但又是业务依赖的代码”,重构成本极高。
t3code在这个问题上的优点很明显:它生成的代码就是普通 TypeScript 文件,不依赖什么运行时库,也没有特殊的框架层。从代码层面看,它和手写代码没有任何区别,只是来源不同。这意味着你可以随时放弃这个工具,留下的代码继续维护毫无障碍。我判断一个生成器能不能用,现在会先问一句:如果明天它不更新了,我会不会慌。在这个项目上,答案是不会。
7. 一点零散但实用的经验
折腾完这一轮,我自己的感受是:t3code这类工具价值确实大,但它需要正确的使用姿势。最后分享几个不起眼但对体验影响很大的经验。
如果你决定在自己的项目里试用,建议从“默认配置”开始,用一段时间再上自定义模板。一上来就改写模板,你大概率会把精力花在模板语法上,而不是体验“schema 驱动开发”这个核心工作流。等习惯了生成链路,再去逐个定制输出格式,压力会小很多。
生成完后花十分钟完整读一遍生成的代码。很多人会用生成器但从来不读生成结果,导致出现问题时完全无从下手。我花了两小时把 User 和 Post 两个模型的生成结果从头到尾读了一遍,之后遇到类型报错、关联关系不对这些问题,几乎扫一眼代码就知道问题在哪。这份熟悉感是排错能力的地基。
还有一个小技巧:把generate:all纳入日常开发习惯。每改完一轮 schema,先跑pnpm generate:all,再顺手看一眼 git diff。如果发现生成的代码里有出乎意料的变化,那往往是 schema 里的关系配置有问题。把它当作一面镜子,帮你提前发现设计隐患。
最后我想说的是,不要神化代码生成器。它能帮你把重复劳动大幅压缩,但业务逻辑的核心判断、架构取舍、异常处理,这些永远得靠人来完成。工具负责把地基打平,你怎么在这块地基上盖出好楼,才是长期要修炼的功课。