news 2026/10/28 6:43:08

t3code:一个为T3 Stack全栈项目生成CRUD代码的CLI

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
t3code:一个为T3 Stack全栈项目生成CRUD代码的CLI

如果几个月前有人告诉我,我会为了省下重复写样板代码的时间,自己撸一个全栈脚手架工具,我肯定觉得他疯了。但在连续给三个项目搭 T3 Stack 环境之后,我服了。t3code 这个项目就是在这种情况下诞生的——一个面向 TypeScript 全栈开发的代码生成 CLI,它能把 Next.js + tRPC + Prisma + Tailwind 的初始工程、路由模板、数据库模型和页面骨架一次性生成到位。项目本身跑在 Node 18 上,发布不到一个月已经有四百多个 star。这篇文章我会把项目的设计思路、核心实现和踩坑记录完整拆一遍,适合正在用 T3 技术栈或者考虑自建脚手架的朋友参考。

T3 Stack 近年声量不小,但 t3code 解决的问题和 create-t3-app 不太一样。create-t3-app 的目标是“一条命令搭好基础工程”,而 t3code 更像一个“代码生成器”:工程已经跑起来之后,你还要继续新增 blog、product、order 之类的资源,每次都要手动写 Prisma model、tRPC router、React Query 调用和页面组件,t3code 就是把这些重复劳动变成t3code add resource blog这种命令。这篇文章我尽量少讲空话,把项目结构、关键技术点、实操流程和坑全摆出来,照着做你也能自己写一个类似的生成器,或者直接拿它提速日常开发。

1. 项目整体设计与思路拆解

1.1 T3 Stack 的甜头与痛点

聊 t3code 之前,先得搞清楚为什么会有这个需求。T3 Stack 指的是 TypeScript + Next.js + tRPC + Prisma + Tailwind CSS 这套组合,核心卖点只有一个:端到端类型安全。从数据库表到 Prisma Client,再到 tRPC 路由,最后到前端 React Query 的返回值,全程都是同一个 TypeScript 类型体系。第一次体验完这个闭环之后,大多数人都会觉得回不去“前端定义接口、后端再写一遍类型”的老路。

但问题也随之而来。T3 Stack 并不是单一框架,你需要维护的“约定”非常多:Prisma model 里要定义字段,tRPC router 要写 CRUD 入参和返回,zod schema 要另写一份校验规则,前端还要把 useQuery / useMutation 的 hooks 和页面组件拼起来。一个简单的 Blog 资源,按最精简的写法也要动五六个文件,而且每个文件的命名、路径、导出方式都有隐性约定。第一次写是新鲜,写到第三个类似资源的时候,我直接想吐。

这就是 t3code 的出发点:把这些“隐性约定”变成显式的模板和生成规则。不是用黑魔法,也不搞反射,就是老老实实按项目需要的结构,把文件生成出来。所以它的核心不是“一次性把项目建好”,而是“后续每加一个资源,都能用同一条路径、同一套代码风格把它落地”。

1.2 定位:不是模板,而是生成器

我最早也想找个现成的 starter 模板解决问题。但试了一圈发现,模板只能解决“项目初始时一次性的结构”,解决不了“后续持续新增模块”的痛点。比如 create-t3-app 帮你把 Next.js、tRPC、Prisma 初始化好,但当你需要加一个Post模块,它还是帮不了你——你得自己复制上一个模块再改字段,复制的过程中稍有遗漏,类型安全链条就断了。

所以 t3code 的定位很明确:一个运行在已有 T3 工程上的代码生成器。你通过命令行告诉它“我要增加一个 blog 资源,包含标题、正文、是否发布三个字段”,它自动完成以下事情:

  • 在prisma/schema.prisma中追加对应的 model,并格式化;
  • 在src/server/api/routers下生成一个 tRPC router,包含增删改查接口;
  • 在src/server/api/root.ts中自动注册 router;
  • 在src/hooks下生成 React Query 调用封装或者直接生成 tRPC React hooks;
  • 在src/app下生成列表页、创建页、详情页和编辑页的基本骨架;
  • 如果配置了 Tailwind,顺手把表单和表格的样式类也写上。

这套设计相当于把你的团队约定固化成了命令。新人来了不需要翻 old code,敲一条命令,出来的代码风格和规范跟你手写的一致。这也让我意识到,脚手架工具的价值不在于魔法,而在于把“重复”和“约定”分离:重复的部分交给机器,约定的部分写进模板。

1.3 技术选型背后的取舍

做一个 CLI 工具,语言选型上其实没什么悬念:既然目标是给 T3 项目用,自己也得是 TypeScript 写的。我选用 Node.js + Commander.js 来处理命令参数。Commander 是社区里最老牌的参数解析库,虽然 clack、prompts 这些交互库体验更好,但 CLI 工具的稳定性优先,Commander 足够简单且文档齐全。

模板渲染我一开始想用手写字符串拼接,因为生成代码的类型比较复杂,拼接过程中很容易出现空格错位。后来换成了 Handlebars。原因是 Handlebars 语法简单、不依赖运行时 AST 能力,模板文件保持.hbs后缀,编辑器还有高亮插件。配合自定义 helper,可以很轻松地做“变量名格式转换”。文件名、类名、变量名之间的格式差异,是这个项目里最常见的 bug 来源。

打包发布用的是 ESBuild,而不是 tsc。因为 CLI 工具最终要发布到 npm,按理说 tsc 也能编译,但 ESBuild 能把多个模块打包成一个单文件,体积更小、启动更快,还能顺带把类型声明文件单独输出。对于npx t3code这种场景,启动速度直接影响了用户体验,ESBuild 是最省心的选择。

代码生成器的核心不是命令解析,也不是模板引擎,而是“命名转换系统”。同一个blog_post,在不同场景下需要不同格式:数据库 model 用BlogPost,文件名用blog.ts,前端变量用blogPost,路由路径用blog。t3code 里专门写了一个 nameParser 工具,统一处理蛇形、驼峰、帕斯卡、短横线之间的转换,所有生成器共用这一套。

2. 核心细节解析与实操要点

2.1 目录结构:让每个生成器只做一件事

t3code 的源码目录并不复杂,核心思路是“一个生成器只负责一类文件”。这样拆分的好处很直接:以后想支持新的 ORM、新的 UI 框架,只需要加一个生成器,而不用去碰其他模块。

t3code/ ├── src/ │ ├── commands/ │ │ ├── create.ts # 创建新项目 │ │ ├── add.ts # 添加资源/模块 │ │ └── list.ts # 列出可用模板 │ ├── generators/ │ │ ├── prismaGenerator.ts # schema.prisma 追加 │ │ ├── routerGenerator.ts # tRPC router 生成 │ │ ├── hookGenerator.ts # React Query hooks │ │ └── pageGenerator.ts # 页面组件 │ ├── templates/ │ │ ├── router.hbs │ │ ├── schema.hbs │ │ ├── list-page.hbs │ │ └── ... │ ├── utils/ │ │ ├── nameParser.ts # 命名格式转换 │ │ └── fileWriter.ts # 安全写文件 │ └── index.ts # 入口,注册命令

fileWriter是我花了比较多功夫的地方。生成器面对的是用户已有的工程,不能上来就覆盖文件。它默认采用“不覆盖已存在文件”的策略,如果要强制覆盖需要显式传入--force。另外写文件时会自动创建父目录,并且统一处理换行符,确保在 Windows 和 macOS 下生成的代码行为保持一致。这个细节看着小,但团队跨平台协作时异常值很高。

2.2 关键文件拆解:从 schema.prisma 到 tRPC Router

以生成一个 Blog 资源为例,我在src/templates/schema.hbs里写的是这样的片段:

model {{pascalCase resource}} { id String @id @default(cuid()) title String content String published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }

有人可能会问,为什么所有字段都要带 id 和 createdAt?因为后续跟 tRPC router 的接口设计联动,统一约定可以避免“这个表有 createdAt 那个表没有”的混乱。用户命令里传入的字段会通过 Handlebars 的 helper 拼接到模板中,比如--fields "title:string,content:text,published:boolean",会根据类型映射成 Prisma 支持的类型。text在 MySQL 下会变成@db.Text,这个映射逻辑维护在 prismaGenerator 里。

tRPC router 的模板就复杂一些。我按 tRPC v10 的写法生成,因为当时 v11 还在快速迭代,稳定性不够。生成的 router 长这样:

import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { createBlogSchema, updateBlogSchema } from "~/server/api/schemas/blog"; export const blogRouter = createTRPCRouter({ list: publicProcedure .input(z.object({ page: z.number().default(1) })) .query(async ({ ctx }) => { const items = await ctx.prisma.blog.findMany({ orderBy: { createdAt: "desc" }, }); return { items, total: items.length }; }), create: publicProcedure .input(createBlogSchema) .mutation(async ({ ctx, input }) => { return ctx.prisma.blog.create({ data: input }); }), update: publicProcedure .input(updateBlogSchema) .mutation(async ({ ctx, input }) => { const { id, ...data } = input; return ctx.prisma.blog.update({ where: { id }, data }); }), delete: publicProcedure .input(z.object({ id: z.string().cuid() })) .mutation(async ({ ctx, input }) => { return ctx.prisma.blog.delete({ where: { id: input.id } }); }), });

注意这里我把 zod schema 单独拆到schemas/blog.ts,而不是直接写在 router 里。原因很简单:前端新增和编辑表单要复用同一套 zod 校验规则。如果只在 router 里定义,前端页面拿到的是 tRPC 函数类型,做表单错误提示时还要反推,太麻烦。拆出来之后,前后端共享一个 schema,类型安全才算真正闭环。

2.3 生成器的变量注入逻辑与设计模式

生成器之所以能“看懂”你的命令,核心在于变量注入。add.ts接收到参数之后,先通过 nameParser 把blog-post解析成一组命名变量:

const parsed = nameParser.parse("blog-post"); // { kebab: "blog-post", camel: "blogPost", pascal: "BlogPost", snake: "blog_post", plural: "blogs" }

然后把用户字段、命名变量、可选的扩展参数塞进一个 context 对象,再交给模板。这个模式本质上就是经典的 Template Method:定义好渲染流程,具体字段由模板变量决定。t3code 里还有一个 helper 叫toZodType,把string转成z.string(),把number转成z.number(),这样用户不需要手写 zod schema,生成器直接完成。

还有一点容易忽略:生成后的代码必须被格式化。我先调用模板渲染出文件,再在写完文件后手动执行prettier --write,而不是依赖用户自己格式化。因为模板里的空格和缩进,尤其是嵌套 JSX 结构,用字符串拼接很难保证可读性。把 Prettier 集成到生成流程里,可以说是输出质量最关键的保障。

3. 实操过程:30 分钟生成一个全栈 CRUD 模块

3.1 初始化 T3 工程与安装 t3code

如果你还没有 T3 工程,最直接的方式是先跑一遍 create-t3-app:

npm create t3-app@latest myapp

创建时选择 TypeScript、Next.js、tRPC、Prisma、Tailwind。初始化完成之后,全局安装 t3code:

npm install -g t3code

或者不想全局安装也可以用 npx:

npx t3code --help

如果你已经在用 T3 Stack,可以直接在任意已有项目里使用 t3code,不需要从零创建。这也是它跟 create-t3-app 最明显的差异。

3.2 定义资源模型:以 Blog Post 为例

假设新需求是做一个博客模块,需要标题、正文、发布状态三个字段。命令很简单:

t3code add resource blog-post --fields "title:string,content:text,published:boolean"

执行之后,t3code 会先检查当前目录下有没有prisma/schema.prisma和src/server/api/root.ts这些关键文件。如果找不到,会明确报错,而不是盲目生成。检查通过后,生成器开始干活。prismaGenerator在 schema 文件末尾追加 model,routerGenerator创建 router 文件,hookGenerator生成 React Query hooks,pageGenerator创建页面。

整个流程完成后,你会看到类似这样的输出:

✅ schema.prisma: BlogPost model appended ✅ api/routers/blogPost.ts: tRPC router created ✅ api/root.ts: blogPostRouter registered ✅ hooks/useBlogPost.ts: React Query hooks created ✅ app/blog/list/page.tsx: list page created ✅ app/blog/new/page.tsx: create page created ✅ app/blog/[id]/page.tsx: detail page created ✅ app/blog/[id]/edit/page.tsx: edit page created 格式化完成,执行 npx prisma migrate dev --name add_blog_post

我把 npm 包里的日志输出简化了一下,实际会有更多颜色。但核心逻辑就是这些。

3.3 迁移数据库与前端联调

生成代码只是第一步,数据库还得迁移。因为 t3code 不会擅自执行prisma migrate——生成器不应该有改数据库这种副作用。你需要手动跑:

npx prisma migrate dev --name add_blog_post npx prisma generate

跑完迁移后,启动开发服务器:

npm run dev

访问http://localhost:3000/blog,你会看到一个基本的博客列表页。页面里的表格是用 Tailwind 样式写的,每一行有编辑和删除按钮,删除操作走的是 tRPC mutation。如果你对默认的样式不满意,完全可以只保留页面结构,CSS 类名改掉就是。

这里有一个我自己强烈建议的做法:生成器生成的页面不要直接拿来当最终产品,它们更像“一个能跑的参考实现”。你可以在它的基础上改布局、加业务逻辑。但要注意,下次再次运行 t3code 时,如果文件已经存在,默认不会覆盖,所以你在生成文件里加入的自定义逻辑是安全的。只有你显式用--force时才会重新生成。

我实测过,一个包含 Prisma model、tRPC router、React Query hooks 和四个页面的 CRUD 模块,从命令执行到浏览器上出现列表页,最快只需要 30 分钟,这还是包括了看日志和等待依赖编译的时间。手动复制粘贴的旧流程,我至少要写 300 行代码,而且每写一个模块都要改五六个文件,容易漏。

3.4 生成代码示例与前端调用方式

为了让你更直观地看到落地效果,我把 t3code 生成的前端 hook 调用片段贴出来。这里用的是 tRPC 的 React Query 集成,v10 的 API 写法:

import { api } from "~/utils/api"; export function useBlogPosts() { return api.blogPost.list.useQuery({ page: 1 }); } export function useCreateBlogPost() { const utils = api.useUtils(); return api.blogPost.create.useMutation({ onSuccess: async () => { await utils.blogPost.list.invalidate(); }, }); }

服务端组件里如果也要拿数据,t3code 生成的模板会调用serverClient:

import { serverClient } from "~/server/api/client"; export default async function BlogListPage() { const { items } = await serverClient.blogPost.list({ page: 1 }); return <BlogList initialItems={items} />; }

App Router 的边界问题比较微妙。如果你的页面带"use client",可以直接调用 api hooks;如果你想服务端渲染,就要用serverClient。t3code 默认生成的是客户端组件,因为列表页涉及点击事件,客户端拿数据最省事。服务器端渲染留给用户按需改造。

4. 常见问题与排查技巧实录

4.1 Prisma 迁移生成的类型不同步

最容易踩的坑,就是执行完prisma migrate dev之后,TS 类型还是旧的。这个问题的根源不在于 t3code,而是 Prisma Client 的生成机制:migrate dev会触发生成,但如果你修改 schema 后只执行了migrate dev,编辑器里的 TS Server 没有立即刷新,就会看到Type 'BlogPost' is not assignable之类的报错。解决办法是手动执行npx prisma generate,必要时重启 TS Server。

t3code 在命令结束后已经帮你把prisma generate的提示打出来了,但很多使用者不会仔细看。所以我建议把项目的 postinstall 脚本加上:

"postinstall": "prisma generate"

这样每次重新安装依赖之后,Prisma Client 一定会重新生成,能省掉很多莫名其妙的“类型不存在”问题。

4.2 tRPC 泛型报错与类型收敛

使用生成器时,如果团队里的 tRPC 版本不统一,很容易在公共 schema 上出问题。比如 tRPC v10 和 v11 的createTRPCRouter导入路径不同,v11 推荐从@trpc/server直接导入,而 v10 有单独的 adapter。t3code 默认生成的模板是按 v10 写的,如果你升级到 v11,要把server/api/trpc.ts里的createTRPCRouter、publicProcedure的导出路径改一下。

另外我遇到过这样的类型报错:router 方法返回的items类型能被RouterOutputs识别,但加上total: items.length之后,前端调用data.total一切正常,可一旦在相同函数里再 return 一个常量,TS 就开始报“实例化过深”。这其实是 tRPC 结合 Zod 的递归类型导致的,不是代码写错了。经验是不要让单个 router 方法 return 嵌套太深的对象,尽可能拆成小类型,或者显式标注返回类型。

4.3 Next.js App Router 与服务端/客户端边界

t3code 默认生成的是客户端组件页面,因为列表页需要绑定事件。但如果你希望页面在服务端渲染,就会遇到一个问题:生成器生成的页面里使用了useQuery,这在客户端组件里没问题,可一旦外面套一层服务端组件,数据获取时机就乱了。

我的经验是,把“数据获取”和“页面展示”拆成两个组件。服务端组件里用serverClient拿数据,然后传给一个纯函数组件去展示。如果你觉得这种拆法太啰嗦,也可以直接把整个列表页做成服务端组件,把增删改成 form action 或 API route 方式实现。但那是另一套模式,生成器没有覆盖,因为 t3code 面向的是 tRPC 这条默认链路。

4.4 升级依赖时的版本锁问题

t3code 生成的项目里,package.json的依赖版本我是故意固定了主版本号的,比如next: ^14.2.0,不跟随 latest。因为模板代码和依赖版本是强耦合的,Next.js 15 的 App Router 和 React 19 对客户端组件有更多限制,如果用户直接用 latest 安装,生成出来的代码很可能跑不起来。

所以我在 README 里强调了版本策略:t3code 的每个 release 会对应一组经过验证的依赖版本,升级大版本时,会用 codemod 或 release note 说明变化。这个做法也值得所有自建脚手架的人参考——不要让你的模板成为依赖升级的受害者。

4.5 命名冲突与 fileWriter 安全策略

我自己的项目里用过product和products两个资源,结果因为复数转换,生成的文件路径撞了。t3code 的 nameParser 把product的复数解析成products,导致生成app/products目录,而用户原本products是另一个模块,文件直接被覆盖了。后来我改了策略:add命令默认不覆盖,并要求检测到目录冲突时强制报错,除非传入--force。这个改动虽然让命令变啰嗦了一点,但换来的是“不会悄无声息破坏你已有代码”。

fileWriter 还有一个细节:写文件前会检查路径是否存在,存在的话先备份.bak,再尝试覆盖。这样即使--force传错了,也能从备份里恢复。CLI 工具面向的是真实项目,容错能力比功能丰富更重要。

4.6 常见问题速查表

下面这组“症状、原因、解决”的对照,是我在实际使用中反复遇到过的场景,整理成表格方便你直接查。

症状原因解决
执行迁移后仍然看不到新 modelPrisma Client 没有重新生成手动执行npx prisma generate
tRPC router 中 return 嵌套过深Zod + tRPC 类型递归较深拆分返回类型或显式标注
生成文件被意外覆盖资源复数名冲突默认不覆盖,必要时先备份再--force
Next.js App Router 数据不显示服务端组件里误用 useQuery改用serverClient或拆分数据获取
升级 Next.js 后模板报错依赖版本与模板不匹配锁定 t3code 对应版本,不要用 latest

这套速查表也是我 README 里保留的最后一块拼图。真正用 CLI 的人,遇到问题没有时间翻源码,他们需要的是最快定位问题的路径。

写到这里,t3code 这个项目从设计到实现、从实操到踩坑,基本聊透了。我自己在连续写了两周命令行工具之后,最大的感受是:代码生成器最难的不是模板,而是“什么时候不要生成”。每一条需要手动确认的策略,都是对现有项目安全性的敬畏。这里最后给一个小建议:不管你是用 t3code 还是自己写脚手架,一定要把生成器当成团队约定的一部分来维护,而不是一次性工具。代码生成器的价值,不在于一次能生成多少代码,而在于它能让你的团队保持一致的技术决策。如果你也在被重复样板代码折磨,不妨从一个小资源开始,把你最常用的一套 CRUD 流程固化下来,这份收益会远超你的投入。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 6:42:13

impeccable 实战:用 CLI 与浏览器扩展精修 AI 生成的前端代码

1. 从“impeccable”这个词说起&#xff1a;它到底指什么第一次看到“impeccable”这个词&#xff0c;很多人会愣一下——这不是个形容词吗&#xff1f;意思是“无可挑剔的、完美的”。它怎么会跟 AI coding agents、frontend design、CLI、browser extension 这些词挂上钩&…

作者头像 李华
网站建设 2026/10/28 6:41:42

t3code三层编码约定:提升代码可读性与团队协作效率的实践指南

1. 从“t3code”这个名字说起&#xff1a;它到底指什么第一次看到“t3code”这个词&#xff0c;很多人会一头雾水。它不像“React”“Docker”那样有官方文档背书&#xff0c;也不像“LeetCode”那样有明确的刷题定位。我在几个技术社区翻了一圈&#xff0c;发现大家对这个词的…

作者头像 李华
网站建设 2026/10/28 6:40:52

t3code开发者工具链解析:Electron与CLI跨平台分发实践

1. 从"t3code"这个名字说起&#xff1a;一个被低估的开发者工具入口第一次看到"t3code"这个词&#xff0c;很多人会以为是某个新出的代码编辑器或者在线IDE。实际上&#xff0c;从它关联的热搜词——Electron、CLI、Homebrew、winget、codex cli、openspec…

作者头像 李华
网站建设 2026/10/28 6:39:38

QTcpSocket API深度解析:从能跑到跑得稳的实战指南

1. 从一个真实场景说起&#xff1a;为什么我要把QTcpSocket的API翻个底朝天前阵子接手一个内部工具的重构&#xff0c;需求很朴素&#xff1a;做一个局域网内的设备状态采集客户端&#xff0c;定时向若干台设备发查询指令&#xff0c;收回应答后解析展示。听起来是不是特别像大…

作者头像 李华
网站建设 2026/10/28 6:37:37

用MATLAB同时求解GPS电离层TEC与卫星接收机DCB

简介&#xff1a;面向GPS导航定位与电离层研究场景&#xff0c;提供一份基于Matlab的完整实现&#xff0c;解决利用GPS观测和精密星历&#xff0c;通过多项式或球谐函数模型同时估计接收机与卫星DCB&#xff0c;进而计算电离层TEC并提供延迟改正的问题&#xff0c;适合导航定位…

作者头像 李华
网站建设 2026/10/28 6:36:29

雾天交通目标检测:从物理建模到雾鲁棒YOLO设计

1. 雾天交通目标检测为什么不是“加个滤镜就能解决”的事我第一次接到某高校交通实验室的委托时&#xff0c;对方说&#xff1a;“雾天监控看不清&#xff0c;你们能不能给图像加个去雾算法&#xff0c;再跑个YOLOv5就行&#xff1f;”——当时我就知道&#xff0c;这活儿没表面…

作者头像 李华