ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

t3code:T3 Stack 端到端类型安全全栈开发实践

t3code:T3 Stack 端到端类型安全全栈开发实践

先聊点实际的。我第一次接触t3code这个名字,是在我把整整半年折腾出来的 T3 Stack 项目沉淀成模板库那会儿。当时手头同时维护三个全栈项目,每个项目的前后端联调都要花掉我大量时间:后端改了个字段名,前端等到编译时报错才发现;文档写了十几页,没人看,也没人更新;接口路径稍微一改,整个调用链跟着乱。T3 Stack 就是在这种情况下进入我视野的,而 t3code 是我对这套技术组合的个人工程化落地实践。这篇文章我会把从脚手架初始化、数据库设计、tRPC 路由、鉴权接入到生产部署的全过程拆开讲,记录我实际踩过的坑和最后沉淀下来的方案,适合已经会用 React 和 TypeScript、但还没系统性接触过 T3 这套组合的前端或全栈开发者,也适合独立开发者想快速搭一个类型安全全栈项目时参考。

1. t3code 是什么:T3 Stack 背后的工程哲学

1.1 先搞清楚 T3 到底在说哪三件事

T3 Stack 这个名字很多人听过,但它的定义其实一直在变。早期的 T3 指的是 The T3 Stack,由 Theo 带火,核心组合是 Next.js、TypeScript、tRPC 和 Tailwind CSS,后来随着生态演进,Prisma 和 NextAuth.js 也几乎成了默认成员。T3 里的“T”其实源自 Tier 3,意思是“足够高但又不过度复杂的三层架构”——不是简单地把技术堆在一起,而是围绕一个核心诉求组织代码:端到端的类型安全。

t3code 这个词在社区里有不同理解,有人拿它当 T3 项目的代名词,也有人像我一样,把它当成一套可复用的工程实践模板。我更倾向于后一种理解。T3 天生不是一个“框架”,它没有规定你必须用哪个 ORM、必须把文件放在哪个目录,它只给了你一个骨架和一套原则。所以你在 GitHub 上搜到的 create-t3-app 模板、各种 t3-code 仓库,本质上都是对这套原则的工程化落地。我的 t3code 实践库也是这么来的——把最顺手的配置、最不容易出错的目录结构、最稳妥的部署方案固化下来,避免每次从零开始重复踩坑。

传统全栈项目里,前后端像是两个公司,靠传真机沟通:后端定义 JSON 结构,前端靠文档和后端约定字段名,类型全靠手写 DTO 或者硬编码“接口响应就是这个形状”。一旦响应结构变了,最先崩溃的不是运行着的服务,而是两边的信任。T3 解决的正是这个问题,它把后端接口的类型直接暴露给前端,字段改了,IDE 里立刻报错。你可以把 tRPC 想象成在前后端之间拉了一根类型管道,数据形状在这里是同一个 TypeScript 类型系统的两个视图。

1.2 类型安全为什么成了稀缺品

拿我自己曾经写过的例子来说。用户列表接口的响应是{ user: { id, name, email } },后来后端把user.name改成了user.username,并且 SQL 查出来的列名也改了。在传统 API 模式下,除了后端自己,没人知道这个字段已经变了。Swagger 文档也许有人更新过,也许没有,前端代码里resp.user.name依然存在,直到生产环境用户反馈“名字不见了”,你才开始排查。你可能会说,这不是有 TypeScript 吗?问题是传统 REST API 的类型边界只存在于前后端各自的代码里,跨过 HTTP 边界之后,类型检查就断了。

T3 的做法是把这条边界彻底打通。tRPC 在服务端定义 router 时,输入输出的类型就已经被推导出来,前端通过trpc.post.getAll.query()调用时,返回类型的推断精确到字段级别。你改 Prisma schema 里的username字段,跑一次prisma generate,服务端的 tRPC router 类型变化,前端调用处马上跳出红色波浪线。省掉的不是“写类型定义”的时间,而是排查“类型文档和实际数据不一致”的时间。在实际项目里,这种不一致才是真正浪费大头的隐性成本。

对于没接触过 tRPC 的朋友,我打个比方。传统 REST 联调像两个部门用传真沟通:你发过去的表格对方怎么填、字段名怎么定,你只能在电话里反复确认。T3 里的 tRPC 更像同一个办公室共享一块白板:你写上去的内容对方实时能看到,而且大家用的是同一支笔、同一套格式。不是说你不需要理解白板上内容的含义,至少格式不会再成为阻碍沟通的障碍了。

1.3 这套东西适合谁,不适合谁

先说适合谁。我是做独立开发和二三十人规模小团队的全栈工程师,手头项目数量多、业务变化快、没有专职 API 文档维护者。T3 特别适合这种场景,它把“类型安全”往前移了一整个阶段,团队里就算有人改字段忘了通知,代码编译期就能暴露。其次,适合已经有 React 基础和 TypeScript 基础的人,你只需要补上 tRPC 和 Prisma 这两块新知识,剩下的 Next.js 和 Tailwind 都是熟悉的面孔。

那不适合谁呢?如果你的业务需要一个被大量第三方调用的公开 REST API,tRPC 并不是一个好的对外暴露方案,它更适合作为应用自己的 BFF 层,外部客户端跨网络调用不适用。另外如果你前端不是 TypeScript,而是小程序原生 JS 或者某个老旧的 jQuery 系统,端到端类型穿透就无从谈起,tRPC 带来的收益会大打折扣。最后,如果你的团队里没人愿意写 TypeScript,那 T3 这套东西在你团队里大概率会变形,最后变成“一个 Next.js 项目加了个没人用的 tRPC”。工具再好,也得有会用、愿意用的人。

2. 从脚手架开始:初始化一个 t3code 项目

2.1 环境准备与命令行细节

我初始化 T3 项目用的是官方脚手架 create-t3-app,它比你自己手动装 Next.js、tRPC、Prisma、Tailwind 再一个个配置要省下至少半天时间。而且难能可贵的是,这个脚手架把很多容易出错的细节都处理好了:tRPC 的 App Router 适配层、Prisma client 的全局单例、NextAuth 的 session 上下文接入,全都帮你配置好了。你改的只是业务代码,不用从零铺基础设施。

环境要求很明确:Node.js 18.17.0 及以上,包管理器推荐 pnpm。命令长这样:

node -v # 小于 18.17 就去升级 pnpm create t3-app@latest my-t3-code

运行之后,脚手架会给你一串交互式选项。我个人的选择是:App Router(默认)、tRPC 选上、Prisma 选上、NextAuth.js 选上、Tailwind CSS 选上。注意有一个选项叫 "Experimental features" 或者类似的启用项,比如Drizzle ORM和Valibot之类的新东西,我建议新项目先不要过度追求新,以稳定为主,等 Drizzle 生态彻底成熟了你再切换,成本也不高。

脚手架跑完后,它会提示你设置环境变量。别急着忽略,DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL这三个变量是这个项目能不能跑起来的关键。不过更细的坑放在下一节,这里只说一点:你可以先照抄.env.example里的占位符,但别直接把占位符当成可用的配置,后面一定会出问题。

2.2 生成后的目录结构拆解

脚手架生成的目录结构里,我重点讲四个地方。第一个是src/server/db.ts,这是 Prisma client 的单例入口。它用了globalThis来缓存 client 实例,避免在 Next.js 热更新时创建大量数据库连接。生产环境跑久了你会发现连接数爆掉,很多时候就是没做这种单例处理。

第二个是src/server/api/trpc.ts。这个文件是整个 tRPC 后端的核心工厂。它做了三件事:创建 tRPC context、初始化 tRPC 实例、定义公共 procedure 和受保护 procedure。它的代码看一眼就懂:

const createTRPCContext = async (opts) => { const session = await getServerSession(); return { db, session, ...opts }; }; const t = initTRPC.context<typeof createTRPCContext>().create(); export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use( t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user } } }); }) );

第三个是src/app/api/trpc/[trpc]/route.ts。这是 tRPC 在 App Router 下的 HTTP 入口。如果你把这个文件放错目录,或者手贱改了大括号里的[trpc],你会浪费几个小时去排查“为什么所有接口 404”。这个文件基本不需要改,但你要知道它存在、负责什么。

第四个是src/trpc/server.ts和src/trpc/react.tsx。前者是给服务端组件和 Server Actions 调用的 caller 封装,后者是 React Query 的 Provider 和 hooks 封装。前端组件里trpc.note.getAll.useQuery()能用,靠的就是react.tsx的封装。

2.3 选型纠结:Prisma 还是 Drizzle

脚手架默认带的是 Prisma,但确实有一股越来越多人转 Drizzle 的趋势。我在 t3code 里的实践是:默认 Prisma,但 Schema 设计要尽量摆脱 Prisma 的思维惯性。为什么这么说?

Prisma 的优点是对底层 SQL 做了高度抽象,schema 文件就是你的数据库设计文档,迁移工具也成熟,团队上手快。缺点是遇到特别复杂的查询时,它的 API 会写得很别扭,性能调优也隔着层纱。Drizzle 更贴近原生 SQL,写起来厚重,但性能和可控性强,TypeScript 也很完整。然而 Drizzle 在迁移工具、生态成熟度上目前还是有差距。

我一般这样建议:如果团队成员都有 SQL 功底,又不喜欢黑盒,选 Drizzle;如果团队更看重开发速度、迁移安全和代码可读性,选 Prisma。这个表格可以做参考:

对比维度PrismaDrizzle
学习曲线平缓,DSL 一致性好陡峭,需懂 SQL
查询能力强,但复杂查询别扭强,贴近原生
迁移工具成熟,有 migrate dev还在快速迭代
类型安全很好极好
适用场景默认推荐、中小规模团队SQL 老兵、性能敏感场景

不管选哪个,T3 的核心价值没变:tRPC 给你端到端类型安全,ORM 只是数据层的实现细节。DATABASE_URL指向同一个数据库,router 结构完全一样,变更的成本没有想象中那么大。

3. 写一段真实的 tRPC CRUD:以“用户笔记”为例

3.1 数据模型先行:从 schema 开始设计

t3code 的开发流程我建议是“schema 优先”。先想清楚数据库表结构,再写 router,再写前端调用。因为类型安全的链条源头就在 Prisma schema 里,schema 一变,后面每一层的类型都会跟着变。

拿一个用户笔记功能举例。我要一张只属于当前登录用户的笔记表。schema 写得简练一点:

model Note { id String @id @default(cuid()) title String content String? userId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) }

这里有两个细节值得说。一是主键为什么用cuid()而不是自增Int。cuid 在分布式环境下不用担心并发生成冲突,也避免从 URL 里暴露笔记数量的隐私问题。二是@@index([userId]),如果你的查询里大量出现where: { userId: ... },没有这行索引,数据量稍微上来一点就是全表扫描,页面慢到怀疑服务器在南极。

写完 schema 之后,执行npx prisma migrate dev --name init,生成迁移文件。如果 schema 有语法问题,这一部就会暴露,不用等到运行时。

3.2 编写 tRPC Router:query 和 mutation 怎么写才规范

接下来是 tRPC 的 router。我一般把数据相关的 router 分文件放,比如src/server/api/routers/note.ts,然后在根 router 里聚合。

先定义一个 query,用来获取当前用户的笔记列表。这里我使用protectedProcedure,因为笔记是隐私内容,必须校验登录状态:

export const noteRouter = createTRPCRouter({ getAll: protectedProcedure.query(({ ctx }) => { return ctx.db.note.findMany({ where: { userId: ctx.session.user.id }, orderBy: { createdAt: "desc" }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1, "标题不能为空"), content: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.note.create({ data: { title: input.title, content: input.content, userId: ctx.session.user.id, }, }); }), });

这里有一个新手容易犯的错误:直接在 handler 里if (!ctx.session)然后throw。不是说不行,但很啰嗦。protectedProcedure的 middleware 已经统一做了拦截,路由里就能保证ctx.session.user一定存在。infer 出来的上下文里就带上了 session,类型上它不会让你漏掉空值判断,这就是类型穿透带来的安全感。

mutation 里的.input()用了 Zod。T3 社区对记录保存方式有过一番纠结,Zod 是最稳的选择。Zod 在这里承担两个职责:运行时校验非法输入,编译时推断精确类型。如果有一天你忘了给某个 mutation 加 input,TypeScript 会直接在 router 定义处报错,因为你调用的那个 mutation 签名要求 input 必须存在。

3.3 前端调用:useQuery 与 useMutation 的联动干活

前端调用邻居部分也很关键。t3code 项目里,前端不会直接 fetch/api/trpc/note.getAll,一切都是通过src/trpc/react.tsx里的封装调用,它把 React Query 类型全部打通了。

拿到笔记列表的代码是:

const { data: notes, isLoading } = trpc.note.getAll.useQuery();

新增笔记之后,需要通过useUtils来刷新列表缓存:

const utils = trpc.useUtils(); const { mutate: createNote } = trpc.note.create.useMutation({ onSuccess: () => { utils.note.getAll.invalidate(); }, });

React Query 在 T3 里的角色是数据缓存和状态管理。invalidate()就像是你在刷新微信消息列表时下拉一下,它会重新拉取getAll数据。如果不做这步,新增的笔记不会出现在列表里,你得手动刷新页面。这里有个经验:对列表数据尽量用invalidate,而不是直接操作缓存写数据。操作缓存写看似省了一次请求,但数据一致性很容易在多次连续操作时出问题。invalidate虽然多一次网络开销,但换来的是简单可靠。

3.4 校验与错误信息打磨:别让用户看到英文报错

Zod 的错误信息默认是英文的,而且字段路径是title: required这种不好看的格式。我建议给input的每个字段都写清楚中文提示,比如上面代码里的"标题不能为空"。别偷懒,一个团队里只要有一个人图省事不写,某个深夜线上弹出一句 "Invalid input" 的验证错误,你就得花十分钟去猜是哪个字段验证失败了。

另外,tRPC 的错误码和服务端 HTTP 状态不是一回事。tRPC 里UNAUTHORIZED对应 401,NOT_FOUND对应 404,但它并不会帮你自动处理前端提示。前端接错误的规范做法是:

const { mutate: createNote, error } = trpc.note.create.useMutation();

error.message里是服务端抛出的原始信息,要展示给用户之前,最好先用 Zod 的 errorMap 或自定义 message 做一层整理。你不希望用户看到内部实现细节,也不希望他看到一段英文堆栈。

4. 认证接入与状态管理:session 是 T3 项目的命门

4.1 NextAuth + Prisma 的接入顺序

T3 项目里认证方案一般是 NextAuth.js,配合数据库 session 存储。脚手架已经配好了 NextAuth 的基本结构,但真正要跑通有几个顺序问题。

先设定环境变量。.env文件里必须有:

DATABASE_URL="postgresql://user:password@localhost:5432/t3code" NEXTAUTH_SECRET="自己生成一串足够长的随机字符" NEXTAUTH_URL="http://localhost:3000"

NEXTAUTH_SECRET特别关键。本地开发时不写,NextAuth 会运行在一个开发模式,但生产环境如果不写,服务直接启动失败。生成一个随机 secret 的办法是openssl rand -base64 32,别想着自己手打一串字符当成保险。

接着如果有 OAuth 登录需求,去 Google 或 GitHub 控制台申请GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET。这里要按 NextAuth 的 adapter 配置来。如果用了 Prisma 存 session,你需要把 NextAuth 的PrismaAdapter接上去,它会在数据库里自动建表,用来存 User、Account、Session 和 VerificationToken。建表前要先执行prisma migrate dev,否则运行时 Prisma 会报“表不存在”。

4.2 protectedProcedure 到底保护了什么

很多教程把 tRPC 的 protectedProcedure 当成“页面守卫”在讲,但我更愿意把它理解成 API 守卫。它保护的是数据访问本身,不是 UI 里某一条导航链接是否可见。前端页面你可以根据登录状态决定渲染什么,但如果没有这一层 API 守卫,任何懂一点网络请求的人都能绕过界面直接调你的接口。

T3 的 protectedProcedure 是基于会话中间件的。会话不存在就会抛UNAUTHORIZED,前端收到错误后自动跳转登录页。如果你只是在前端判断了登录态而接口没做保护,等于把家门钥匙放在门口垫子下面,运气不好就有人摸进去了。

还需要注意一点:服务端组件里调用 tRPC 也要走这个守卫。Next.js App Router 服务端组件里可以直接从数据库查数据,但这不意味着你可以绕过 tRPC。如果你在服务端组件里直接ctx.db.note.findMany(),那和 “全栈” 没关系了,类型安全链条被切断,后续维护会很难受。正确的做法是通过src/trpc/server.ts的createCaller调用 tRPC API,让同一套中间件和类型系统在服务端也生效。

4.3 中间件与 RSC 的边界

Next.js 里middleware.ts经常被用来自动检查登录态,比如未登录用户访问题问某个路径时直接重定向到登录页。T3 实践里建议:middleware 只负责“页面级路由守卫”,而且最好只处理无需数据库的场景,比如检查 JWTsessioncookie 是否存在。因为 middleware 常常跑在 Edge Runtime,不能访问 Prisma Client,完全做不了数据库操作。

在 App Router 模式下,页面级别的守卫我更推荐在 Server Component 层面做。用一个protectedServerPage函数包装一下,它内部调getServerSession(),判断没有 session 就redirect("/login")。这样所有页面组件不用重复写守卫逻辑,职责也更清晰:

层级职责用什么
MW全局路径拦截、静态资源过滤Middleware + JWT
RSC页面级登录校验getServerSession + redirect
tRPC数据级权限控制protectedProcedure
UI按角色显示不同交互组件内 useSession

这个分层是 T3 项目里最难讲清楚的坑之一。很多新人把所有的拦截都放在 middleware 里,结果有一天用 tRPC caller 在 RSC 里查数据库时发现 session 拿到的是空值,因为 middleware 只是重定向了浏览器请求,RSC 内部的数据流没走那套东西。把职责拆清楚,这种问题才能从根源上避免。

5. 部署与生产环境:从 typecheck 到数据库迁移

5.1 构建前先过类型检查

T3 脚手架里的package.json默认提供了一个check脚本,内容是next lint && tsc --noEmit。但很多人部署时只跑next build,完全忽略了这个check。Next.js 的 build 过程确实会做一部分类型检查,但不是全量的,很多跨文件的类型问题能漏过去。问题不大,直到运行时才爆出来,费时费神。

我在 t3code 里的 CI 流程,第一步就是跑pnpm check,第二步再跑pnpm build。如果你用的托管平台支持多阶段构建,把这两步拆开。如果你用 GitHub Actions,参考这一段:

- name: Check & Build run: | pnpm check pnpm build

这只是一个很简单的示意,实际跑起来你会发现check卡住的往往不是代码,而是你写了一半的 schema 改动。这正是目的所在:把问题挡在构建之前。

5.2 数据库迁移策略:dev 用 push,生产用 deploy

Prisma 的迁移指令有几个容易混淆,我这个坑踩得比较深。prisma db push是直接把 schema 同步到数据库,适合开发前期飞快迭代。prisma migrate dev是生成迁移文件并应用,适合已经进入稳定期。prisma migrate deploy是生产环境专用,只执行项目的 migrations 目录下的迁移文件,不会自动生成新迁移。

t3code 的实践是:隔离环境里用db push没问题,但 CI 里也要遵守这个区分。我见过不少人在 CI 里用prisma db push,结果迁移历史里记录缺失,生产环境没法回滚,一下把团队带进坑里。正确的生产部署流程是:

# 迁移步骤 npx prisma migrate deploy # 生成 client(部署平台一般自动执行) npx prisma generate

如果生产环境需要建表,先本地或审查分支里执行npx prisma migrate dev --name your_change,生成迁移文件,提交到代码库,CI 里跑migrate deploy应用它。这套流程是标准的“migration as code”思路,避免直接手工改生产数据库。

5.3 部署后的几个诡异故障

部署之后我遇到过几个特别折腾的问题,列出来供你对照。

第一个是PrismaClientInitializationError,十有八九是DATABASE_URL没设置。很多托管平台只把环境变量给到“生产环境”分组,预览环境或一次性分支环境是空白的,部署时就报错了。办法是在部署控制台把同一套环境变量复制全。

第二个是数据库连接池耗尽。这件事在 serverless 平台上尤其明显,每个请求都会创建新的 Prisma client,如果没做单例缓存,连接数会线性增长。即使做了单例,PostgreSQL 默认连接数 100,并发一旦上来了也可能爆。我在生产环境里给DATABASE_URL加上连接数限制参数:

DATABASE_URL="postgresql://user:password@host:5432/db?connection_limit=5"

这样每个 serverless 实例最多占用 5 个连接,极大降低压垮数据库的风险。

第三个是 NextAuth 在生产环境提示 “Invalid check: jwt” 之类的问题。通常和 session 存储策略有关。如果你用的 Prisma 存 session,检查是否把 NextAuth 的session: { strategy: "database" }配了。默认策略是jwt,它不需要 database 表,但你的PrismaAdapter又建了表,两者不冲突但行为让人困惑。t3code 的默认建议是:App Router 项目里用 JWT 策略,能省掉一次数据库查询,配合中间件也顺滑;用了 database 策略并在 RSC 里读 session,就会增加一次数据库查询,但换来的可以是更可控的服务端 session 管理。用哪种没有绝对对错,知道自己在干嘛就行。

6. 常见问题排查与最终实践心得

6.1 高频报错速查表

报错/现象原因处理方式
TS2339:Property 'id' does not exist on type 'Session'NextAuth 类型声明没扩展创建types/next-auth.d.ts,声明Session.user.id
PrismaClientInitializationErrorDATABASE_URL未设置检查环境变量,重启部署
tRPC 所有接口 404[trpc]路由文件放错位置确保路径是src/app/api/trpc/[trpc]/route.ts
TRPCClientError: UNAUTHORIZED未登录调用 protectedProcedure前端判断登录态后跳转登录页,或检查 NextAuth 配置
部署后白屏/Analytics 无数据缺少NEXTAUTH_SECRET生成并配置随机 secret
数据库连接数满未做 client 单例或连接数过大用globalThis缓存 Prisma client,加connection_limit

6.2 我的几点取舍心得

用 T3 这套组合三年多,最大的感悟是:不要被“必须全套使用”束缚。t3code 这个名字对我来说是一个工程实践库,但它并不是一套强制的全家桶套餐。你可以在一个已有项目里只接入 tRPC,把原来的 REST 接口逐步替换;也可以不用 Prisma,用 Drizzle 或直接写 SQL 查询。只要保留核心的“类型穿透”,T3 的价值就已经体现出来了。反过来,如果因为强制要求使用某个框架导致团队效率下降,那就是舍本逐末了。

比如我最近一个项目里并没有上全套 T3,而是只用了 Next.js + TypeScript + tRPC,因为那个项目的数据库是已有的遗留 Schema,用 Prisma 重新建模反而碍手碍脚,Drizzle 更贴近现有 SQL。但数据层的选择没有影响 tRPC 的端到端类型体验,前端照样类型安全。这说明 T3 的骨架是解耦的,你可以像拆乐高一样按需取用,而不是当做一个黑盒式的全家桶。

还有一个我特别想强调的点:T3 项目要想保持舒服,schema 改动频率必须被克制。你可以用 tRPC 做到改一个字段前端立刻爆红,但如果你每天都在改字段,那前端队友的体验会非常糟糕。类型安全给了我们安全感,但也可以让人变得松懈,觉得改字段很容易。我在 t3code 的规范里明确要求:任何 schema 字段变更必须走一次“数据库迁移 + tRPC 类型检查 + 前端调用点确认”三件套,否则就不应该合入主干代码。

6.3 往后还能怎么演

T3 的生命力在于它始终在跟着整个 React 和 Next.js 生态演进。App Router 的 Server Actions 出来后,有人觉得 tRPC 会被取代,但我个人看法恰恰相反——Server Actions 更适合做表单提交这一类轻量交互,而 tRPC 的 router 模式更适合做结构化、强类型的数据查询接口。实际项目里两者已经开始共存了:有些团队用 Server Actions 做页面上的表单,用 tRPC 做数据加载和跨组件共享状态。这张中间的接缝会越来越平滑,值得持续观察。

我最近也在试 Drizzle + Valibot 的组合,后者作为 Zod 的轻量替代,在 bundle size 上的优势很有吸引力。如果你选的模板比较新,create-t3-app 也已经有了 Drizzle 选项,可以试试看。但不管底下的数据校验器换成谁,tRPC 的核心模式“输入输出沿着 TypeScript 类型穿透到底”始终没有动摇。我之所以愿意一直跟进 T3 的生态变化,恰恰是因为它的选型并不偏执。主流的方案在它这里都能有一席之地。

最后分享一个小技巧。如果你在维护 t3code 类似的项目,建议在每次 Prisma schema 改动后跑一遍npx prisma format && npx prisma generate,前者整理 schema 文件格式,后者重新生成类型。很多人改完 schema 之后忘了跑generate,接着就是满屏的 TS 报错。把这两条命令收进大脑肌肉记忆,能帮你省下很多和一排排红色波浪线搏斗的时间。

写代码这件事,本质上是为了少写代码。类型安全就是帮你少写的那部分,不是约束你的。T3 这套东西我用了这么久,最大的收益不是“不出 bug”,而是“出 bug 的时候知道去哪里查”,这比什么都值。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进