标题"t3code"在圈内混久了的人一眼就能猜到它的意思——这是一个以T3 Stack为核心技术栈的全栈项目代号。T3在这里不是版本号三,而是"The T3 Stack"的简写,社区里习惯把它当成一套现代Web开发的组合拳:Next.js负责框架层、TypeScript锁类型、tRPC打通前后端调用、Tailwind CSS管样式,再搭配Prisma操作数据库、NextAuth处理登录认证。t3code说白了就是这套技术栈的完整代码实践。
我这篇文章不打算做概念科普,而是把我用t3code这个代号从零搭建一个带用户体系的团队任务管理应用的真实过程拆开讲。里面涉及选型理由、Schema设计、tRPC的端到端类型安全链路、部署时的坑、以及几处版本冲突的排查记录。适合两类人看:一类是听过T3 Stack但一直没完整跑通一个项目的前端开发者,另一类是已经在用Next.js但想搞清楚tRPC和Prisma到底怎么嵌进来的人。
1. t3code到底是什么:一个基于T3 Stack的完整全栈项目实践
1.1 “t3”不是版本号,而是一套技术栈的速记
先说清楚命名。很多人第一次看到"t3code"的时候会下意识觉得这是某个软件的第三个大版本,其实不是。T3最初来自"Three.js"社区的玩笑——Three的"Three"正好压了字母T和数字3的双关。但后来社区把The T3 Stack定义为一套固定搭配:Next.js + TypeScript + tRPC + Tailwind CSS,四个T开头的东西凑在一起,就成了T3。
不过实际用起来,光这四个还不够完整,一个真实项目还得有数据层、认证层。所以社区里大家所说的T3 Stack通常默认带上Prisma和NextAuth.js。t3code这个仓库名,就是我的T3 Stack实战项目代号,里面的代码完全围绕这套方案展开。
1.2 我拿t3code做了什么
项目的具体形态是一个轻量级的团队任务管理工具,功能不复杂但覆盖了全栈应用的所有核心环节:
- 用户注册、登录、会话管理(NextAuth + Credentials)
- 创建项目、邀请成员(Prisma数据模型 + 多对多关系)
- 任务的增删改查、状态流转、指派负责人(tRPC Router + Mutation)
- 列表页、详情页的实时刷新(React Query缓存失效)
- 部署到生产环境(Vercel + PostgreSQL)
选"任务管理"这个方向是有意为之。它足够典型,既包含认证,又包含CRUD,还有跨表查询和权限控制的边界场景。你可以把t3code当成一个模板仓库,把里面的项目实体换成商品、订单、文章都行。
1.3 这篇实战记录适合谁
说实话,T3 Stack对小白并不算友好。它把太多配置自动化了,新手在create-t3-app生成的模板里改东西很容易"能跑但不懂为什么"。所以这篇文章更适合已经写过几个React项目、对Next.js的App Router有一定概念、同时想知道"为什么这么多人推荐tRPC代替REST"的开发者。
如果你是纯后端或者纯前端背景,也没关系。t3code的技术栈里最值钱的部分恰恰是它把"类型安全的边界"拉得很清晰,读完你会对现代Web应用的黏合层有更直观的感知。
2. 为什么是这套组合:Next.js + tRPC + Prisma的选型逻辑
2.1 Next.js:前端的路由和API都在一个进程里
选Next.js不是因为它最潮,而是因为它让t3code只用一个代码库就能同时处理页面渲染和接口供给。App Router的服务器组件天然适合做需要登录态的页面,而Route Handlers又能承担轻量API的角色。关键在于,Next.js的服务器组件可以直接在服务端读数据库、读会话,不用额外暴露接口。
这在任务管理应用里的直接好处是:列表页的初始数据可以由服务器组件直接查询Prisma拿,不需要先加载页面再发一次异步请求。首屏速度、用户体验、代码复杂度三方面都受益。
2.2 tRPC:真正意义上端到端的类型安全
t3code的核心亮点其实是tRPC。传统前后端协作方式里,最麻烦的不是写接口,而是维护接口文档和类型对齐。REST接口写好了,前端要手动定义类型;改一个字段名,前端类型不报错,直到运行时才发现。
tRPC的思路完全不同:你在服务端定义一个Router,里面的每个procedure就是一条可调用的"接口",前端拿到的是由这个Router推导出来的类型,调用的时候自动获得入参和出参的类型校验。改后端字段,前端编译直接报错。没有OpenAPI、没有代码生成、没有额外的schema文件,全靠TypeScript的推导能力。
在t3code里,任务状态的枚举、用户ID类型、日期字段,全部在前后端共用一个类型源。我从写这个项目到迭代了十几个版本,几乎没有出过"字段名打错了导致线上白屏"的问题。
2.3 Prisma:数据库操作不是写SQL而是写Schema
Prisma在T3 Stack里承担数据层。它不像ORM那么笨重,本质上是"Schema驱动的数据库访问层"。你在schema.prisma里定义模型,跑一次migrate生成表结构,客户端查询就带完整的类型提示。
t3code里最常用的是这几类查询:
- 嵌套查询:查项目时一次性把任务、成员、创建人全部带出来
- 关联过滤:查"指派给我且状态未完成的任务"
- 事务操作:创建项目时同时写入成员关系表
Prisma还有一个对前端开发者特别友好的特性:它的查询结果天然是结构化对象,不需要像手写SQL那样做繁琐的结果集映射。配上TypeScript联合类型,前端可以直接用project.tasks这种属性访问方式。
2.4 这套组合的边界:什么时候不该用T3 Stack
诚实地讲,t3code这套技术栈不是万能的。我在选型之前做过几轮对比评估:
| 场景 | T3 Stack表现 | 更合适的方案 |
|---|---|---|
| 中小企业内部工具 | 很好,开发效率高 | 无 |
| 高并发C端接口 | 偏弱,tRPC调试成本高 | NestJS + OpenAPI |
| 需要对接数十个第三方系统的网关 | 不合适,类型推导优势消失 | GraphQL + BFF |
| 纯静态内容网站 | 大材小用 | Astro / 纯Next.js |
如果你的核心业务是复杂的高性能接口聚合,或者整个团队前端和后端严格分离、需要稳定的API契约文件对外发布,那T3 Stack并不理想。tRPC的强项是"同一个团队全栈一体化开发",弱项是"跨团队跨语言的接口契约"。几百兆的OpenAPI规范文档在金融、政企项目里依然有价值,那是另一个赛道。
3. 从零搭建t3code:环境准备、Schema设计与认证集成
3.1 create-t3-app:三十秒把项目骨架搭出来
t3code是我用官方脚手架建的,命令很简单:
npx create-t3-app@latest t3code过程中会有几个交互式选项。针对t3code场景我的建议是:
- 使用TypeScript:必须
- 使用Next.js App Router:选
- 使用tRPC:选
- 使用Prisma:选
- 使用NextAuth.js:选
- 使用Tailwind CSS:选
- 初始化Git:选
还有一个包管理器选择的选项,我用的是pnpm。原因是pnpm的依赖管理更干净,尤其在Next.js + tRPC这种依赖树比较深的项目里,能避免很多"幽灵依赖"问题。
3.2 项目目录结构的核心解读
脚手架生成完的目录,重点看这几个地方:
t3code/ ├── prisma/ │ └── schema.prisma # 数据模型定义 ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ └── auth/ │ │ │ └── [...nextauth]/ │ │ │ └── route.ts # NextAuth回调 │ │ └── _components/ # 客户端组件 │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts # 根Router,汇聚所有子路由 │ │ │ └── routers/ # 各业务子路由 │ │ ├── auth.ts # NextAuth配置 │ │ └── db.ts # Prisma客户端实例 │ ├── trpc/ │ │ └── server.ts # tRPC服务端工具函数 │ └── env.js # 环境变量校验这套结构最大的意义是把"服务端专属"和"客户端共享"的代码物理隔离。server/api/routers下面的代码永远不会被打进浏览器端bundle,因为tRPC的调用边界在这里是明确的。这比老式Next.js Pages Router里混写getServerSideProps和前端业务代码要清晰得多。
3.3 数据建模:任务管理域的三个核心模型
t3code的业务模型不复杂,但关系设计有讲究。我直接在prisma/schema.prisma里定义:
model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] projects ProjectMember[] tasks Task[] } model Project { id String @id @default(cuid()) name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt ownerId String owner User @relation(fields: [ownerId], references: [id]) members ProjectMember[] tasks Task[] } model Task { id String @id @default(cuid()) title String status TaskStatus @default(TODO) priority TaskPriority @default(MEDIUM) projectId String project Project @relation(fields: [projectId], references: [id]) assigneeId String? assignee User? @relation(fields: [assigneeId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt dueDate DateTime? } model ProjectMember { userId String projectId String role MemberRole @default(MEMBER) user User @relation(fields: [userId], references: [id]) project Project @relation(fields: [projectId], references: [id]) @@id([userId, projectId]) }几个决策点我展开说一下:
- ProjectMember用联合主键:
@@id([userId, projectId]),避免额外生成一个自增ID。多对多关系表本来就不需要自己的独立ID。 - Task的assignee是可选外键:任务不一定马上指派负责人,允许为空更符合实际。
- 枚举类型TaskStatus:
TODO / IN_PROGRESS / DONE / CANCELLED,枚举在Prisma里是真实数据库枚举,查询和过滤都走数据库层,性能没问题。 - cuid()而不是autoincrement():这个我重点说下。任务管理项目大概率以后要做分布式同步或离线功能,字符串ID可以避免整数ID的合并冲突。而且cuid在URL里不会暴露数据量,隐私性好一点。
定义完执行一次迁移:
npx prisma migrate dev --name init如果数据库还没准备好,可以用Docker起一个PostgreSQL实例,或者直接用Vercel Postgres / Neon 的免费云库。本地开发建议Docker,曾经踩过一次Neon实例休眠导致连接慢的问题,本地开发体验被拖累不少。
3.4 NextAuth集成:为什么不用手写JWT
认证部分我选了NextAuth.js。刚开始我也纠结过要不要自己写JWT签发逻辑,毕竟任务管理这种场景登录逻辑很简单。但仔细想了下,t3code的定位是"把T3 Stack的完整能力跑通",NextAuth同样也是T3官方生态的一部分,选它更顺。
我的配置是这样的:
// src/server/auth.ts import { getServerSession } from "next-auth"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { db } from "./db"; export const authOptions = { adapter: PrismaAdapter(db), providers: [ // 实际开发中这里会配 CredentialsProvider 或 OAuth ], session: { strategy: "jwt", }, callbacks: { session({ session, token }) { if (session.user) { session.user.id = token.sub; } return session; }, }, }; export const getServerAuthSession = () => getServerSession(authOptions);这里有个非常关键的坑要提醒一下:PrismaAdapter和JWT session策略不能同时共用一些数据库字段。早期版本里NextAuth的JWT策略会自动在User表里加一些额外字段,而PrismaAdapter期望的是完整的Account/Session表。如果你和我一样选择了strategy: "jwt",那么User模型里的accounts、sessions关系其实不会用到,但Prisma schema里必须保留这些关系定义,否则PrismaAdapter启动会报错。
还有登录页的登录方式选择。t3code我最终只用了邮箱+密码的Credentials Provider。简单可靠,而且不依赖第三方平台。但Credentials Provider默认不持久化"邮箱验证"状态,所以schema里的emailVerified字段基本是空着的。如果以后要做"注册邮箱验证"功能,还是得切到OAuth或补一个验证码逻辑。
3.5 环境变量:开发第一天就踩过的坑
脚手架会生成一个.env.example,直接把内容复制成.env再填值。t3code最少需要这几个:
DATABASE_URL="postgresql://user:password@localhost:5432/t3code" NEXTAUTH_SECRET="此处生成一个随机字符串" NEXTAUTH_URL="http://localhost:3000"NEXTAUTH_SECRET不能随便填,必须是一个足够随机的字符串。我用这个命令生成:
openssl rand -base64 32生产环境务必改成环境变量注入,别把秘钥写在Git提交里。这个纪律性问题在t3code开发期间帮我减少了很多麻烦。
4. 用tRPC打通前后端:类型安全的CRUD完整实现
4.1 Router、Procedure、Context:tRPC的三个基本概念
tRPC里每个概念都对应一个实际事物。Router是一组接口的集合,Procedure是接口本身(可以理解为"可调用的过程"),Context是每个请求进来时携带的上下文对象。
在t3code的src/server/api/root.ts里,根Router像这样:
import { createTRPCRouter } from "~/server/api/trpc"; import { projectRouter } from "./routers/project"; import { taskRouter } from "./routers/task"; import { authRouter } from "./routers/auth"; export const appRouter = createTRPCRouter({ project: projectRouter, task: taskRouter, auth: authRouter, }); export type AppRouter = typeof appRouter;前端调用时是api.project.getAll()、api.task.create()这样的链式命名空间。模块归属感清晰,团队协作时不容易撞名。
Context在t3code主要是放两样东西:Prisma客户端和会话信息:
// src/trpc/server.ts export const createTRPCContext = async (opts) => { const session = await getServerAuthSession(); return { db, session, ...opts, }; };每个Procedure都可以通过ctx访问当前登录用户。这个设计让权限检查变得异常简洁。
4.2 查询和变更:以Task为例完整代码
任务列表的查询我用了受保护的procedure,只有登录用户才能调用:
// src/server/api/routers/task.ts import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; import { TaskStatus, TaskPriority } from "@prisma/client"; export const taskRouter = createTRPCRouter({ // 获取当前用户负责的任务 getMyTasks: protectedProcedure .query(async ({ ctx }) => { return ctx.db.task.findMany({ where: { OR: [ { assigneeId: ctx.session.user.id }, { project: { members: { some: { userId: ctx.session.user.id } } } }, ], }, include: { project: { select: { id: true, name: true } }, assignee: { select: { id: true, name: true, image: true } }, }, orderBy: { createdAt: "desc" }, }); }), // 创建任务 create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), description: z.string().optional(), projectId: z.string().cuid(), assigneeId: z.string().cuid().optional(), status: z.nativeEnum(TaskStatus).default(TaskStatus.TODO), priority: z.nativeEnum(TaskPriority).default(TaskPriority.MEDIUM), dueDate: z.date().optional(), }) ) .mutation(async ({ ctx, input }) => { // 额外校验:确保用户确实是该项目的成员 const membership = await ctx.db.projectMember.findUnique({ where: { userId_projectId: { userId: ctx.session.user.id, projectId: input.projectId, }, }, }); if (!membership) { throw new Error("你不是该项目的成员,无法创建任务"); } return ctx.db.task.create({ data: { ...input, }, }); }), });这里可以看到tRPC的一个显著优点:输入校验用Zod在服务端做,前端获得推导出的输入类型。z.nativeEnum(TaskStatus)让状态值只能是数据库枚举里的合法值,不是任何字符串都放行。
前端对应的调用长这样:
// src/app/tasks/new/page.tsx (客户端组件) "use client"; import { api } from "~/trpc/react"; export function CreateTaskForm({ projectId }: { projectId: string }) { const utils = api.useUtils(); const createTask = api.task.create.useMutation({ onSuccess: () => { utils.task.getMyTasks.invalidate(); }, }); return ( <form onSubmit={(e) => { e.preventDefault(); const formData = new FormData(e.currentTarget); createTask.mutate({ title: String(formData.get("title")), projectId, assigneeId: String(formData.get("assigneeId")), }); }} > {/* 表单字段... */} </form> ); }useMutation是tRPC基于React Query封装的Hook。调用mutate自动处理loading、error、success状态。invalidate()让缓存失效并重新拉取,前端UI就完成了实时刷新。
4.3 服务端调用:避免多余的异步请求
t3code里有一个场景是"服务端组件直接拿数据渲染页面"。比如项目详情页,在服务端组件里直接调用tRPC,不需要先渲染页面再发一次fetch:
// src/app/projects/[id]/page.tsx import { api } from "~/trpc/server"; export default async function ProjectPage({ params }: { params: { id: string } }) { const project = await api.project.getById({ id: params.id }); if (!project) { return notFound(); } return ( <main> <h1>{project.name}</h1> {/* 渲染任务列表 */} </main> ); }这里用到的~/trpc/server是在服务端创建的调用代理,它绕过网络请求直接进入Router执行。T3 Stack把这种模式叫"server caller"。好处是:数据获取和页面渲染在同一层,天然SSR,用户看到首屏时数据已经完全就绪。
4.4 错误处理:用TRPCError而不是throw new Error
我一开始偷懒直接在mutation里throw new Error("..."),后来发现前端拿到的错误信息是割裂的。t3code现在统一改用TRPCError:
import { TRPCError } from "@trpc/server"; if (!membership) { throw new TRPCError({ code: "FORBIDDEN", message: "你不是该项目的成员", }); }TRPCError的code会被tRPC自动映射成HTTP状态码,同时保留自定义message。前端在.catch回调里能拿到结构化的错误对象,不用自己去解析网络错误体。统一错误处理这个习惯,建议所有用tRPC的团队都一上来就养成。
5. 部署与运维:从本地调试到线上环境的完整链路
5.1 环境变量的生产环境管理
t3code第一版部署到Vercel之前,我在环境变量上栽了个跟头:本地.env里DATABASE_URL用的是本地PostgreSQL,线上环境忘了配,结果构建能成功,但一访问数据库相关接口就500。
Vercel的环境变量配置在Project Settings > Environment Variables里。建议按环境分别配置:
| 变量 | 开发环境 | 生产环境 |
|---|---|---|
| DATABASE_URL | 本地PostgreSQL | 云数据库连接串 |
| NEXTAUTH_URL | http://localhost:3000 | https://你的域名 |
| NEXTAUTH_SECRET | 任意随机串 | 独立生成的强随机秘钥 |
其中NEXTAUTH_SECRET生产环境必须和开发环境不同,否则一旦本地代码泄露,生产的会话就有可能被伪造。用openssl重新生成一次,放到生产环境变量里。
5.2 Vercel部署:Serverless的坑与应对
Vercel部署Next.js是天然适配的,t3code推到GitHub后连上Vercel,push主分支就自动构建发布。但Serverless环境有几个现实问题:
- 函数冷启动:数据库连接是重资源,每次冷启动都重新建连会拖慢响应。解决方法是全局复用Prisma客户端。脚手架生成的
src/server/db.ts已经做了全局单例,生产环境通常没问题。 - 连接数限制:PostgreSQL云数据库免费档一般限制并发连接数,Serverless环境下多个实例并发连接容易打满连接池。应对方式是改用PgBouncer兼容的连接串,或者把Prisma的
connection_limit调低。t3code在生产上最终把连接数限制设成了5:
// src/server/db.ts const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }; export const db = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], });线上日志我只保留error级别,开发环境打开query日志方便调试。这个区分帮我提前发现了几次N+1查询问题。
5.3 Docker部署:当你不满足于Vercel的时候
Vercel很方便,但如果你有自己的服务器、或者客户要求私有化部署,就得用Docker。t3code也尝试了Docker方案,多阶段构建的Dockerfile核心是这两步:
FROM node:20-alpine AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN apk add --no-cache libc6-compat && pnpm install --frozen-lockfile FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npx prisma generate && pnpm build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules RUN chmod +x /app/node_modules/.bin/next CMD ["node_modules/.bin/next", "start"]关键点有两个:一是二进制阶段不用alpine的Prisma引擎,会出现glibc兼容问题,必须加libc6-compat;二是Dockerfile里要显式执行prisma generate,否则Prisma客户端不会包含在构建产物里,线上运行时会报"PrismaClient Initialization Error"。
5.4 数据库迁移与种子数据
本地开发和生产环境的数据库结构同步,要用prisma migrate deploy而不是migrate dev。migrate dev含重置数据库和生成迁移文件的功能,deploy只执行没跑过的迁移,不重置数据:
在CI/CD里加一步:
npx prisma migrate deploy种子数据方面,我在prisma/seed.ts里写了一个脚本,创建测试用户和几个示例项目,方便新环境快速联调。用tsx prisma/seed.ts运行即可。如果你不想写TS脚本,也可以用Prisma Studio手动造数据,但项目一多人就乱了,建议还是脚本化。
6. t3code开发中踩过的坑:版本冲突、类型推导与边界情况
6.1 Next.js和tRPC的版本兼容:升级容易踩雷
t3code最早是用Next.js 14.1 + tRPC 10.x搭的。后来Next.js 15发布,社区开始大规模迁移,我也试着升级了一下,结果直接遇到编译报错:next-auth依赖的react版本和Next.js 15要求的React 19不兼容。
排查链路过一遍:先看错误信息定位到next-auth的包声明,再用npm ls react查版本树,最后在package.json里把next-auth锁定到一个兼容版本,同时react回退到18.x。这类问题在新脚手架下其实已经少了很多,但如果你和老项目一样是手动集成,一定要先做版本矩阵检查再动手升级。
6.2 Prisma Schema的枚举变更:生成容易忘了迁移
有一次我改TaskStatus枚举,加了一个ARCHIVED值,本地测试没问题,但部署到生产环境跑起来报错说找不到ARCHIVED。原因很简单:prisma migrate dev只在本地跑了,生产的数据库没执行对应迁移。生产环境又执行了一遍prisma migrate deploy,瞬间解决。
这类坑的教训是:修改Schema之后,必须同时提交迁移文件,并确保线上执行的迁移顺序和本地一致。可以在CI里增加一个"检查迁移文件是否已生成"的步骤,防漏。
6.3 tRPC的类型推导边界:不要在客户端用any
测试t3code的时候,我有一处因为懒,在客户端组件里把api.task.getMyTasks的返回结果用as any处理了。结果整个组件从此不再享受类型提示,改字段也不报错,直到联调时才发现字段穿透了。
忍耐一时,以后全是坑。tRPC类型安全的唯一前提是你给它完整的类型链路。一旦任何一个环节用了any或显式断言,类型推导就像水管破了,后面的类型全部失效。如果非要用局部类型覆盖,也请使用satisfies而不是as。
6.4 冷启动查询超时:连接池的hidden cost
前面提到生产环境连接数限制,实际遇到的现象是:用户第一次访问时页面转圈5秒以上,之后恢复正常。排查发现Serverless新实例冷启动时,Prisma要建立N个连接尝试,超过数据库限制的并发就排队,排队就超时。
修复方案是两层:数据库侧把connection_limit调低到和实例数匹配,代码侧为Prisma客户端加连接超时配置:
new PrismaClient({ log: ..., datasourceUrl: process.env.DATABASE_URL, // 缩短连接等待 });6.5 实用建议:经验谈
踩了一圈坑,t3code沉淀下来的几条日常规范,值得说说:
- 每个Router都必须做权限边界测试。tRPC的protectedProcedure只保证了"登录用户可访问",但"这个登录用户是否有权操作这条数据"还是得在业务层显式判断。
- Redis可缓存但不强制。任务管理应用的数据实时性要求高,用了缓存反而容易返回脏数据。t3code直到当前版本都没上Redis,查询慢的接口直接优化Prisma查询和索引。
- 日志是最便宜的可观测性。Vercel提供的日志不太好用,但至少把错误日志打印全了。线上出了问题,第一时间看日志比猜快十倍。
写在最后的实战体会
t3code这个项目从起名、搭骨架、写业务代码到部署上线,完整走完一遍T3 Stack之后,我的感受是:这套技术栈最大的价值不是提升编码速度,而是消灭了整个类别的错误。类型链路的完整闭环让"前后端字段不对齐""改接口忘了改文档""参数传错但不报错"这些传统痛点几乎全部消失。作为个人项目,它让我能把更多精力放在业务逻辑和交互体验上,而不是调试那些本该让编译器帮我抓出来的低级问题。
如果你正准备上手T3 Stack,我建议别从"看文档"开始,直接建一个空项目,把今天文章里的三个模型(带用户关系的业务实体、状态枚举、成员多对多)照着敲一遍,再跑通一次部署。等你把tRPC的调用链路和Prisma的迁移流程摸顺了,你会回来感谢这个技术选型的。
最后再分享一个小技巧:如果你也打算把t3code推进到生产,记得在package.json里加一条postinstall脚本,让它自动执行prisma generate。这个脚本能让你在CI/CD流程里少踩一个非常隐蔽的坑——很多时候不是代码错了,而是Prisma客户端没有重新生成。