☰
t3code:T3 Stack全栈开发中的CRUD代码生成器实践
2026/10/26 8:47:21 网站建设 项目流程

做 TypeScript 全栈开发的时间越长,我越对重复劳动失去耐心。每次开新项目,都要重新搭 Next.js、重新配 tRPC、重新写 Prisma schema、再手敲一套 CRUD 接口和前端页面,光这些样板代码就能耗掉一整天。今天想聊的 t3code,就是我从这个痛点里折腾出来的一个顺手工具,同时也是一套可以沉淀给团队使用的全栈代码生成方案。它的核心思路是:把 T3 Stack(TypeScript + 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 t3code@latest 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 schema:

model 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 预设了两种 procedure:publicProcedure和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 默认命名不一致。比如你自己之前把列表接口叫getCategoryList,t3code 生成的名字是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 类型,如果本地没有生成 Client,IDE 会先飘红,运行时直接报错。解决方式很简单:

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 generate
UNAUTHORIZEDprotectedProcedure 未登录登录或临时切换 publicProcedure
Zod 报字段类型错误form 提交字符串未转数字使用模板默认的 valueAsNumber 配置
tRPC 泛型不匹配手改 router 后类型未对齐检查 AppRouter 是否正确引用新 router
生成代码格式混乱模板或环境差异导致执行 eslint --fix + prettier --write
模板更新不生效生成缓存未刷新删除 .t3code/.cache 后重新生成
自动 watch 不触发schema 文件变更未保存确认 t3code watch 还在运行,重启

排查问题的时候还有个小技巧:t3code 的verbose模式会输出完整的生成日志。你在命令后面加上--verbose,就能看到每一步生成的输入输出,哪个模型、哪个模板、哪个变量出了问题一目了然。遇到错误时,先不要急着去改生成的代码,而是根据日志找到是模板出了问题,还是 schema 数据结构有问题。很多时候,一个问题反复出现,根源往往不在目标代码里,而在上游的模型定义上。

写在最后的实操体会

我自己用 t3code 跑了差不多半年,最大的感受是:这类工具要解决的从来不是“写不出来代码”的问题,而是“让类型安全和迭代效率兼得”的问题。脚手架大家都会搭,真正难的是搭完之后的持续演进。t3code 的 watch 模式 + schema 驱动生成,让改模型变成了一件非常“爽”的事,改完字段,接口、类型、表单一次性同步到位,这种感觉是手动维护完全体会不到的。

如果你正在做一个长期演进的中后台项目,或者团队里有多个项目需要统一技术栈和代码风格,我建议可以选一个非核心模块先试点。先跑通模型生成、模板定制、团队规范这一整套流程,再逐步扩大适用范围。别想着一次性把所有功能都生成出来,生成器的价值不在于“全”,而在于“稳”——它给团队建立了代码的“唯一事实来源”,所有重复劳动都收敛到模型和模板,这才是它真正的意义所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询