如果你最近在新项目的初始化阶段反复折腾过,大概率能理解我为什么愿意花时间把 t3code 从一堆脚本里捞出来单独维护。t3code 是我整理的一套代码工程化生成器,名字里的 T3 沿用了 TypeScript、Tailwind、tRPC 这条技术主线的说法,code 则代表它更关注最终落地的代码形态。简单说,它用一条 CLI 命令生成一个已经配好目录规范、类型安全链路、样式体系和基础鉴权的 Next.js 全栈项目,后续还能按业务模块增量生成 CRUD 代码,省掉复制粘贴时最容易出现的低级错误。适合正在做中台、后台工具、内部 SaaS 的团队,也适合个人开发者想快速验证思路。
先别急着把它当成某个现成脚手架的替身。t3code 解决的重点不在“初始化出来的项目能不能跑”,而在于让项目里的代码长成一套可预期的样子:业务模块独立、类型定义单一来源、前端调用接口不靠猜。整个工具在我看来是一个低成本的基础设施,投入一次,后续每个新模块都省心。下面把设计思路、核心实现、实操过程和踩坑记录完整梳理一遍,想接入自己团队的人可以直接参考这套做法。
1. 为什么从零搭一个工程反而更慢:t3code 的设计思路
1.1 被低估的初始化成本
很多团队开新项目,第一反应是create-next-app。跑完命令确实能快速得到一个可运行的基础项目,但真正开始写业务时,你还是会卡在几类问题上:目录结构没有统一,有人按页面堆文件,有人按组件分目录;接口调用路径靠手写字符串,前端改一个字段,后端完全不报错;ESLint、Prettier、Husky 这些基础配置散落在各台电脑上,无法保证每个成员提交的代码格式一致。这些成本不是不可逾越,但每个项目都重来一遍,就非常浪费。
我印象最深的是一个后台项目,团队前三天几乎没写业务代码,全在补环境、调 lint、商量接口数据结构。等到第四天真正开始写接口时,发现 Prisma 模型和前端类型已经是两套东西了。这种场景在中小团队里太常见了,不是某个人能力不行,而是“项目启动”这件事本身缺少一条固定路径。t3code 的第一版需求就是这么来的:把每个项目都要做一遍的动作收敛成一个模板,同时保留根据场景裁剪的空间。
所以它不是一个框架,而是一个代码生成器。它的内核是模板和配置,不是运行时依赖。业务代码里不会出现 t3code 的 SDK,项目跑起来之后,它只是一个普普通通的 Next.js + TypeScript + Prisma + tRPC 项目。好处是后续可以随时放弃这套工具,已经生成的代码不会绑架你的项目。
1.2 从“复制模板”到“参数化生成”
早年我也用过复制整个模板仓库的方式,先把项目 A 拷过来,再全局替换项目名。问题出在替换不干净、隐藏配置没同步、多个项目各自演进后差异越来越大。更麻烦的是,模板仓库一旦更新,已经复制出去的项目很难自动吸收改动,团队里逐渐出现“同一个项目,三种写法”的乱象。
t3code 的做法是用 JSON 配置文件描述项目特征,再通过模板引擎一次性渲染出完整项目。同样的模板,传入不同的项目名、包管理器、是否接入 Prisma、是否需要鉴权,得到不同的结果,而不是生成完再手动删文件。这样做的好处是项目之间只共享“生成逻辑”,不共享“物理文件”,每个项目仍然可以独立演进。
这里的关键是配置项不能太多,否则生成器本身就成了一个需要维护的项目。我实际只保留了十几个顶层配置,比如项目名、包名、模块列表、数据库类型、认证方案。其余的细节都收敛到固定约定里,比如路由必须放在src/app,业务组件放在对应模块目录下。约定优先于配置,这是 t3code 能保持轻量的原因,也是它和那种“通过几十个选项组装项目”的工具最大的区别。
2. 核心细节解析:目录结构、类型安全与模块拆分
要理解 t3code 的价值,不能只看命令怎么跑,更值得看的是默认生成出来的代码长什么样。很多脚手架只解决“能运行”,不解决“好维护”,而后面这件事才是团队真正需要的基础设施。
2.1 目录结构:按 feature 而不是按技术分层
大部分 Next.js 项目教程里都会让你把components和hooks分别建一个顶层目录。这种按技术类型分层的结构在小项目里看着干净,一旦业务模块变多,就会变成“找文件靠翻,改完忘记还有关联文件”。t3code 默认生成的是按 feature 组织的目录:每个业务模块拥有独立的api、components、schemas、types,模块之间尽量不互相依赖。
src/ app/ (auth)/... (dashboard)/... modules/ auth/ api/ components/ schemas/ types.ts project/ api/ components/ schemas/ types.ts lib/ db.ts auth.ts query-client.ts shared/ components/ utils/这个结构对绝大多数后台业务足够清晰。app只做路由入口和布局,真正的业务实体都放在modules下。shared放跨模块复用的基础件,例如按钮、输入框、日期工具,但不放任何和业务字段强相关的组件。团队协作时,新人找到一个模块只需要看一个目录,不用在大杂烩的components里靠记忆力翻找。
有人会担心按模块拆分会导致重复代码变多,比如两个模块都用到了同一个表格组件。我的处理方式是基础组件下沉到shared,业务组件留在模块内。比如“用户表格”和“项目表格”虽然长得像,但列和操作逻辑完全不同,放一起反而要写大量条件分支。按模块隔离之后,改一个模块的操作不会影响另一个模块,回归成本明显变小。
2.2 类型安全三层联动:Schema、ORM、RPC
t3code 的另一个核心设计是让一条数据从数据库到表单,全程类型可追踪。默认链路是 Zod 定义校验规则,Prisma 定义数据库模型,tRPC 暴露 API,前端 React Query 根据 tRPC router 自动推导出类型。听起来复杂,实际理解就是:在一处修改字段,其他环节马上能感知。
// src/modules/member/schemas/create-member.schema.ts import { z } from "zod"; export const createMemberSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(50), role: z.enum(["ADMIN", "MEMBER"]).default("MEMBER"), }); export type CreateMemberInput = z.infer<typeof createMemberSchema>;上面这段 schema 既是 API 入参校验,又是前端表单的字段规则来源。t3code 在生成模块时会自动把 schema 接入 tRPC router,所以前端调用createMember接口时,如果没有传email,类型检查阶段就会报错,根本轮不到运行时才返回 400。这个体验比传统 REST 接口靠文档和 Postman 同步字段要稳得多。
| 对比项 | 传统 REST / RPC | t3code 默认链路 |
|---|---|---|
| 接口定义 | 手写路由字符串和入参类型 | router + Schema 自动生成 |
| 数据库字段 | 手动同步到模型和接口 | Prisma 模型 + Zod schema 双写但可检查 |
| 前端调用 | 手写类型断言或复制类型 | tRPC 自动推导 |
| 校验时机 | 运行时才发现缺失 | 类型期 + 运行时双层兜底 |
这里要强调一点,双写并不等于没有成本。t3code 的做法不是消灭双写,而是把两处的关联做得足够显眼。生成模块时,Prisma model 和 Zod schema 放在同一个模块目录下,改动时一眼就能看到对应关系。后续如果调数据库字段,顺手同步 schema 就行了,而不是像传统项目那样在三个不同层级里手工找。
3. 实操过程:从零初始化一个 t3code 项目并接入业务模块
3.1 环境准备与 CLI 初始化
实际操作层面不需要太多前置条件。我用的是 Node 18 和 pnpm,因为模板默认生成 pnpm workspace,理论上 npm 和 yarn 也能跑,但没有 pnpm 的按需安装体验顺畅。输入下面命令:
npx t3code init my-app命令执行后会依次问几个问题:项目名称、包管理器、数据库类型、是否启用 NextAuth 鉴权、初始模块列表。项目名称我推荐用中划线小写,例如my-admin,避免生成 import 路径时出现大小写不一致的问题。选择完成后,CLI 会创建一个my-app目录并安装依赖,过程大概一到两分钟,取决于网络。
初始化后目录里会有一个t3code.config.json文件,它是后续增量生成模块的配置中心。比如我已经启动了一个项目,现在要加一个文章管理模块,不需要重新init,直接执行t3code module add即可。这个设计很贴近实际项目节奏——没有谁会在项目初始化时就把所有模块规划好,大部分时候是边做边加。
第一次使用会有点不习惯的是,t3code 默认不会生成src/pages目录,而是按 Next.js App Router 的方式生成src/app。App Router 的嵌套布局和 server component 能力很适合后台项目,但如果你是从 Pages Router 时代过来的,需要先适应布局文件layout.tsx和页面文件page.tsx的分工。
3.2 配置数据库并生成第一个业务模块
假设我现在要做一个项目成员管理功能。数据库用的 Postgres,初始化时已经选择了 Prisma。第一步是把数据库连接串写入.env:
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"然后执行下面的命令同步表结构:
pnpm prisma db push接着用 t3code 生成成员模块。我习惯先定义字段,再让生成器去补全整套代码:
npx t3code module add member \ --fields "email:string@email" \ --fields "name:string@min:1,max:50" \ --fields "role:enum:ADMIN,MEMBER"这条命令会在src/modules/member下生成 Prisma 模型、Zod schema、tRPC router 和前端 React Query hooks,同时把路由自动挂载到app的一级菜单中。生成完看一眼代码,会发现比自己手动写完再去补类型要可靠很多。
// src/modules/member/api/member-router.ts import { createTRPCRouter, protectedProcedure } from "~/lib/trpc"; import { createMemberSchema, listMemberSchema } from "../schemas/member.schema"; export const memberRouter = createTRPCRouter({ create: protectedProcedure .input(createMemberSchema) .mutation(async ({ ctx, input }) => { return ctx.db.member.create({ data: input }); }), list: protectedProcedure .input(listMemberSchema) .query(async ({ ctx, input }) => { return ctx.db.member.findMany(); }), });这里有一个细节:生成器在 schema 里自动把 email 字段设为唯一的 Prisma 模型字段,并在 Zod 里做了 email 格式校验。如果你后续想调整字段类型,不要直接改 router,而是改 schema 文件后重新执行生成命令,或者手动同步 Prisma 模型。t3code 并不会每次都在现有文件上强覆盖,这样可以避免误删业务代码,但也意味着改完模板规则后需要自己重建已有模块,而不是自动迁移。
4. 常见问题与排查技巧实录
4.1 pnpm 依赖冲突与 peerDependencies 处理
用 t3code 生成的模板默认把 Next.js、React、tRPC 的版本锁定在一个组合里,按原样跑一般没问题。但很多人在使用一段时间后手动升级某个依赖,就会开始出现版本冲突。我在实践中遇到最多的是 React 18 与某个 UI 库的 peerDependencies 不兼容,或者 tRPC 的 server 端和 client 端版本不一致。
tRPC 这个生态比较特殊,@trpc/server和@trpc/client必须保持版本一致,否则类型推导会直接崩掉。像 NextAuth 和 Prisma 这层还相对宽容,而 tRPC 只要差一个小版本,inferRouterOutputs那一类工具类型就会失效。这时候最好的做法不是单独升级某一个包,而是整组升级 tRPC 相关依赖。如果项目里还有其他库和服务端版本锁死,可以在 pnpm 的 overrides 里强制统一:
// package.json { "pnpm": { "overrides": { "@trpc/server": "^10.45.0", "@trpc/client": "^10.45.0" } } }我通常会先执行pnpm why @trpc/server看实际安装版本,再决定要不要 override,而不是盲加。依赖冲突这个东西,越早处理越省钱,拖到业务代码多起来再升级,成本会翻倍。
4.2 tRPC 类型不同步与路由 404 的排查顺序
新人在接入模块时最容易遇到两个现象:前端调用某个 procedure 时类型报错,以及运行时接口 404。前者八成是类型没有更新。tRPC 的类型是根据 router 结构推导的,如果你改了 router 里的 method,但 Next.js dev server 没有完成重新编译,或者多包项目里 client 端引用的是旧入口,就会一直看到类型报错。可以先停掉项目,重新执行pnpm dev,如果还不行就检查t3code.config.json是否把新模块正确注册到 router 聚合文件里。
路由 404 的情况,我一般按这个顺序排查:
- 先看
src/app目录下路由文件位置是否和模块名一致。 - 再看 tRPC root router 是否挂载了新模块。
- 用浏览器 network 面板看实际请求路径。
- 最后确认 React Query 的 key 和 tRPC 路径是否被手写过。
React Query 的 key 和 tRPC 的路径是自动生成的,手动改过 key 才会对不上。另外还有一个容易被忽略的小坑:App Router 的 server component 里直接调用 tRPC procedure 时,如果传入了非序列化对象,会触发运行时错误。虽然和 404 无关,但排查思路是一样的——先怀疑约定被破坏,再怀疑框架出问题。
| 现象 | 优先排查点 | 常用处理 |
|---|---|---|
| 类型报错 | tRPC server/client 版本不一致 | 整组升级或 overrides |
| 类型不更新 | dev server 未重新编译 | 重启 dev 进程 |
| 接口 404 | root router 未挂载 | 检查模块生成配置 |
| 请求路径错误 | 手写了 query key | 交给自动生成的 hook |
4.3 让模板长期好用的三个维护习惯
t3code 这类工具最大的敌人是模板和业务代码漂移。我见过不少团队,刚开始模板很统一,三个月后每个模块写法都不一样。三个习惯能减少这个问题:
- 第一,约定式生成先于手写。能用 t3code 生成的模块就不要手抄,至少保证初始代码结构一致。人工介入越多,风格分裂越快。
- 第二,模板变更走版本化。不要直接在生成项目里改一个文件然后到处复制,把变更提交回 t3code 的模板仓库,再发布一个新版本。否则模板更新和项目实际代码会越走越远。
- 第三,定期抽查。我在团队里每隔一个迭代会挑一个模块做代码评审,重点看 schema 是否在 Prisma 和 Zod 两边同步,router 里是否混入了别的模块的接口。这是最简单也最容易被忽略的维护动作。
以上三个习惯加上工具本身的约定,基本能让项目在半年内保持“不看目录也知道文件在哪”的状态。如果你正好也在为新项目工程化发愁,不妨基于这套思路先把模板搭起来,哪怕不用 t3code 这个名字,也可以从模块化目录和类型联动这两点入手。模板带来的一致性收益,远比多敲几条初始化命令更大。