做全栈开发这些年,我最烦的就是新项目开工时那套重复劳动:初始化脚手架、配 TypeScript、接数据库 ORM、写 CORS、调 ESLint。这些事不是难,而是琐碎,每次都要花掉一两个小时,还容易在版本上踩坑。t3code 的出现算是把我从这部分杂活里解放出来了——它不是普通的脚手架,而是基于 T3 Stack 的一套代码生成工具,默认帮你组装好 Next.js、TypeScript、tRPC、Prisma 和 Tailwind,并且能按需生成 Router、Schema、类型定义和 CRUD 模板。这篇文章就把我这段时间用 t3code 的完整体验写出来,从环境准备到生成代码,再到避坑,一条龙讲透。
这篇文章适合两类人:一是刚接触 T3 Stack、想从零快速搭起一个全栈项目的开发者,二是已经在手动写 tRPC 和 Prisma、但嫌重复劳动太多的老手。不同基础的读者都能从这里拿到可以直接抄的配置和命令,少走弯路。
1. t3code 项目定位与技术选型解读
1.1 为什么会有这样一个工具
先把背景说清楚。T3 Stack 是近年来前端圈子里很火的一个技术组合,核心成员包括 Next.js、TypeScript、tRPC、Tailwind CSS,再加一个 Prisma 作为数据库工具。组合本身很优秀,但问题也出在这:优秀的东西往往门槛高,你得自己把这几样东西串起来。装依赖、写 tsconfig、配置 Tailwind、初始化 Prisma、定义 tRPC 路由,每一步都得仔细,一步错就报一堆类型错误。
t3code 的目标就是把这套"串联工作"标准化。它做的事情可以概括成两件:第一,用交互式命令替你完成项目初始化,生成一套已经配置好的 T3 Stack 项目骨架;第二,提供代码生成能力,让你在后续开发中不用每次手写 tRPC Router 和 Prisma Model 的样板代码,直接执行一条命令就能把带类型安全的代码模板生成出来。
我最初对这类工具是持谨慎态度的。因为很多代码生成器生成的代码太"死",一旦要改就得全改,反而比手写还麻烦。但 t3code 的设计思路不太一样,它生成的不是不可维护的黑盒代码,而是基于模板的可读代码——生成之后你照样可以改,它只是把最重复的那 80% 先替你写了。
1.2 T3 Stack 到底解决了什么问题
要理解 t3code 的价值,得先理解 T3 Stack 本身。很多初学者第一次看到 tRPC 这个词都懵,这很正常。用生活里的话说:传统前后端联调像是两个人分别在两张纸上写字,再互相拍照发给对方看,偶尔还有传丢传错的。tRPC 则是两个人共用一本同步更新的笔记本,前端定义一个函数,后端立刻就有对应的类型提示,不用再手动维护一份 API 文档和一堆接口类型。
Next.js 负责页面和 API 路由,Tailwind 负责样式,Prisma 负责数据库建表和查询,tRPC 负责类型安全的接口调用,TypeScript 负责把这些全都串在类型系统里。这套组合的好处是全程端到端类型安全:从数据库字段到前端 UI,中间的每一层都有类型把关,改动字段名时编辑器会直接标红提醒你哪里漏了。
但代价也很明显:这套东西的配置项非常多。以 Prisma 为例,光 datasource、generator、model 三个模块的配置就能写出一堆坑来,稍不注意就出现PrismaClientInitializationError。而 t3code 正是把这一套最佳实践固化成了模板,让新手也能按规范的方式起步。
1.3 t3code 的定位差异:脚手架之外,它还做代码生成
市面上很多脚手架,比如 create-react-app、create-next-app,核心能力是"初始化那一刻"。项目创建完,脚手架的任务就结束了,后续所有业务代码都得自己写。t3code 不一样,它的核心能力在于"持续生成"。
我实际用下来的感受是,它像一个内置了全栈最佳实践的代码助手。当你需要新增一个业务模块时,执行t3code generate model和t3code generate router,它会自动创建 Prisma Model、tRPC Router、Zod Schema 以及基础的 CRUD 接口,还会顺手帮你把类型文件更新了。这些代码都有明确的注释,不是那种加密过一样的压缩代码,完全可以二次编辑。
这决定了 t3code 适合的场景:中小型全栈项目、内部管理系统、个人项目快速原型。如果项目已经高度定制化,或者团队有自己的一套框架规范,那用它反而不合适。工具嘛,选对了场景才是好的。
2. 环境准备与项目初始化实操
2.1 前置环境清单
在跑 t3code 之前,先检查一下本机环境。我把常用的版本列在这里:
| 依赖 | 建议版本 | 说明 |
|---|---|---|
| Node.js | >= 18.17 | 低于这个版本,Next.js 14 和 tRPC v10 会有兼容问题 |
| pnpm | >= 8.0 | t3code 默认使用 pnpm 作为包管理器,速度快且能处理依赖提升问题 |
| git | 任意较新版本 | 初始化时自动创建 git 仓库 |
| Docker(可选) | 任意 | 本地跑 PostgreSQL 数据库时很好用 |
这里特别强调一下 pnpm。如果你习惯用 npm,t3code 在交互式初始化时会问你"是否使用 pnpm",我建议不要跳过。pnpm 对 Next.js 的依赖管理更干净,尤其是避免幽灵依赖导致的运行时错误。相信我,在 pnpm 上踩过的坑,比你想的要多。
2.2 一条命令创建项目
环境准备好后,执行:
npx create-t3code@latest my-app命令执行后会出现一个交互式界面,问你要不要启用这些模块:
? Select which modules to include: [x] Next.js [x] TypeScript [x] tRPC [x] Prisma [x] Tailwind CSS [ ] NextAuth.js我的习惯是:做内部工具或原型项目时,先把 NextAuth 关掉,等需要登录功能时再手动加。因为 NextAuth 的配置项多,对数据库表结构也有要求,初期引入容易分心。核心五件套(Next.js、TypeScript、tRPC、Prisma、Tailwind)全开,这个组合最稳。
还有一步它会问你数据库类型,默认是 SQLite,也支持 PostgreSQL 和 MySQL。如果是做简单Demo,SQLite 零配置最省事。如果准备做正式项目,选 PostgreSQL 准没错——这个决定最好一开始就做好,后面换数据库时要重新生成 Prisma 迁移,挺麻烦的。
2.3 初始化后的目录结构
初始化完成后,项目结构大致如下:
my-app/ ├── src/ │ ├── pages/ # Next.js 页面路由 │ ├── server/ │ │ ├── api/ # tRPC API 路由入口 │ │ ├── db/ # Prisma client 实例 │ │ └── api/ │ │ ├── routers/ # tRPC 子路由 │ │ ├── root.ts # 根路由聚合 │ │ └── trpc.ts # tRPC 上下文与初始化 │ └── utils/ ├── prisma/ │ └── schema.prisma # 数据库模型定义 ├── tailwind.config.ts ├── tsconfig.json └── package.json这个结构和官方 create-t3-app 基本一致,说明 t3code 是在 T3 官方最佳实践之上做的增强,不是另起炉灶。这也意味着你在社区搜到的很多 tRPC 教程、Prisma 教程,在这里都适用,学习成本没有浪费。
首次进入项目后,先跑pnpm install,再跑npx prisma db push把初始模型同步到数据库。t3code 初始化模板里已经内置了一个示例模型(比如用户和帖子),你可以先跑通一次完整的 CRUD 循环,再开始删改。
3. 核心功能拆解:三种高频代码生成场景
3.1 生成 tRPC Router:从手写到模板化
t3code 的代码生成命令格式很统一,基本是t3code generate <类型> <名称>。我先从最常用的 Router 说起。
假设你现在想给"文章"模块写一个 tRPC Router,不需要手动建文件,执行:
npx t3code generate router post它会自动在src/server/api/routers/post.ts生成一个文件。生成的初始代码长这样:
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; import { z } from "zod"; export const postRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { const items = await ctx.prisma.post.findMany({ orderBy: { createdAt: "desc" }, }); return items; }), getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { return await ctx.prisma.post.findUnique({ where: { id: input.id }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1), content: z.string() })) .mutation(async ({ ctx, input }) => { return await ctx.prisma.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });注意它默认生成了三种 procedure:publicProcedure不需要登录即可访问,protectedProcedure需要登录。这个区分很实用,因为实际项目中绝大多数查询都是公开的,而写操作基本都要求登录。它这么做正好逼着你一开始就思考接口的访问权限,而不是等上线后发现接口裸奔。
生成之后别急着完事,你需要手动把postRouter注册到src/server/api/root.ts里。这一步 t3code 没有做成全自动,原因是注册根路由涉及模块依赖关系,自动改容易出错。手动加上这行,整个 Router 就算通了:
import { postRouter } from "~/server/api/routers/post"; export const appRouter = createTRPCRouter({ post: postRouter, });前端调用时,直接体验一把类型安全:
const { data } = api.post.list.useQuery(); api.post.create.useMutation();不需要自己定义接口类型,前端拿到的每条数据都带着 Prisma 生成的类型。改一次 Prisma 模型,前后端类型全部同步更新,这就是当初选这套栈最值的地方。
3.2 Prisma Schema 生成与模型同步
Router 生成之后,紧接着要做的是创建对应的数据模型。手动写 Prisma Model 其实不难,难的是搞清楚字段类型、关系、索引和默认值。t3code 这里提供的生成命令是:
npx t3code generate model post执行前它会问你这个模型包含哪些字段,支持的类型包括字符串、整数、布尔值、日期时间和枚举。同样以 post 为例,完整一点的生成结果:
model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }这里有几个细节值得说明。@default(cuid())比自增整数更适合分布式环境和 URL 暴露场景,因为 id 不可枚举,猜不透别人的数据量。onDelete: Cascade表示删除用户时级联删除其所有文章,这个选择要看业务需求,如果你的文章需要保留归档,那这里应该改成SetNull并让 authorId 可空。
模型定义完成后执行:
npx prisma db push npx prisma generate因为 t3code 生成的模型和 Router 是配套的,所以执行完这两行后,刚才postRouter里的ctx.prisma.post就能正确识别所有字段了。我在使用中还发现一个技巧:当 Prisma 模型改多了以后,先用npx prisma format格式化一下 schema 文件再 push,能省下不少因为格式错乱导致的报错。
3.3 Zod Schema 与类型安全表单校验
第三个高频场景是 Zod Schema。tRPC 里每一项input校验都依赖 Zod,但很多人在写表单校验时仍然手写if (title.length < 1)这种土方法。t3code 针对这个场景提供了一个便捷命令,直接生成与 Prisma Model 字段对齐的 Zod Schema:
npx t3code generate schema post生成的 schema 会被放在一个统一的src/server/api/schemas/post.ts文件里,并和 Router 的 input 做映射。它的价值在于:Zod Schema、Prisma Model、tRPC 类型三者保持同步生成,不会出现数据库允许为空、接口层却校验必填这种低级错误。
比如生成的校验规则会对齐字段约束:
import { z } from "zod"; export const createPostSchema = z.object({ title: z.string().min(1, "标题不能为空").max(100, "标题最多100字"), content: z.string().min(1), published: z.boolean().default(false), }); export type CreatePostInput = z.infer<typeof createPostSchema>;z.infer这个操作值得多说两句:它把 Zod Schema 反推成了 TypeScript 类型,导入到前端后,表单的onSubmit参数就能自动获得推断类型,不用手动写 interface。这是类型安全闭环的重要一环。
当然,校验规则不可能完全自动生成。比如 title 最大长度是 100 还是 200,这属于业务规则,工具只能用 Prisma 的类型和长度约束给出默认判断。生成后你仍然需要根据业务去微调规则。工具能替你完成的是骨架和 90% 的体力活,剩下的 10% 业务判断是省不掉的,这反而是好事。
4. 一个完整示例:用户管理模块 CRUD
4.1 需求梳理与准备
讲完单独功能,我用一个完整的用户管理模块把整个流程串起来。需求也很常见:管理员后台需要查看用户列表、创建用户、重置用户密码、删除用户。
这个模块完全可以用 t3code 的生成能力快速搭出雏形。先把用户模型补全,因为默认模板里的 User 模型比较简略,我会手动添加一个role字段,用于区分管理员和普通用户。
enum Role { ADMIN USER } model User { id String @id @default(cuid()) name String? email String @unique role Role @default(USER) createdAt DateTime @default(now()) }值得注意的是,如果你要在这个项目上接 NextAuth,这个 User 模型还需要有accounts和sessions关系,这个要小心处理。t3code 的开发人员在这个地方做了个贴心处理:当你执行t3code generate model user并检测到项目已启用 NextAuth 时,它会自动补全关联字段,避免你回去手写一堆容易出错的关系。
4.2 用 t3code 生成整套 CRUD 模板
模型更新后,在prisma/schema.prisma里同步好 Model,执行数据库同步:
npx prisma db push npx prisma generate接着用 t3code 生成一个用户管理的 Router:
npx t3code generate router user --crud注意这个--crud参数,它会生成比默认模板更完整的增删改查方法,包括list、getById、create、update、delete五个方法。逐个看下关键部分:
update: protectedProcedure .input(z.object({ id: z.string(), name: z.string().optional(), role: z.enum(["ADMIN", "USER"]).optional(), })) .mutation(async ({ ctx, input }) => { return await ctx.prisma.user.update({ where: { id: input.id }, data: { name: input.name, role: input.role, }, }); }), delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { return await ctx.prisma.user.delete({ where: { id: input.id }, }); }),这里有个权限问题必须要自己补一段逻辑:现在任何登录用户都能调用update和delete更新别人的资料,这显然不行。我一般在update前加一个判断:只有ADMIN角色能操作,或者id === ctx.session.user.id时允许用户更新自己的信息。
update: protectedProcedure .input(...) .mutation(async ({ ctx, input }) => { if (ctx.session.user.role !== "ADMIN" && ctx.session.user.id !== input.id) { throw new TRPCError({ code: "FORBIDDEN", message: "无权操作" }); } // 省略后续更新逻辑 });t3code 不会替你写业务权限,但它生成protectedProcedure已经守住了"未登录不能调用"的底线。至于"登录了但没权限"这种场景,就得靠你自己的业务判断来补强。
4.3 前后端联调与细节优化
后端 Router 生成后,前端调用非常顺手。在 React Hook 里直接用:
const users = api.user.list.useQuery(); const updateUser = api.user.update.useMutation({ onSuccess: () => users.refetch(), });这里有一个小设计值得分享:t3code 生成的update方法中,入参类型已经解析好了Prisma.UserUpdateInput,但它在生成输入的时候故意改成可选项,避免前端需要把整条用户数据原样回传的情况。这种细节说明工具作者是真做过实际项目,不是纸上谈兵。很多初学者写更新接口时会把整个对象回传,这在真实项目中既不安全也没效率。
联调时我还会用 tRPC 的isLoading和isFetching区分初次加载和后台请求。很多 UI 库的 loading 态混乱,就是因为没有区分这两者。第一次进页面时用isLoading展示骨架屏,后续操作时用isFetching控制按钮禁用,体验会细腻很多。
5. 常见问题与坑位速查
5.1 数据库连接失败
t3code 搭配 SQLite 时很少出问题,问题主要集中在 PostgreSQL。新建的 Postgres 数据库如果是在 Docker 里跑的,连接串一定要核对 host 和端口:
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"常见错误是PrismaClientInitializationError,十有八九是连接串里用户名密码写错,或者数据库还没创建成功。还有一个坑:如果你在docker-compose.yml里配置了数据库但在容器内又映射了不同的端口,本地连接时端口写错也会报这个错。排查顺序建议是:先docker ps确认容器在跑,再试连接串,最后才看代码。
5.2 Prisma 版本与生成类型不同步
这是使用 t3code 时最隐蔽的问题。当 Prisma 模型改动后,如果你只跑了prisma db push而忘了再跑npx prisma generate,前端 TypeScript 不会报错,但运行时ctx.prisma.user.findMany()可能拿不到新字段的类型。
这两个命令的区别在于:db push把 schema 同步到数据库,generate更新 client 的 TypeScript 类型。只执行前者,数据库变了但你的类型文件没更新;只执行后者,反过来。我的习惯是每次改完 schema,必须连着执行:
npx prisma format npx prisma db push npx prisma generate三个命令一条龙,缺一不可。如果用了 Prisma 的 migration 工作流,那就把中间那步换成npx prisma migrate dev --name 描述。
5.3 端口冲突与环境变量
Next.js 开发服务器的默认端口是 3000,但如果你同时开了多个前端项目,或者有别的服务占用了 3000 端口,启动时会直接报EADDRINUSE。解决方法很简单,在启动命令里改端口:
pnpm dev -- -p 3001还有一种情况是.env文件里的DATABASE_URL被误删或没创建。t3code 初始化模板里会生成一个.env.example,第一次使用时最好复制一份为.env再填值。很多报错其实都源于环境变量缺失,而不是代码问题。
5.4 tRPC 类型报错
使用 t3code 生成的 Router 后,前端经常出现的一个类型报错是Unable to infer type from context。这个问题通常是因为在trpc.ts里创建的createTRPCContext返回的prisma实例类型不匹配。检查一下src/server/db.ts里导出的是不是PrismaClient实例,并且trpc.ts里引用的是同一个实例。它是单例模式,不能每次请求重新new PrismaClient(),否则会出现连接数爆炸,还会导致类型推断互相不认。
另外一个常见问题是,修改 Router 后前端类型没有立即更新。tRPC 的类型是通过 TypeScript 推断的,理论上改完后端,前端保存一下就能同步。如果没同步,重启一下 dev server 基本都能解决。
5.5 生成代码后的二次修改注意事项
最后提醒一下:t3code 生成代码后,如果你做了大量手改,后续再执行同名的generate命令,它不会智能合并,而是直接覆盖。我在早期的项目里踩过一次,改了不少业务逻辑的 Model 被一个覆盖命令打回原形。后来我的做法是:生成后把 Router 文件里的核心业务逻辑全部迁移出生成区域,让生成的文件只保留薄薄的一层 CRUD 壳子。这样即使被覆盖,损失也在可控范围内。
6. 我的一些使用心得与扩展建议
用 t3code 跑了两个项目之后,我对它的定位有了更清晰的认识:它是一个非常好的全栈项目起步工具,也是一个平时开发中能持续调用的代码助手,但它不是万能代码生成器。它的价值在于帮你把 T3 Stack 的最佳实践固化成可复用的模板,让你把精力花在业务逻辑和产品体验上。
按照个人经验,新手用 t3code 的最好路径是:先用默认模板完整跑一个 Demo,理解 tRPC 的请求链路和 Prisma 的类型传导,再尝试用 generate 命令生成一个新的业务模块,最后再逐步加权限、加校验。不要一上来就改它生成的模板内部结构,否则你很难判断问题出在工具还是自己代码里。
最后分享一个小技巧:如果团队协作,建议把生成代码的规范写进 README,包括命令格式、模型命名习惯、是否允许手改生成区。t3code 本身已经帮你把重复劳动压缩到最低了,剩下的就是让团队在使用时保持约定一致。毕竟工具是死的,流程才是活的,把流程理顺了,开发效率的提升会远超你装几个脚手架带来的那点收益。