
这两年做全栈项目我最烦的一件事就是从零搭一套 T3 StackNext.js TypeScript Tailwind tRPC Prisma NextAuth虽然每个技术单独看都熟但组合起来要配置的东西实在太多。表单校验、API 路由、数据库表、类型推导每次都是复制上一套项目的模板再改改着改着就出各种低级问题。t3code 这个工具解决的就是这件事——它把 T3 Stack 的初始化、模块生成、模板管理做成了一套命令行工作流不用再靠人工复制粘贴来组建项目骨架。这篇文章就聊聊我实际使用 t3code 的心得包括它的核心逻辑、常用命令、一次完整的模块生成实战以及我踩过的几个坑。适合已经接触过 T3 Stack 但觉得搭建效率太低的开发者也适合想给团队统一项目规范的人参考。1. t3code 解决的核心问题T3 Stack 的手工组装成本太高1.1 先算一笔账不靠工具搭建一套全栈模块要多少重复劳动假如你现在要在一个 Next.js 项目里加一个“文章管理”功能传统做法大概是这样先去 Prisma schema 里写一个 Article 模型然后npx prisma migrate建表。接着到 server 目录下新建一个 tRPC router写 CRUD 方法再把 router 注册到根路由。光这一步涉及文件就有 4、5 个。然后是页面列表页、创建页、编辑页每个页面都要引 useQuery、useMutation还要写 zod 校验 schema最后类型定义还得手动保持和 Prisma 模型一致。整个过程如果运气好半小时能搞定一个基础模块运气不好类型报错、router 没注册、字段名拼错一上午就没了。我统计过一个 10 人左右的团队每人每周大概要产出 3 到 5 个类似模块模板代码大约占到总代码量的 30%。这 30% 的代码如果靠人肉复制既不标准也特别容易因为字段不同改出一堆漏。1.2 t3code 的定位脚手架之上再走一步create-t3-app 解决的是“初始项目怎么搭建”它给你一个干净的工作区但之后每加一个表、每多一个 API还是要自己动手。t3code 想做的是“项目往后的每个模块怎么生成”它把从数据模型到页面组件这条链路做了一个约定式生成你只需要定义好业务模型t3code 会按照内置模板帮你生成 Prisma schema、tRPC router、zod 校验、React Query hooks、基础页面组件。打个比方create-t3-app 像是给你一套毛坯房t3code 则是房子里的一套组合家具你要做的是告诉它“我需要一个衣柜、一张书桌”它按统一标准给你拼好。你仍然可以自己调整细节但不用再每次从零量尺寸。1.3 我为什么会选它类型安全和团队规范的双重需求我选 t3code 不只是为了省时间。T3 Stack 的核心价值是端到端类型安全类型从 Prisma 模型一路推导到前端组件。但如果代码是手工复制改的很容易在某次复制时把类型写死、把 any 带进来类型安全就名存实亡。用生成器可以保证同一套模型只维护一个来源生成出来的代码天然保持类型链条完整。另一个原因是团队规范。组里每个人写 CRUD 的代码风格都不一样有人喜欢把所有逻辑堆在一个文件里有人喜欢拆 service 层review 代码时经常因为这些争论浪费时间。用 t3code 把生成模板统一了基础代码大家完全一致差异只在业务逻辑本身code review 就轻松很多。2. 环境准备与初始化把 t3code 跑起来2.1 安装前置依赖先说环境。t3code 本身是 Node 环境下的命令行工具我目前用的是 0.4.x 版本建议 Node.js 尽量用 18.17 以上我一开始用 16 跑直接报了一个语法不支持的错误。你需要先在本地装好 pnpm当然 npm 也行但 T3 Stack 官方项目现在默认推荐 pnpm。# 全局安装 t3code npm install -g t3code # 验证是否装好 t3code --version这里有个细节t3code 的全局命令在初始化时会检查当前目录有没有package.json如果没有它会默认你是在一个全新目录里引导你走create-t3-app的流程。如果你是想在已有项目里补模块一定要在项目根目录执行命令否则它会认为你要新建一个项目。2.2 初始化一个带 t3code 配置的 T3 Stack 项目全新项目我用的是这条命令t3code init my-app这个命令实际做的事情比想象中多。它不只是创建一个 Next.js 项目还会按 T3 Stack 标准组合安装 Next.js、TypeScript、Tailwind CSS、tRPC、Prisma、NextAuth创建t3code.config.ts配置文件这个文件后面所有生成命令都依赖它自动初始化 Git 仓库并生成规范的.gitignore跑一次npm install装完依赖后执行prisma db push做首次数据库同步。要注意的是t3code init默认连的是本地数据库URL 是postgresql://postgres:postgreslocalhost:5432/my-app。如果你的数据库密码或端口不一样先改.env里的DATABASE_URL再跑初始化不然最后一步会失败。我刚开始就是没改卡在 prisma 验证环节。虽然不影响后续使用但看着报错心里不踏实。2.3 配置文件里最关键的三个字段完成后项目根目录会多一个t3code.config.ts我建议你稍微花五分钟读一下因为后面所有生成行为都由它控制。关键的几个字段export default defineT3CodeConfig({ projectType: app, // 或 pages对应 Next.js 两种路由模式 databaseProvider: postgresql, // 目前支持 postgresql 和 sqlite authProvider: nextauth, // 认证方案影响生成代码里 getServerSession 的位置 formatOnGenerate: true, // 生成后自动跑 prettier/eslint templateDirectory: ./templates, // 自定义模板目录 });formatOnGenerate这个字段我强烈建议保持开启。因为生成的代码格式可能和你项目里 prettier 的配置有细微差异如果开启它会在生成结束后自动执行 format 和 lint 修复减少你的手动操作。3. t3code 的核心生成逻辑数据模型单一来源代码多处派生3.1 三条核心命令generate model / generate api / generate page安装配置好之后t3code 的日常用法其实集中在几条命令上。我常用的是# 在 Prisma schema 中生成数据模型 t3code generate model --name Task # 生成对应的 tRPC router zod schema t3code generate api --model Task # 生成前后端一整套模型API页面 t3code generate scaffold --model Task --fields title:string,status:enum[TODO,DOING,DONE]日常开发里我用scaffold比较多但理解它内部机制对排查问题很有帮助。scaffold命令不是凭空变出代码它的执行链路是这样的解析你在--fields里声明的字段转换成内部 AST 表示根据 AST 更新prisma/schema.prisma追加新的 model 定义读取模板目录下的router.tpl.ts把 AST 信息填充进去生成新的 tRPC router 文件在trpc/appRouter.ts里自动注册这个 router省得你手动加生成前端页面文件放到src/pages或src/app对应目录最后统一执行 prettier。这个设计的核心思想是“单一来源”业务模型字段只在命令行里写一次、在 Prisma schema 里落地其余所有代码都从 schema 派生。所以你要修改字段正确的流程是改 Prisma schema然后重新跑t3code generate api --model Task让它同步更新 router 层而不是手动去改前端类型定义。3.2 字段声明语法速查--fields的语法值得记一下它直接影响生成的模型。我整理过一张速查表类型写法生成的 Prisma 类型字符串title:stringString可选字符串nickname:string?String?数字price:intInt浮点score:floatFloat布尔published:booleanBoolean枚举status:enum[TODO,DOING,DONE]enum Status { TODO DOING DONE }日期createdAt:dateTimeDateTime default(now())关联author:relation[User]User relation(fields: [authorId], references: [id])注意关联关系是最容易生成的类型因为它还依赖目标模型本身的 id 字段和关系名。如果你传的 relation 目标不存在t3code 会先提醒你目标模型未定义不会硬生成。我当时试着写author:relation[User]主题模型里没有 User它直接停在 Prisma 更新这一步日志写得很清楚Target model User not found in schema然后就不动了。所以我通常建议先定义关联的核心模型再生成引用它的外键模型。3.3 生成后的文件布局哪些文件被改了第一次用 scaffold 时我以为它只新增文件没想到它会改schema.prisma和appRouter.ts。这个行为如果不了解git diff 时会以为文件是被意外改动的。我整理过生成后常见的改动清单新增prisma/schema.prisma中的 model 定义新增src/server/api/routers/task.ts修改src/server/api/root.ts注册 router新增src/utils/zod-schemas/task.ts新增页面列表页、创建页、编辑页具体路径要看项目类型新增src/server/api/routers/task.types.ts之类的类型汇总文件。如果你是用generate model和generate api分步走的建议在generate model后先手动npx prisma migrate dev一次等数据库表真正建立了再跑generate api。倒过来顺序可能会让 t3code 在扫描 Prisma 现有 schema 时读不到新模型报一个 “Model Task exists in files but not in database” 的警告。这个警告不影响代码生成但 API 跑起来时会因为查不到表而报 500。4. 实战记录用 t3code 生成一个带标签的任务管理模块4.1 定义数据模型任务 标签的多对多关系我拿一个常见场景来演示任务管理。任务需要标题、描述、状态、优先级还要能打多个标签。标签本身是一个独立模型任务和标签是多对多关系。用 t3code 操作时我先建标签模型再建带关联的任务模型。第一步生成标签模型t3code generate model --name Tag --fields name:string,color:string?这一步只是往 schema.prisma 里写入模型定义我看了下生成的 schemamodel Tag { id String id default(cuid()) name String color String? createdAt DateTime default(now()) updatedAt DateTime updatedAt }然后先做一次迁移npx prisma migrate dev --name init-tag第二步生成任务模型。这里涉及枚举和关联。我先用枚举字段t3code generate model --name Task --fields title:string,description:string?,status:enum[BACKLOG,TODO,IN_PROGRESS,DONE],priority:enum[LOW,MEDIUM,HIGH],tags:relation[Tag]结果生成了model Task { id String id default(cuid()) title String description String? status Status priority Priority tags Tag[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } enum Status { BACKLOG TODO IN_PROGRESS DONE } enum Priority { LOW MEDIUM HIGH }注意关联字段tags:relation[Tag]在 Prisma 里生成了一个隐式的多对多关系表也就是 Prisma 的隐式 m2m。它没有在 schema 里直接展开但数据库迁移时会自动生成一张_TaskToTag关系表。如果你希望关系表更可控比如以后要加“添加时间”这种字段就得手动改成显式关联表。这是个容易忽略的点后面我会再展开。4.2 生成 tRPC routert3code generate api 之后发生了什么模型建好后我跑t3code generate api --model Task这里 prompt 询问我是否需要包含 NextAuth 权限保护我选了是。生成出来的 router 大概是这个结构import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; import { TaskSchema, TaskUpdateSchema } from ~/utils/zod-schemas/task; export const taskRouter createTRPCRouter({ list: protectedProcedure.input(z.object({ page: z.number().default(1), pageSize: z.number().default(20) })) .query(async ({ ctx }) { const items await ctx.db.task.findMany({ orderBy: { createdAt: desc } }); return items; }), detail: protectedProcedure.input(z.object({ id: z.string() })) .query(async ({ ctx, input }) { return ctx.db.task.findUnique({ where: { id: input.id }, include: { tags: true } }); }), create: protectedProcedure.input(TaskSchema) .mutation(async ({ ctx, input }) { return ctx.db.task.create({ data: input }); }), update: protectedProcedure.input(TaskUpdateSchema) .mutation(async ({ ctx, input }) { const { id, ...data } input; return ctx.db.task.update({ where: { id }, data }); }), delete: protectedProcedure.input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { return ctx.db.task.delete({ where: { id: input.id } }); }), });这个 router 有几个细节是手工写时容易漏的分页参数虽然写了但实际 list 没有实现 count 和 where 条件t3code 只生成了基础形态如果你需要模糊搜索或过滤得自己修改。另外create里直接塞入 input 是有隐患的如果 input 中包含 id、createdAt 等字段数据库会拒绝或报错。t3code 生成的TaskSchema默认会omit掉这些自动字段。我用的时候确认了这一点它生成的 zod schema 是export const TaskSchema z.object({ title: z.string().min(1), description: z.string().nullable().optional(), status: z.enum([BACKLOG, TODO, IN_PROGRESS, DONE]).default(BACKLOG), priority: z.enum([LOW, MEDIUM, HIGH]).default(MEDIUM), tags: z.array(z.string()).optional(), }); export const TaskUpdateSchema TaskSchema.partial().extend({ id: z.string(), });这里有一个不太顺手的地方tags字段允许传字符串数组但语义上是标签 id 数组。真正写入前需要在createmutation 里把tags转成tags: { connect: input.tags.map(id ({ id })) }。t3code 生成的代码默认没有做这个转换我是在业务里自己补的。这也是为什么我说生成代码是“基础版本”它把框架搭好了但业务关系还得你自己理。4.3 生成页面与组件React Query hooks 与表单框架接着我跑t3code generate page --model Task这一步生成三个页面组件任务列表、任务创建、任务编辑。列表页的查询逻辑它已经帮你接好了const { data, isLoading } api.task.list.useQuery({ page: 1, pageSize: 20 });表单页用了 React Hook Form 加 zod resolver但没有生成 UI 库的组件——它默认生成的是原生 HTML 表单只套了 Tailwind 的简单样式。这是 t3code 比较“克制”的地方它不会替你选组件库像 shadcn/ui 或者 Mantine 这类 UI 方案需要你自己额外封装一层。所以拿到生成页面后我的习惯是把表单控件封装成自己的FormInput和FormSelect组件然后替换掉原生 input。如果你项目里的表单控件有统一的 props 约定t3code 的自定义模板可以帮上忙下一节会讲到。4.4 联调验证跑通数据流的检查清单模块生成完之后我没有马上开页面而是先跑了一遍接口检查npx prisma migrate dev --name add-task让数据库表真正建立npm run dev起服务用 tRPC 的调试面板如果你开了 dev 工具直接调 task.create传入合法 payload看数据库是否写入检查 task.list 是否能读出来最后再打开前端页面验证组件绑定。这个顺序很重要。我曾经直接在生成完页面后就去浏览器操作结果接口报 500才发现数据库迁移还没跑。另外如果你改了 Prisma schema 但没跑 migratet3code 生成的 API 和前端逻辑代码没问题但运行时必然失败。记住生成代码解决的只是代码层问题数据库层始终要自己迁移。t3code 不会帮你执行迁移这也符合“脚手架不做魔法事情”的原则。5. 踩坑记录与完整排查链路5.1 版本不匹配导致的类型报错一根导火索引发的连环崩我升级 t3code 到 0.4.x 后没有同步更新项目里 Next.js 和 tRPC 的版本紧接着就出现了一个极难排查的问题。现象是生成的页面代码里api.task.list.useQuery()返回的data类型变成了unknown前端模板里data.items.map()直接报红线。我一开始以为是 t3code 生成的 hooks 有问题手动去改类型结果越改越乱。后来冷静下来检查了node_modules里的 tRPC 版本发现项目里的trpc/client是 10.x而 t3code 0.4.x 默认是为 11.x 生成的。排查链路第一层前端类型报错 → 定位到 useQuery 返回类型丢失 第二层检查 trpc 客户端配置 → 发现 client creator 语法和生成的 router 不匹配 第三层对比 node_modules 版本 → 发现 trpc/* 版本低于 t3code 预期修复方案就是升级npm install trpc/serverlatest trpc/clientlatest trpc/react-querylatest升级后再看类型立刻恢复。从此我长了一个教训用任何带代码生成能力的工具生成工具的版本预期要和项目依赖版本保持同步。最简单的方式是每次升级 t3code 后顺手看一下它的发布说明里有没有写“minimum dependency versions”。5.2 生成代码与 ESLint 规则冲突接口回调参数未使用另一个让我困惑的问题是生成的页面里删除确认弹窗的回调函数里有个参数我没用到ESLint 直接报typescript-eslint/no-unused-vars。这本身不算问题改一下代码就好但问题是这个报错会卡住我的 CI——我们把 lint 放在了 push 前的检查里。排查下来发现t3code 生成模板里默认给删除回调写了一个onDelete: async (id: string) { ... }的签名但业务逻辑没用到 id。它其实是想让你在回调里调用deleteMutation.mutate({ id })如果我不用这个 id就应该把回调参数改成() 。处理方式当然是一行代码的事但真正值得说的是生成的代码不一定是 lint 友好的应该把 formatOnGenerate 和 lint 修复理解为一个基本策略而不是万能策略。有些规则是语义层面的自动修复工具不会改因为它不知道你是想用还是不想用。我的解决思路是在模板层面调整修改自定义模板里的回调签名让它在未见得使用 id 时不生成这个参数。这正好引出了下一个话题自定义模板。5.3 热更新失效的排查生成器修改了 appRouter 但没有触发缓存清理这个坑最费时间。现象是我跑完t3code generate api --model Task控制台显示成功appRouter.ts文件确实被改动了但浏览器页面上访问.list接口一直 404。重启 dev server 之后才恢复。我当时的日志链路浏览器请求 task.list → 404 检查 appRouter.ts → taskRouter 已注册 检查 task router 文件 → 文件存在且内容正确 手动访问其他 router → 正常 重启 dev server → task.list 恢复正常原因倒不是代码错误而是 Next.js 在 dev 模式下对路由文件的模块依赖解析有时候不会立即感知到新注册的 router。尤其当你同时修改了多个被引用的文件时热更新的依赖图更新会滞后。避免这个问题的办法很朴素生成 API 相关文件后手动重启一次next dev。如果你在写脚本自动化生成流程也可以在生成后向 dev server 发送一个 SIGUSR2 触发重启但最简单可行的方案就是你人去按一下CtrlC再起一次。后来我查了 t3code 的 issue确认这是 Next.js dev server 本身的热更新机制问题不是生成器 bug。5.4 排查方法论不要急着修改生成结果以上三个问题排错下来我有一个很深的体会遇到生成代码的问题先搞清是哪一层出了问题再动手改。我给自己定了一个三层检查法第一层生成器本身是否按预期输出了文件。检查文件内容、文件位置是否匹配配置 第二层项目依赖版本和配置是否被生成器信任。尤其是 tRPC、Prisma、Next.js 版本 第三层运行时环境是否满足代码预期。数据库迁移了没有、dev server 重启了没有、环境变量配了没有。把这三层过完大多数问题都能定位到具体环节。90% 的“t3code 生成的代码有问题”实际都不是生成器的问题而是某一层依赖没有满足。6. 团队协作里的 t3code模板统一与自动化实践6.1 生成代码是提交进 Git还是忽略掉团队用生成器时第一个分歧就是生成的代码要不要提交到 Git。我的意见很明确要提交。理由有三条第一t3code 生成的文件和其他手写代码一样参与 code review如果有人改了生成结果review 能看得到第二CI 会在这些文件上跑 lint 和类型检查能及时暴露问题第三生成器版本不是所有人都保证一致提交生成的产物可以避免因为生成器版本不同导致文件内容漂移。当然代价是每次 schema 变更都会带来一次大 diff。行业里有一种做法是不提交、在 CI 里统一生成但我个人觉得中小型团队没必要承担这个复杂度和 CI 执行环境安装生成器的成本。6.2 模板仓库的维护自定义模板的目录结构和版本管理t3code 的自定义模板由templateDirectory配置指定。我在团队里建了一个templates/目录里面放了一套定制化的 CRUD 模板目录结构大概是这样templates/ router.tpl.ts # tRPC router 模板 schema.tpl.ts # zod schema 模板 page-list.tpl.tsx # 列表页模板 page-create.tpl.tsx # 创建页模板 page-edit.tpl.tsx # 编辑页模板 shared-hooks.tpl.ts # 公共 hooks 模板这套模板我们会定期 review。每次发现某种代码模式在项目里重复出现就会把它抽象进模板。比如我们在所有列表页都加了右上角筛选栏我会在 page-list 模板里内置一个FilterArea占位生成时如果检测到模型里有枚举字段就自动生成对应筛选下拉框。这里有一个关键经验模板文件使用的变量要先在文档里列清楚。因为 t3code 模板的变量是绑定在内部 AST 上的比如{{modelNamePascal}}、{{fields}}不是你想插哪个都能插。我最初写模板时想当然地用了{{modelDescription}}结果生成的时候变量没被替换原样输出到了代码里。看了源码才知道它提供的变量集合里根本没有 description 这个字段。6.3 用脚本把生成流程串进 CI/CD最后说说自动化。我们已经把 t3code 的生成流程做进了一个自定义 script{ scripts: { generate:task: t3code generate scaffold --model Task --fields \...\, generate:all: node scripts/regenerate-all.mjs prisma migrate dev } }regenerate-all.mjs做的事情很简单读取一个集中的models.definition.json文件遍历里面声明的所有模型逐一调用 t3code 的 generate 命令最后执行格式化。这个方式很适合新项目起步时批量铺基础模块。不过我建议模型清单要维护得克制一些一次生成三五个模块就够不要一口气生成十几个不然后面数据库关系设计有问题返工成本特别大。接入 CI 时我目前没有在 push 阶段自动跑生成只在打包发布前做一次diff check在 CI 里跑一遍generate:all然后git diff --exit-code检查是否有未提交的生成差异。如果有差异说明本地没有同步生成CI 直接失败。这个检查虽然简单但能防止“本地改了模板、忘记重新生成代码”的情况发生。7. 我最后补的一句提醒t3code 不是银弹它不会替你做业务决策也不会帮你设计数据库关系。它擅长的是把“重复的、机械的、容易出错的模板代码”稳定地生产出来。我用它最大的收益不是写代码速度翻倍而是大脑里这部分模板拼装的负担被卸载了更多的精力可以放在数据模型设计、接口边界和业务逻辑上。如果你打算在团队里引入这个工具我建议先从一个小模块试水把模板按自己的习惯调过一轮、让一两个成员走通一遍之后再推广这样收到的反馈会更真实也不会因为初期的小问题搞得大家反感。工具最终是服务人的怎么用、用到什么程度还是要看自己团队的节奏。