从零搭建 t3code:一个基于 T3 技术栈的代码生成器实战复盘
去年我做了一个代号叫 t3code 的私有项目,初衷很简单——团队里每次开新模块,都要重复手写 CRUD、配路由、建表、定义请求类型,前后端类型不统一的问题每隔几周就爆一次。我决定把这些重复劳动压榨到一个内部脚手架工具里,技术栈干脆选了当时社区讨论度最高的 T3 Stack:Next.js、tRPC、Tailwind CSS、Prisma。折腾了大概一个月,项目从只能生成一个空目录,变成了输入一份轻量 YAML 配置就能产出带数据库模型、接口路由和前端调用代码的完整工程骨架。
这篇文章不是 T3 的官方文档翻译,而是我从 t3code 这个真实项目里沉淀下来的完整实操记录。我会把初始化阶段的坑、Prisma 建模的取舍、tRPC 路由的拆分方式、Tailwind 工程化细节、部署上线的注意事项全部拆开讲。适合对全栈 TypeScript 感兴趣、想搭内部工具或内容生成类应用、以及准备入坑 T3 技术栈的朋友参考。
1. 我为什么把 t3code 押在 T3 技术栈上
1.1 T3 到底是一套什么组合
T3 Stack 的核心成员是 Next.js、tRPC、Tailwind CSS、Prisma,主要负责人 Theo 对这套组合的定位很明确:类型安全优先,用一套语言把全栈串起来。翻译成大白话就是——前端、后端、数据库访问层全用 TypeScript 写,类型从数据库一路穿透到页面组件,中间不带断裂。
我之前经历过传统 REST + Swagger + 手写前端 API 层的痛苦。后端改了一个字段名,前端接口文档可能还没更新;就算文档更新了,手写的 TypeScript 类型也经常漏改。这种问题不会让服务挂掉,但会让联调变得极其消耗耐心。T3 这套组合最吸引我的地方,就是它试图从架构层面根除这类问题。
具体到每个成员:
- Next.js 负责页面和应用框架,选用 App Router 模式后,服务端组件和客户端组件的边界清晰,配合 tRPC 可以省掉大量自定义 API 层的胶水代码。
- tRPC 是一个纯 TypeScript 的 RPC 框架,不用写 OpenAPI 定义,不用生成客户端 SDK,路由逻辑天然被类型约束。
- Prisma 负责数据建模和迁移,schema 文件即数据库结构的事实来源,搭配生成的客户端,查询结果天然带完整类型。
- Tailwind CSS 负责样式,原子类的写法在快速迭代内部工具时效率极高,配合深色模式切换也能做得非常灵活。
这套组合在 t3code 项目里有非常明确的落点:我需要一个工具去解析用户的 YAML 配置,把配置转成 Prisma 模型、tRPC 路由代码和前端调用代码。这类代码生成工具本质上就是读取结构化输入、拼接字符串、输出文件,而 T3 的类型安全能力能让我在生成代码之前,先把配置模型的类型钉死。
1.2 t3code 要解决的真实痛点
我先把 t3code 的定位说清楚,它不是一个公开展示的产品,而是我给自己和团队用的内部提效工具。使用流程是这样的:
- 用户写一份简单的 YAML 配置,描述一个业务模块需要哪些字段、哪些接口。
- t3code 解析这份配置,在内存中构建一个“模块模型”。
- t3code 根据模块模型,生成一套完整的工程代码,包括 Prisma model、tRPC router、前端 hooks 调用、页面列表与表单组件。
- 用户把生成的代码复制进现有项目,改改字段名和业务逻辑就能跑起来。
我在调研时对比过其他脚手架方案。有些团队用 Plop.js 生成模板文件,有些用 Nx 的 generators,还有的直接靠 IDE 代码片段。它们都有各自的场景优势,但我当时的核心诉求很硬:生成出来的代码必须带有完整类型推导,而且服务端逻辑和前端调用层必须共享同一套类型来源。
如果只用 Plop 做纯字符串拼接,生成出来的 tRPC 调用代码虽然能看,但没法在生成过程里“即时校验”用户配置是否合理。而 T3 技术栈给了我一个天然的运行环境——我可以在 t3code 自身内部定义一套严格的 Zod schema 来验证用户输入,再用 Prisma 的 schema 生成能力去映射模型字段。这样用户在最终产物里拿到的类型,和我在生成器内部使用的类型是同一套推理体系,谁也没法在中间悄悄引入不兼容的字段。
1.3 和传统方案对比,T3 的优势在哪
| 对比维度 | 传统 REST + 手写类型 | T3 Stack |
|---|---|---|
| 接口类型来源 | OpenAPI / 手写 d.ts,双份维护 | tRPC 路由推断,单份来源 |
| 数据库变更影响面 | 后端改,前端不知情 | Prisma migrate 后类型全链路感知 |
| 页面初始化成本 | 手动建目录、配路由、写请求层 | 路由+调用层自动生成 |
| 团队新人上手门槛 | 需要理解多套工具链 | 只需理解 TypeScript 和少量约定 |
这张表里最关键的是第二行。在 t3code 的实践里,我生成代码的目标项目本身也是 T3 技术栈,所以 Prisma 字段一旦变更,tRPC router 的输入输出类型会立即变化,前端调用处的编译错误会精确指向需要修改的位置,而不是等到运行时才发现字段对不上。
2. 初始化阶段:create-t3-app 之后真正需要手动调优的部分
2.1 初始化命令与选项取舍
t3code 自身的初始化,我用的是 create-t3-app 官方脚手架。执行命令如下:
pnpm create t3-app@latest t3code交互式界面会让你选择启用哪些模块。我当时勾选了 Next.js、Tailwind CSS、Prisma、tRPC,没有勾选 NextAuth。原因很简单:t3code 是内部工具,不需要复杂的第三方登录体系,早期少一个鉴权模块,排查问题的复杂度会低很多。等后续需要权限控制时,再补 NextAuth 也不迟。
这里提醒一句,Node 版本最好保证在 18.17 以上。create-t3-app 当前版本对 Node 的版本要求比较严格,我有一次在 Node 16 的机器上执行初始化命令,直接报了个Unsupported Node.js version的错,换了 nvm 切到 Node 20 之后才顺利跑通。
初始化完成后,目录结构大致是:
├── prisma │ ├── schema.prisma │ └── migrations ├── src │ ├── pages │ │ ├── api │ │ │ └── trpc │ │ │ └── [trpc].ts │ │ └── index.tsx │ ├── server │ │ ├── api │ │ │ └── routers │ │ │ ├── root.ts │ │ │ └── post.ts │ │ ├── auth.ts │ │ ├── db.ts │ │ └── trpc.ts │ ├── styles │ │ └── globals.css │ └── utils │ └── api.ts默认结构里包含一个 post 示例路由,实际开发会把它删掉或改名。我在实践里倾向于保留 root.ts,但把示例的 post.ts 替换成自己业务的路由文件。
2.2 依赖装好后先改这些配置
脚手架给的是开箱即用的状态,真正写业务前我通常会改几处配置,避免后面返工。
第一处是src/server/trpc.ts里的 context 定义。默认情况下,create-t3-app 的 context 里只放着 db 实例。t3code 需要一个接收命令行参数或上传配置的入口,所以我扩展了 context,让服务端可以拿到用户的配置文件名和当前工作目录。实现方式就是在 createContext 函数里读取一些元信息,而不是只依赖 Next.js 的请求对象。
第二处是tailwind.config.ts。create-t3-app 默认的 content 配置已经覆盖了./src/**/*.{ts,tsx},但如果 t3code 要生成代码、并且这些生成后的代码也需要被本地解析样式类名,就得把生成目录加进 content。我当时的习惯是把生成的代码统一输出到./generated/**/*.{ts,tsx},所以在 tailwind.config 里手动补充了这一行。
第三处是 Prisma 数据库连接。本地开发我一直用 SQLite,整个初始化零成本,跑迁移秒过。但生产环境我计划用 PostgreSQL,所以 schema.prisma 里的 provider 最终会切到postgresql。为了不让本地开发和线上行为差太远,我在.env里准备了DATABASE_URL和POSTGRES_URL两套连接串,通过prisma/schema.prisma里的环境变量切换。这个方法在部署章节还会细讲。
2.3 我在初始化阶段踩过的三个环境级大坑
第一个坑是 pnpm 版本不一致导致的幽灵依赖问题。我在公司电脑上用的 pnpm 是 8.x,家里电脑是 9.x,两个版本对依赖提升的默认策略不一样,导致@prisma/client在 lint 阶段偶尔报“找不到模块”。最终解决方案是锁定packageManager字段,并且统一用 corepack 激活。
第二个坑是 tRPC 版本升级带来的 API 变化。早期版本里createReactQueryHooks的用法在新的统一入口@trpc/react-query中已经变了。我是照着网上老旧文章写的代码,结果运行时报了一堆类型错误。查文档确认后,把所有 tRPC 相关 import 改成新入口,问题才消失。这里必须强调:t3code 这类工具本身就是吃类型安全红利的,如果依赖的 tRPC 版本混乱,整个类型体系会大打折扣。
第三个坑是环境变量文件没有加载到 Next.js 的构建流程里。Prisma 的DATABASE_URL写在.env里没被 Next.js 识别,导致prisma generate能过,但真正访问数据库时报错。create-t3-app 默认用了dotenv加载逻辑,但如果你自定义了.env.local或其他文件名,就要确保加载顺序正确。我最后把连接串统一放进了.env,并在.env.example里放了模板,团队其他成员克隆后复制改名即可。
3. 数据层:Prisma 建模、迁移与那些让人当场裂开的数据库坑
3.1 t3code 的数据表设计思路
t3code 作为代码生成器,自身需求并不复杂,但它的数据模型很有代表性,天然包含“配置模板”和“生成结果”两类对象。我的 Prisma schema 里核心表是这几张:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| Template | 保存一份代码生成模板 | id, name, schemaText, fields |
| GenerationRecord | 记录一次生成任务的输入输出 | id, templateId, inputYaml, outputPaths, status |
| GeneratedFile | 生成出的文件列表 | id, recordId, path, content, hash |
| ModuleConfig | 模块级配置,复用常见字段 | id, moduleName, tableName, authType |
从这些表中能明显看到 t3code 的业务本质:它不是保存数据库表结构,而是保存“生成规则”和“生成轨迹”。模板是规则,生成记录是每次执行的结果快照。这样如果生成过程里出了问题,我可以回看某一次的输入和输出,定位是哪一步的字符串拼接逻辑坏了。
实际 schema 的开头部分是这样写的:
generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" // 本地开发 url = env("DATABASE_URL") } model Template { id String @id @default(cuid()) name String @unique schemaText String fields Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt records GenerationRecord[] } model GenerationRecord { id String @id @default(cuid()) templateId String template Template @relation(fields: [templateId], references: [id]) inputYaml String status String @default("pending") outputPaths Json files GeneratedFile[] createdAt DateTime @default(now()) } model GeneratedFile { id String @id @default(cuid()) recordId String record GenerationRecord @relation(fields: [recordId], references: [id]) path String content String hash String updatedAt DateTime @updatedAt }这里有一个设计细节值得说说:schemaText和fields同时存在,schemaText保存的是用户原始配置的字符串形式,fields保存的是经过解析后的结构化字段数组。之所以两份数据都留,是为了可追溯——如果后续我更新了解析器逻辑,旧记录里的fields可能和新逻辑不兼容,但schemaText永远是用户最初给的那份原始输入。
3.2 迁移过程中关于字段命名与关系约束的教训
Prisma 迁移我用的是prisma migrate dev,日常开发会创建新的迁移文件。但这中间有几个坑非常隐蔽。
第一是字段类型与数据库的映射差异。我在业务配置里用了Json类型存 fields,这在 SQLite 和 PostgreSQL 里都能工作,但如果迁回 MySQL,Json的默认值处理方式会不一样。t3code 的目标用户是内部团队,数据库大概率会选 PostgreSQL,这个问题在设计阶段就得跟团队成员说清楚,避免有人拿 MySQL 起项目时满头问号。
第二是关系字段的命名约束。Prisma 的关系字段需要区分“纯外键字段”和“关系导航字段”。我一开始在GenerationRecord里写了templateId,又写了一个template,这就是 Prisma 的标准模式。但如果表多起来,很容易把templateId误当成可以随机赋值的普通字段。Prisma 在迁移时会校验关系完整性,曾经因为我把一条GenerationRecord记录指向了不存在的Template,prisma migrate dev过程中报出外键约束错误,后来清理掉脏数据才解决。
第三是级联删除策略。默认情况下,删除一个Template时 Prisma 并不允许直接删除,因为存在GenerationRecord的外键引用。我最初在 UI 里做了删除模板的按钮,一操作就报错。后来在GenerationRecord的template关系上加了onDelete: Cascade,才算解决了。这个决策在后面救了我一次——清理测试数据时,一条命令就能把模板和它所有生成记录一起删掉,非常干净。
3.3 别把 SQLite 的便利当成生产环境默认值
开发期 SQLite 确实香,拉起来就跑,连 Docker 都不用装。但 t3code 切换生产环境时,我选的是 PostgreSQL,这里面有一些衔接性工作值得注意。
SQLite 对Json类型的存储本质是文本,PostgreSQL 则是原生 JSONB,两者在对 JSON 做嵌套查询时的行为不一致。我在代码生成器的“字段解析”逻辑里大量用到了fields的嵌套读取,开发阶段用 SQLite 没有暴露问题,换成 PostgreSQL 后发现某些深层次的查询需要.运算符才能取到嵌套值,Prisma 的StringFilter对 JSON 字段支持也有限。最终我把这部分逻辑全部挪到应用层做 JSON 解析,数据库只负责保存和读取整个 JSON 文档,不在数据库里做 JSON 内容级过滤。这样虽然牺牲了一点数据库侧的查询能力,但换来的是跨数据库的高度可移植性。
4. tRPC 路由设计与类型安全带来的实际收益
4.1 路由怎么拆才不烂尾
t3code 的 tRPC 路由拆成了三个层次:root.ts是总入口,把所有子路由聚合到一个appRouter里;trpc.ts是创建路由实例和中间件的地方;routers/目录下每个业务域一个文件。
这种拆法不是什么惊天设计,但确实救了我一次。早期我把代码生成逻辑全部塞进一个路由文件里,大概写了 400 行,后面每加一个配置项都要在同一个文件里找半天。后来我按业务域拆成templateRouter、generationRouter、fileRouter三个文件,各自的输入校验和输出类型变得一目了然。
tRPC 的 procedure 是类型推导的核心单元。每个 procedure 的输入输出类型都会从其实现中反向推导,只要实现逻辑的类型是完整的,前端调用端拿到的类型也一定是完整的。基于这一点,我养成了一个习惯:所有输入参数必须先过 Zod schema,哪怕只是z.string().optional()。这样做不是形式主义,而是让 tRPC 的输出类型严格绑定到已验证的输入结构上,杜绝“运行期才暴露字段拼错”的问题。
4.2 generationRouter 的核心代码结构
t3code 最核心的路由是生成接口。它接收用户提交的 YAML 配置,后端解析后调用服务层执行代码生成,最后返回生成结果。核心路由代码大概长这样:
import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { generateProjectFiles } from "../services/generator"; import { parseModuleConfig } from "../services/config-parser"; export const generationRouter = createTRPCRouter({ generate: publicProcedure .input( z.object({ templateId: z.string(), yamlContent: z.string().min(1), projectName: z.string().min(1).max(64), overwrite: z.boolean().default(false), }) ) .mutation(async ({ ctx, input }) => { const config = parseModuleConfig(input.yamlContent); const template = await ctx.db.template.findUnique({ where: { id: input.templateId }, }); if (!template) { throw new Error("Template not found"); } const files = await generateProjectFiles({ template, config, projectName: input.projectName, }); const record = await ctx.db.generationRecord.create({ data: { templateId: template.id, inputYaml: input.yamlContent, status: "success", outputPaths: files.map((f) => f.path), }, }); await ctx.db.generatedFile.createMany({ data: files.map((f) => ({ recordId: record.id, path: f.path, content: f.content, hash: f.hash, })), }); return { recordId: record.id, files: files.map((f) => ({ path: f.path, content: f.content })) }; }), getHistory: protectedProcedure .input(z.object({ limit: z.number().min(1).max(50).default(20) })) .query(async ({ ctx, input }) => { const records = await ctx.db.generationRecord.findMany({ orderBy: { createdAt: "desc" }, take: input.limit, include: { template: true, files: true }, }); return records; }), });这段代码里值得注意的点是两个地方都返回了文件列表,一个是给当前调用方即时渲染,另一个是通过generatedFile表持久化。为什么不只存数据库然后查询?因为即时返回可以减少一次查询,而持久化是为了历史回看。两个目的分开,逻辑就清晰了。
4.3 类型安全如何在联调里帮我省掉两个下午
我在 t3code 上第一次切身体会到 tRPC 类型安全的威力,是在改generateProjectFiles服务函数的返回结构时。
有一版我打算把返回的文件对象从{ path, content, hash }扩展为{ path, content, hash, language }。我没有主动去改任何前端组件或 hooks,只是改了服务层代码和返回类型。保存文件的一瞬间,所有依赖这个返回结构的页面组件都出现了红色的类型报错,报错信息精确指出了具体文件路径。整个修改花了大概 20 分钟,其中大部分时间是在挑选language字段的默认值。
对比之前用 REST 接口的团队协作经历:后端改完接口字段,前端在文档站看到更新,然后手动改类型定义,再排查所有调用处,这个过程通常需要半天。如果在 t3code 里没有 tRPC,我可能要为这个字段改动单独写一个变更说明,然后在几个前端文件里小心地搜索替换。而现在编译器替我把所有该改的位置都列出来了,这种体验只有亲身体会才明白值在哪里。
当然,tRPC 不是万能的。一旦你用了any或手写了错误的 Zod schema,类型推导体系就会局部失效。我在开发中增加了一条团队约定:tRPC 路由文件里禁止出现any,所有 procedure 输入必须有 Zod schema,服务层返回类型必须明确标注。只要坚持这两条,t3code 的代码库里几乎没有“类型猜谜”的时刻。
5. Tailwind 与组件分层的样式工程实践
5.1 为什么内部工具也值得认真对待样式
很多内部工具项目的样式都是“能用就行”——字体不统一、间距随机、深色模式等于没有。我的观点正好相反:内部工具才是更需要稳定样式体系的地方。因为内部工具要被团队天天用,视觉混乱会直接稀释使用者的耐心,进而影响工具被接受的意愿。
t3code 是一个代码生成器,界面上需要展示大量代码片段和配置表单,这些场景对样式的要求非常具体:代码区域要等宽字体、行号对齐、语法高亮清晰;表单控件要标签和输入框间距一致;结果列表要能快速区分成功和失败状态。Tailwind 的原子类在这种界面下效率很高,我可以为不同用途的 UI 块快速组合出稳定的样式模式。
5.2 组件分层:从原子类到业务组件
我把页面组件拆成了三层:基础 UI 组件、组合组件、业务组件。
基础 UI 组件负责最细粒度的样式封装。比如Button、Input、Tag、CodeBlock,它们的实现方式统一使用clsx加tailwind-merge处理类名合并。这个组合在 t3code 里非常重要,因为 Tailwind 原子类很容易出现“后面覆盖前面”的问题,而tailwind-merge能智能合并冲突类,防止p-2和p-4同时存在时哪个生效全凭运气。
组合组件是在基础组件之上做结构复用,比如FieldItem把标签、输入框、错误提示组合成标准表单项。业务组件则是跟具体模块绑定,比如生成表单、历史记录卡片。这套分层规则不复杂,但让 t3code 的 40 多个组件文件保持了高度一致的代码风格,后来加新功能时,只要按分层找对文件位置,就能快速定位样式和逻辑。
CodeBlock组件是 t3code 的招牌 UI,我用 Tailwind + 一个轻量语法高亮库实现。核心代码不复杂,但有一个实际经验值得分享:不要把所有代码内容一次性塞进 DOM,生成的文件可能很大,如果一次渲染上千行代码,浏览器会卡顿。后来我加了虚拟滚动,只渲染可视区域的代码行,体验瞬间提升。
5.3 深色模式、响应式布局与 Tailwind 版本选择
t3code 默认支持深色模式,这是我专门花时间做的功能。方法是在tailwind.config.ts里配置darkMode: "class",然后在布局组件里维护一个theme状态,点击切换时给<html>加或去dark类。实测下来非常稳定,唯一要注意的是切换主题后,动态生成的代码块颜色也要跟着变,所以代码高亮库的主题不能硬编码,要根据当前主题动态选择。
响应式布局方面,因为 t3code 主要是桌面工具,移动端自适应不是最优先的事项,但也不会放任不管。我用了一个简单的断点策略:侧边栏在小于lg屏幕时自动折叠成抽屉,内容区保持单列。这个方案比复杂的多列 grid 要实用得多,因为内部工具使用场景绝大多数是 1280 像素以上的桌面显示器。
关于 Tailwind 版本,我在 t3code 启动时用的是 Tailwind 3 的稳定版。Tailwind 4 的配置方式改成了基于 CSS 的@theme,新增了引擎层面的性能优化,但考虑到项目早期我不想为迁移引入不确定性,所以保持 3.x 版本一直到功能稳定。如果你现在从零开始,可以直接用 Tailwind 4,但要注意tailwind.config.ts存在与否会影响很多老文章里的写法,建议直接看官方升级指南,而不是套用 3 的配置。
6. 部署上线与事后反思:t3code 项目的最终沉淀
6.1 部署时最容易翻车的三个点
t3code 部署到生产环境时,我用的是自托管方案:一台 2C4G 的云服务器,Docker 运行 PostgreSQL 和 Next.js 应用。这种方案比直接扔到 Vercel 要麻烦一些,但对于内部工具,自托管更符合团队的数据隔离需求。整个部署过程里,最让我掉头发的是下面三个问题。
第一个是 Prisma 客户端在构建阶段的生成时机。Next.js 的构建过程需要先执行prisma generate生成客户端,再执行next build。如果你用的 CI 流程没有在合适的位置插入prisma generate,打包产物里就会缺失 Prisma 客户端,运行时直接报错。我最终的 Dockerfile 里,关键步骤是RUN npx prisma generate && npm run build,顺序不能颠倒。
第二个是环境变量在 Docker 构建期间的可见性。DATABASE_URL在构建阶段其实是必需的,因为prisma generate会读取 schema 里的环境变量引用。常见错误是把.env写进.dockerignore导致镜像里没有环境变量,然后构建时 Prisma 报错。解决办法不是把.env塞进镜像,而是用 Docker BuildKit 的--build-arg或 CI 系统的 Secrets 注入。
第三个是数据库迁移与多实例部署的时序冲突。如果你用了多副本部署,migrate 不能每个副本都跑。我最初犯的错误是启动命令里写了prisma migrate deploy再启动服务,多副本时发现两个副本同时尝试跑迁移,一度造成锁表。后来改成用独立的迁移 Job 先执行迁移,再启动应用实例,问题就彻底消失了。
6.2 自托管部署方案的完整清单
部署方案做出来之后,我整理了一份可直接复用的清单:
- 数据库:Docker 运行 PostgreSQL 16,数据卷挂载到宿主机,定期备份。
- 应用:Next.js standalone 输出模式,用 Node.js 容器运行。
- 反向代理:Nginx 转发 443 流量到应用端口,配置 SSL 证书。
- 迁移流程:CI 中先构建镜像,再执行迁移命令,最后滚动更新应用容器。
- 日志:应用容器日志统一收集,错误通过 webhook 通知到团队群。
| 环节 | 具体操作 | 避坑要点 |
|---|---|---|
| 构建镜像 | pnpm install --frozen-lockfile | 锁版本,避免二次安装漂移 |
| Prisma 生成 | Dockerfile 中RUN pnpm exec prisma generate | 必须在next build之前 |
| 运行迁移 | 单独 Job 执行prisma migrate deploy | 避免多副本并发执行迁移 |
| 启动应用 | node server.js | 使用 standalone 输出 |
| 备份 | pg_dump定时任务 | 备份文件保留至少 7 天 |
这套流程跑通之后,我再也没被部署问题纠缠过。有一点我想特别说明:内部工具的稳定性优先级并不比对外产品低,因为它一旦挂了,影响的是一整支团队的开发效率,而不是一个用户看不了页面那么简单。
6.3 复盘:t3code 让我重新理解了代码生成器的边界
一个月的开发和上线实践之后,我对 t3code 这个项目的认识已经和最初的设想不太一样了。一开始我以为,代码生成器最核心的价值是省时间,只要生成的代码能跑就行。但真正用起来之后我发现,核心价值其实是“可预期的一致性”——团队所有人拿到的初始代码结构一模一样,不依赖任何个人偏好,这比单纯省掉几小时手写时间要重要得多。
在技术选型上的感悟也很明确。T3 Stack 的类型安全确实不是宣传口号,它在一个生成器项目里表现得尤其突出:因为生成器的输入输出都是结构化数据,类型系统让整个管道从“输入 YAML”到“输出文件”的每一步都有据可查,基本告别了调试时靠打印日志猜结构的阶段。唯一要留神的是,tRPC 的类型安全建立在“全程 TypeScript、无 any、有 Zod 校验”的纪律上,这需要完整团队达成共识,而不是靠某一个人自觉。
如果你也想做类似的东西,我的建议是从一个很窄的场景切入,快速搭出一个能生成一个模块的完整闭环,再去考虑扩展生成范围。别一开始就想着做成全品类脚手架,那样的项目大概率会在模板组合的复杂度里迷失方向。比如 t3code 的第一个可用版本,只支持生成一种模板:单模块的 CRUD 全栈页面。那几十天里所有功能都是围绕这个窄场景打磨的,直到跑顺之后,我才开始支持多层模块和自定义字段类型。
这个思路对任何内部工具都适用:先解决一个足够痛的单一问题,让团队真的开始用,再谈生态和扩展。到最后你会发现,真正有价值的不只是那套生成的代码,而是你对自身业务结构的理解,已经被硬生生逼着梳理清楚了。