资讯动态

t3code:T3 Stack全栈开发中的CRUD代码生成器实践

发布时间:2026/10/26 8:47:35 来源:尧图企业网站定制
做 TypeScript 全栈开发的时间越长我越对重复劳动失去耐心。每次开新项目都要重新搭 Next.js、重新配 tRPC、重新写 Prisma schema、再手敲一套 CRUD 接口和前端页面光这些样板代码就能耗掉一整天。今天想聊的 t3code就是我从这个痛点里折腾出来的一个顺手工具同时也是一套可以沉淀给团队使用的全栈代码生成方案。它的核心思路是把 T3 StackTypeScript Tailwind tRPC Prisma Next.js体系里那些高度重复、又极其容易写错类型的代码通过命令行自动生成让业务开发可以聚焦在数据模型和真实业务逻辑上。这篇文章我会从设计思路、初始化流程、CRUD 自动生成、模板定制到常见问题排查完整拆一遍这个项目的落地全过程适合正在用或准备用 T3 技术栈做全栈应用的开发者参考。1. 项目定位与整体设计思路1.1 t3code 到底解决什么问题t3code 本质上是一个面向 T3 Stack 的代码生成器和项目脚手架。它解决的不是“能不能写出来”的问题而是“写出来之后维护成本太高”的问题。全栈应用里最典型的一类重复劳动是 CRUD一个列表页要写路由、写服务端查询、写 tRPC procedure、写 Zod 校验、写 React Query hook最后还要让所有类型从前到后完全一致。手动写一遍倒还好但一旦模型字段变化比如把一个name改成fullName前后端所有引用都得跟着改漏一个就是运行时错误。我自己第一次意识到这个问题是在一个中后台项目里。那张表有十多个字段涉及三个关联关系我把 API、类型、表单、列表全部写完差不多花了一天半。第二天产品说要改两个字段名我又花了半天全局替换。更离谱的是有几个地方类型宽泛IDE 也没有在编译期拦住问题直到测试环境才暴露。之后我就开始思考如果能把“从 schema 到接口到页面”这条链路的大部分代码直接生成并且所有类型都从一个数据源推导出来是不是就能从根本上消灭这类问题。t3code 就是围绕这个目标设计的。它把数据模型定义作为唯一事实来源然后根据模型生成 Prisma Client、tRPC router、Zod 校验、React Query hooks 以及基础列表页和表单页。生成的代码不是你常见的那种一次性模板而是可以直接在生成结果上继续开发的代码项目后续手动改动不会被覆盖。这个设计决定了它的定位不是一个“初始化就丢”的脚手架而是一个陪伴项目长期演进的开发助手。1.2 核心依赖与技术选型解析t3code 的技术选型并不是我拍脑袋定的而是顺着 T3 Stack 社区的主流方案沉淀下来的。整套生成链路的基石是 Prisma它不仅负责数据库建模还能直接从 schema 导出 TypeScript 类型。t3code 读取 Prisma schema解析出模型名、字段名、类型、关系、默认值这些元信息就是后续所有代码生成的输入。tRPC 是另一个不可或缺的依赖。tRPC 的一大特性是端到端类型安全也就是后端 router 的输入输出类型可以直接推导给前端调用方不需要手写接口类型。t3code 生成的 procedure 会自动带上input的 Zod schema前端包一层createTRPCReact就能在编译期拿到完整的入参和返回值类型。这里的难点在于生成器输出的代码里所有的类型推导必须严格符合 tRPC 的泛型约定否则生成完一运行就是一片报错。所以 t3code 在内部对输出代码做了一次类型检查的“预校验”把常见的泛型误用问题在生成阶段就拦截掉。还有一个容易被忽略的组件是 React Hook Form 加 Zod 的配合方案。表单是 CRUD 里最繁琐的部分t3code 会根据模型字段自动生成表单配置文本、数字、日期、枚举、多选各走各的组件。这个方法的好处是表单校验规则和 tRPC 服务端输入校验共用同一套 Zod schema前后端校验永远保持一致不会再出现“前端说必填后端却允许为空”这种尴尬情况。2. 初始化与目录结构拆解2.1 快速初始化一个 t3code 项目第一次上手 t3code可以用它的初始化命令快速生成一个标准的 T3 Stack 项目。初始化命令会引导你选择需要的功能模块包括 Next.js App Router、tRPC、Prisma、NextAuth、Tailwind 等这一步本质上相当于自动执行了 create-t3-app但额外注入了一套 t3code 专属的目录约定。npx t3codelatest init my-app --trpc --prisma --next-auth --tailwind cd my-app npm install npx prisma db push npm run dev初始化完成之后项目会自动启动两个关键进程一个是 Next.js 开发服务器另一个是 t3code 的 watch 模式。watch 模式会持续监听prisma/schema.prisma的变化一旦发现模型有变更就会重新生成对应的 API 和类型定义。这一步在第一次使用时不太容易感知到但它正是“长期使用不痛苦”的核心你不需要每次改完模型都手动跑一遍代码生成命令工具会在后台替你完成。这里我要特别提醒一点这种 watch 模式本质上是一个代码生成器生成结果会直接落在src/server/api/routers和src/api相关目录下。如果项目处于多人协作中建议把生成目录加入 Git 追踪保证大家拿到的代码是一致的。因为不同机器上的 Prisma 版本或插件版本可能有细微差异生成的代码也会有差异统一提交能让差异在 code review 阶段就被发现。2.2 生成后的目录结构与各模块职责初始化完成后你会发现目录结构比 create-t3-app 默认生成的多了一些约定性很强的目录。这不是冗余而是为了让代码生成器和工作流有明确的落点。下面是我在项目里常用的目录说明整理成表格方便对照目录职责说明src/server/db.tsPrisma Client 实例t3code 会确保这个文件存在且被正确引用src/server/api/routerstRPC 路由定义自动生成的 CRUD 路由都放在这里按模型名分文件src/lib/validatorsZod 校验 schema每个模型对应一个 validator 文件表单和服务端共用src/hooks/queriesReact Query hooks封装好的 useQuery、useMutation 方法src/components/forms表单组件基于字段类型自动生成可手动改造src/components/tables列表展示组件负责分页、筛选、状态展示每个模块的生成都不是孤立完成的。比如生成一个TableList组件时它会自动 import 对应的 React Query hook而那个 hook 会自动引用 tRPC router 的类型router 的类型又来源于 Prisma 模型。这一整条链路在生成时就全部打通开发者拿到的是一套能直接跑起来的功能模块而不是一堆零散的文件。3. 核心功能类型安全 CRUD 与代码生成实操3.1 用一个真实模型走通完整 CRUD只看目录结构可能还不够直观这里用一个真实场景演示一下 t3code 的核心功力。假设我们要给一个“文章系统”加一个Category模型包含名称、排序、是否可见三个字段。先写 Prisma schemamodel Category { id Int id default(autoincrement()) name String sortOrder Int default(0) visible Boolean default(true) createdAt DateTime default(now()) updatedAt DateTime updatedAt }保存文件之后t3code 的 watch 模式会自动检测到模型变化然后做三件事生成 tRPC router、生成 Zod 校验器、生成前端 hooks 和基础页面组件。生成完毕之后在routers/category.ts里会得到类似这样的代码import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; import { categorySchema, categoryCreateSchema, categoryUpdateSchema } from ~/lib/validators/category; import { z } from zod; export const categoryRouter createTRPCRouter({ list: protectedProcedure .input(z.object({ page: z.number().default(1), pageSize: z.number().default(20) })) .query(async ({ ctx, input }) { const [items, total] await ctx.db.$transaction([ ctx.db.category.findMany({ orderBy: { sortOrder: asc }, skip: (input.page - 1) * input.pageSize, take: input.pageSize, }), ctx.db.category.count(), ]); return { items, total }; }), create: protectedProcedure .input(categoryCreateSchema) .mutation(async ({ ctx, input }) { return ctx.db.category.create({ data: input }); }), update: protectedProcedure .input(categoryUpdateSchema) .mutation(async ({ ctx, input }) { const { id, ...data } input; return ctx.db.category.update({ where: { id }, data }); }), remove: protectedProcedure .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) { return ctx.db.category.delete({ where: { id: input.id } }); }), });注意这里的protectedProcedure表示这些操作需要登录才能访问t3code 预设了两种 procedurepublicProcedure和protectedProcedure在生成时它会根据模型配置或者.t3coderc文件来决定用哪个。这样的设计可以避免“生成完接口还要一个个去改权限”的问题尤其是后台管理系统几乎所有接口都需要权限管控。3.2 前后端类型如何自动对齐生成完 router 之后前端能不能拿到类型是 t3code 设计上最值得细品的地方。tRPC 的类型安全依赖于一个全局类型导出也就是AppRouter类型。t3code 在生成新的 router 后会自动更新根 router 的引用让AppRouter类型包含新增的 router。前端 hooks 使用方式非常直接import { api } from ~/utils/api; export const useCategoryList (params: { page: number; pageSize: number }) { return api.category.list.useQuery(params); }; export const useCreateCategory () { const utils api.useUtils(); return api.category.create.useMutation({ onSuccess: () utils.category.list.invalidate(), }); };这段代码看起来平平无奇但关键点在于api.category.list的所有方法名、类型、入参都已经被 TypeScript 严格推导。如果 router 里没有create方法前端立刻就会报错如果input的字段类型不对编译器也会直接甩红。单凭这一点就能把大量联调阶段的低级错误扼杀在编码阶段。生成的基础页面同样会沿用这套类型。比如列表页的TableList组件会用useCategoryList拿数据并把数据传给表格渲染表单页的CategoryForm会用useCreateCategory和useUpdateCategory分别处理新增和编辑。前端根本不需要手写任何 API 文档也不需要自己维护接口调用层的类型定义。4. 模板定制与团队协作4.1 自定义模板的三个关键点t3code 的方案虽然好用但如果团队有自己的代码规范、自己的组件库甚至是自己的权限体系默认模板就不够用了。t3code 提供了模板覆盖机制你可以在项目根目录创建.t3code/templates用同名文件覆盖默认模板。比如团队统一用 Ant Design 而不是默认的 Tailwind 组件那就可以替换table.tsx.template和form.tsx.template后续生成的所有页面都会套用新模板。自定义模板有个关键经验第一要理解模板变量。t3code 基于简单模板引擎支持的变量包括{{modelName}}、{{modelNameLower}}、{{fields}}、{{fieldsForForm}}、{{routes}}等。你不需要关注模板引擎底层怎么实现只需要记住模板文件里的变量就是生成器的数据出口。调试模板时建议先跑一次npx t3code generate --dry-run它会把你定义的模板和实际生成的代码打印在控制台对照着调参效率会高很多。第二点是模板覆盖机制是按文件名匹配的不是按目录名匹配。比如你要替换分类列表页就在.t3code/templates下放一个category-list.tsx.template它只会作用于 Category 模型不会影响其他模型。如果你想让所有生成的页面都统一换结构那就覆盖通用的list.tsx.template。第三点是模板里千万不要写死业务逻辑。模板是给所有模型共用的如果某个页面有特殊逻辑应该在生成之后手动改。t3code 的约定是生成目录下的文件属于“owned code”手动修改后如果你不想被后续生成覆盖可以把它移动到src/components/custom或类似目录生成器只认固定目录。这样既享受了自动生成的效率又不破坏手写代码的灵活性。4.2 存量项目接入与团队落地建议t3code 并不强制你从零开始建项目。如果你的项目已经跑了很长一段时间的 T3 Stack想要接入 t3code 也不是不可能。把现有项目的 Prisma schema 保留然后跑一次npx t3code init --existing它会做一次“diff 式接入”先分析现有目录里哪些代码已经存在哪些缺失然后只生成缺失的部分。这样做的意义在于你不需要推翻重来只需要把原来手动维护的 CRUD 部分逐步替换成自动生成。但这里有个现实问题存量项目的路由和 hooks 命名很可能和 t3code 默认命名不一致。比如你自己之前把列表接口叫getCategoryListt3code 生成的名字是list。如果直接生成会出现两份功能相同的接口。我建议的做法是在接入初期把自动生成的代码放在一个独立的路由前缀下先跑通生成链路再分模块逐步迁移。千万别想着第一次接入就完整替换风险很大。团队落地时还有一点值得注意t3code 生成的代码风格必须和团队的 ESLint / Prettier 配置兼容。模板生成的代码实际上就是字符串拼接很容易出现空行、缩进不对、分号缺失这类格式问题。如果团队有强约束的 lint 规则建议在生成之后自动执行一遍eslint --fix和prettier --write把这些格式问题扼杀在提交之前。我在团队里实践下来这套流程的摩擦点基本都集中在格式和命名上提前在 CI 或 pre-commit 钩子里解决后面就能顺很多。5. 常见问题与排查实录5.1 生成代码运行报错 Top5我在这套工具的长期使用中也踩过不少坑。下面是我整理的高频问题几乎每个刚上手的人都会遇到。第一个最常见的是 Prisma 报错PrismaClientInitializationError错误信息是schema not found或者Query engine binary is missing。这通常发生在你新拉取了代码但没有执行prisma generate的时候。t3code 生成的代码大量依赖 Prisma Client 类型如果本地没有生成 ClientIDE 会先飘红运行时直接报错。解决方式很简单npx prisma generate npx prisma db push第二个高频问题集中在 tRPC 输入类型不匹配。比如前端调用create时传了一个sortOrder: 3但生成的 Zod schema 规定的是z.number()。这倒不是因为类型安全失效而是因为表单组件的值类型是字符串提交时没有做转换。我在模板里会专门给数字字段生成valueAsNumber但如果你的模板是自己改过的很容易漏掉这一步。排查方式很简单看 Zod 报错信息它会一字一句告诉你哪个字段、什么类型、收到了什么值。第三个问题是 NextAuth 的protectedProcedure在本地访问时报UNAUTHORIZED。这个不是 bug而是默认的 session 校验策略。开发时需要先登录。如果你希望某些接口在本地调试时绕过权限可以在.t3coderc里临时把模型的 procedure 改为publicProcedure调试完再改回来。第四个问题是 React Hook Form 的defaultValues和生成的数据结构不一致。比如列表接口返回的字段包含createdAt但表单只定义了编辑字段直接套用会导致表单提交时把多余字段传回后端触发 Zod 的stripUnknown或报错。t3code 生成的表单组件会自动 pick 需要的字段但如果你手改了模板必须注意这一点。我的建议是在手动改造表单的时候始终从categoryFormSchema而不是categorySchema去拿默认值。第五个问题也是团队协作里最隐蔽的模板更新后老项目不生效。t3code 的模板是有版本概念的如果你改了某个公共模板但项目根目录的生成缓存没有同步运行代码生成时可能仍然走旧模板。强制刷新删除.t3code/.cache和src/generated目录之后重新生成一般都能解决。5.2 常见问题速查表把上面这些经验沉淀一下我整理成了一张速查表可以直接贴在项目 Wiki 或者文档里方便团队排查。问题原因快速处理PrismaClientInitializationError本地 Prisma Client 未生成执行npx prisma generateUNAUTHORIZEDprotectedProcedure 未登录登录或临时切换 publicProcedureZod 报字段类型错误form 提交字符串未转数字使用模板默认的 valueAsNumber 配置tRPC 泛型不匹配手改 router 后类型未对齐检查 AppRouter 是否正确引用新 router生成代码格式混乱模板或环境差异导致执行 eslint --fix prettier --write模板更新不生效生成缓存未刷新删除 .t3code/.cache 后重新生成自动 watch 不触发schema 文件变更未保存确认 t3code watch 还在运行重启排查问题的时候还有个小技巧t3code 的verbose模式会输出完整的生成日志。你在命令后面加上--verbose就能看到每一步生成的输入输出哪个模型、哪个模板、哪个变量出了问题一目了然。遇到错误时先不要急着去改生成的代码而是根据日志找到是模板出了问题还是 schema 数据结构有问题。很多时候一个问题反复出现根源往往不在目标代码里而在上游的模型定义上。写在最后的实操体会我自己用 t3code 跑了差不多半年最大的感受是这类工具要解决的从来不是“写不出来代码”的问题而是“让类型安全和迭代效率兼得”的问题。脚手架大家都会搭真正难的是搭完之后的持续演进。t3code 的 watch 模式 schema 驱动生成让改模型变成了一件非常“爽”的事改完字段接口、类型、表单一次性同步到位这种感觉是手动维护完全体会不到的。如果你正在做一个长期演进的中后台项目或者团队里有多个项目需要统一技术栈和代码风格我建议可以选一个非核心模块先试点。先跑通模型生成、模板定制、团队规范这一整套流程再逐步扩大适用范围。别想着一次性把所有功能都生成出来生成器的价值不在于“全”而在于“稳”——它给团队建立了代码的“唯一事实来源”所有重复劳动都收敛到模型和模板这才是它真正的意义所在。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑