最近把一个从零到一的全栈项目收尾了项目代号就叫t3code。这个项目本身不算复杂但把它完整跑下来的过程中我对整个 T3 技术栈的理解比之前深了好几个层次——尤其是类型安全到底能给开发流程省多少事、tRPC 和 Prisma 在真实业务里要怎么配合、以及部署到生产环境时那些文档里不会明说但早晚会踩的坑。这篇文章就把整个思路、关键选型、实操过程、踩坑记录全部摊开来说希望给准备用 Next.js TypeScript tRPC Prisma 这套组合做点东西的朋友提供一份能直接照着走的参考。为什么叫 t3code其实就是 T3 Stack 的代号核心想解决的是很多独立开发者和小团队反复遇到的问题前后端类型不通、接口文档永远滞后、数据库设计跟业务对不上、部署环节东拼西凑。t3code 把这几件事用一套严格类型约束的方式串在一起做做一个中小型全栈应用从数据库到页面渲染全程类型打通改一个字段编辑器里立刻全线标红这种体验用惯了真的回不去。适合谁来参考这篇文章如果你已经会 React 和 JavaScript 基础想认真做一个落地可用的全栈项目或者你是一名技术负责人正在评估团队要不要引入 tRPC 或 T3 这套组合再或者你对后端不可知论这套理念感兴趣但没实操过——这篇文章都值得你花 10 分钟过一遍。下面的内容全部基于我实际跑通t3code项目的过程包括每一步的命令、每个选择的理由、每个坑的排查方式不是抄文档。1. 整体设计与技术选型的底层思路1.1 为什么是 T3 而不是“传统前后端分离”最开始我其实犹豫过要不要用 Vue Spring Boot或者 React Go前后端一拆各干各的。但 t3code 这个项目定位是“一个人能维护的中型业务系统”这种规模下传统前后端分离的协作优势根本发挥不出来反而要付出双倍的沟通成本。T3 的思路核心是“后端不可知论”——不强制你用什么后端但只要你用了 TypeScript就能做到前端直接调用后端函数类型全自动推导不需要手写 API 层不需要根据 OpenAPI 生成客户端更不需要在联调的时候对着接口文档猜字段。t3code 里有一套用户管理、项目档案、日志记录的核心业务如果按传统方式光接口定义、DTO 转换、前端类型声明这三层我预估要多写至少两天的样板代码。用 tRPC 之后这些全都不存在了。另一个关键理由是类型安全的端到端贯通。t3code 的数据模型改动很频繁早期几乎每天调整 Prisma schema。传统方案里改一个后端字段要同步改接口文档、改前端类型、改联调环境任何一个环节漏掉线上就会出隐蔽的运行时错误。但 T3 体系内改掉 Prisma schema运行prisma generatetRPC 的 router 类型会立刻变化前端调用处在编辑器里直接报错问题在编译期就被拦截了。1.2 技术栈拆解每个组件在 t3code 里扮演什么角色t3code 最终采用的组合是技术在项目里的职责Next.js 14 (App Router)页面渲染、路由、服务端组件、API 托管TypeScript全链路类型系统的基础tRPC替代 REST API提供端到端类型安全调用PrismaORM负责数据库模型与迁移Tailwind CSS样式方案快速搭建 UINextAuth身份认证与会话管理这套组合不是随便拼的。Next.js 负责把服务端和客户端整合在一个框架里tRPC 藏在 Next.js 的 API 路由后面本身不需要额外服务器Prisma 管数据库schema 文件同时是类型来源和迁移依据NextAuth 提供 OAuth 和 Session和 tRPC 的 context 对接起来非常顺。每个组件边界清晰但合在一起又像一个整体这正是 T3 想要的效果。我见过有人问“为什么不用 GraphQL”GraphQL 解决的是“前端需要灵活查询多个资源”的问题它引入了 schema、resolver、缓存、codegen 一整层复杂度。t3code 这种中小型系统需求的其实是函数式远程调用 类型推导tRPC 比 GraphQL 轻得多不需要 codegen类型直接从 router 定义推导心智负担小。选型核心逻辑就是一句话能用类型系统硬约束解决的问题就不要引入运行时做额外开销。1.3 目录结构与工程组织t3code 的目录组织完全遵循 T3 官方脚手架但我在实际开发中做了几处调整。主体结构如下t3code/ ├── prisma/ │ ├── schema.prisma │ └── migrations/ ├── src/ │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ ├── root.ts │ │ │ └── trpc.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── app/ │ │ ├── api/trpc/[trpc]/ │ │ └── (routes)/ │ ├── trpc/ │ │ ├── server.ts │ │ └── client.ts │ └── styles/ ├── .env └── package.json几个容易忽视的点src/server/api/trpc.ts是 tRPC 的初始化文件专门负责创建 tRPC 实例、注入 auth 和 db 上下文不要跟业务 router 混在一起src/trpc/server.ts是服务端调用 tRPC 的封装在 Server Component 里直接调用查询时要用它src/trpc/client.ts给客户端提供useQuery等 hooks。这三层是 tRPC 在 Next.js 里能跑通的关键很多人上来就把它们合并了后面加中间件或者按路由拆分时就会很痛苦。2. 核心细节解析与实操要点2.1 Prisma Schema 设计业务先行还是类型先行t3code 有三张核心表用户User、项目Project、操作日志AuditLog。最开始我把它们当成三个独立模型写后来在实际写 router 时发现关联查询特别别扭。如果你也在做类似项目建议先想清楚业务聚合边界再落 schema。我在 t3code 上踩过一个实在的坑一开始给 Project 表加了status字段直接用 String 存储值就是draft、active、archived。开发中没问题但后来有个路由要根据状态过滤统计一个手滑把actived写进去了前端类型完全检查不出来因为它是字符串。排查花了我四十分钟。后来改成 Prisma 的 enumenum ProjectStatus { DRAFT ACTIVE ARCHIVED } model Project { id String id default(cuid()) name String status ProjectStatus default(DRAFT) ownerId String owner User relation(fields: [ownerId], references: [id]) createdAt DateTime default(now()) }改动之后任何非法状态在编译期就被 TypeScript 拒了这类低级错误彻底消失。这是个值得记住的原则能交给类型系统保证的就不要依赖人的纪律。权限方面我的选择是user和admin两种角色用 Prisma enum 加在 User 模型上配合 NextAuth 的 session 在 tRPC middleware 里做鉴权。这块后面细说。2.2 tRPC Router 组织按业务域拆不按操作拆t3code 早期犯过一个组织错误把 createProject、updateProject、deleteProject、listProjects 全塞进一个projectRouter加上 userRouter 和 auditRouterrouter 文件越来越胖。其实 tRPC 允许嵌套 router更合理的做法是按业务域拆分子路由再用createCaller或router合并。实际上我在 mid 阶段按“资源 动作”重构成这样的模式// 文件 src/server/api/routers/projectRouter.ts import { createTRPCRouter, protectedProcedure } from ../trpc; import { z } from zod; export const projectRouter createTRPCRouter({ list: protectedProcedure .input(z.object({ status: z.enum([DRAFT, ACTIVE, ARCHIVED]).optional() })) .query(async ({ ctx, input }) { return ctx.db.project.findMany({ where: input.status ? { status: input.status } : {}, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input(z.object({ name: z.string().min(2), status: z.enum([DRAFT, ACTIVE, ARCHIVED]).default(DRAFT) })) .mutation(async ({ ctx, input }) { return ctx.db.project.create({ data: { name: input.name, status: input.status, ownerId: ctx.session.user.id }, }); }), });Zod 的 schema 直接作为 tRPC 的 input 校验层这个设计很妙——校验规则写一次前端和后端类型同步生效。我在项目里几乎把所有 mutation 的 input 都做了严格 zod 约束比如name最小长度、email格式、可选字段的 default 值。这样带来的间接好处是前端表单校验和后端校验用同一套规则不会出现前端说“填错了”但后端放行或者反过来。另外提醒一句protectedProcedure这种包装方式非常实用我写了一个全局的 auth 中间件用protectedProcedure包裹所有需要登录才能访问的接口没登录直接抛 UNAUTHORIZED 错误。不需要登录的查询比如公开列表页再用publicProcedure。这个分层很清晰也方便以后加角色权限。2.3 NextAuth 与 tRPC 的会话打通tRPC 的 context 是每次请求时创建的标准做法是把 NextAuth 的 session 塞进 tRPC context。t3code 里我在src/server/api/trpc.ts做了这样的处理export const createTRPCContext async (opts: { headers: Headers }) { const session await getServerSession(authOptions); return { db, session, ...opts, }; };然后在 procedure 初始化时通过use.ctx.session?.user拿当前用户。这里有三个容易踩的坑第一getServerSession 必须在服务端调用不能在客户端组件里用否则拿不到 session。t3code 里客户端要获取登录状态统一通过 tRPC 暴露的sessionquery 来拉而不是直接 import NextAuth 的方法。第二NextAuth 的 session 类型默认不全。默认只包含name、email、image我在 User 模型里加了 id 和 role所以要做模块扩展才能取到declare module next-auth { interface Session { user: { id: string; role: UserRole; } DefaultSession[user]; } }不扩展的话ctx.session.user.id会直接报类型错误这也是一个“类型系统逼你正确设计”的典型例子。第三OAuth 登录后Prisma 里可能没有对应用户记录。t3code 用的是 GitHub OAuth Credentials 混合登录我在 schema 里给 User 的account、session绑定 NextAuth 默认的适配器表结构。用 NextAuth 自带适配器时注意要填auth/prisma-adapter并且跑一次prisma db push或prisma migrate dev把扩展表建出来否则登录会直接报 P2010 类错误。3. 实操过程与核心环节实现3.1 从脚手架到第一个页面完整命令记录下面这段是 t3code 实际初始化的完整命令照着跑就行。用项目代号做应用名# 使用 create-t3-app 脚手架 pnpm create t3-applatest t3code # 交互选择时我勾选了 # - Next.js (App Router) # - TypeScript # - tRPC # - Prisma # - Tailwind CSS # - NextAuth # - 数据库选 SQLite开发阶段后续生产切换 PostgreSQL选择过程中有两点建议。数据库我建议开发期先用 SQLite零配置跑起来快但 Prisma schema 里不要写死 SQLite 特有的类型尽量保持可迁移性。t3code 进入部署阶段后切换到了 PostgreSQL只改了DATABASE_URL和provider重跑一次 migrate迁移文件自动生成几乎没有额外成本。脚手架装完后需要先生成基础表。由于勾选了 NextAuthprisma schema 里已经带了Account、Session等模型直接执行pnpm prisma migrate dev --name init pnpm prisma generate随后启动开发服务器pnpm dev打开http://localhost:3000确认首页跑通后整个骨架就活了。t3code 的第一个功能我做了项目列表页它验证了从数据库到 UI 的整条链先在 prisma schema 建表、再写 tRPC router、然后在 Server Component 里用api.project.list()直接取数据全程没有写一个 fetch。3.2 数据库连接与 Prisma Client 初始化t3code 中src/server/db.ts直接使用脚手架生成的代码内部是PrismaClient单例。这背后有个值得一提的细节——Next.js 在开发热重载时如果每次都new PrismaClient()会因为连接溢出报警所以脚手架会在globalThis上做缓存避免开发模式下重复实例化import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma: PrismaClient | undefined }; export const db globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV ! production) { globalForPrisma.prisma db; }这个模式不是可选项而是必选项。我在 t3code 开发中有一阵子频繁保存文件日志里经常出现Error: P1001连接失败排查半天发现就是因为这个实例被反复创建。如果你用 Prisma 配合 Next.js直接把上面这段照抄不要自己 new。3.3 用 tRPC 打通类型安全的数据流tRPC 的核心运行机制是一个基于 Next.js API 路由的 endpoint/api/trpc/*。所有 procedure 请求都打到这个 endpoint 上通过 URL 里的参数区分是哪个 router、哪个 procedure以及是 query 还是 mutation。它不需要单独的服务器进程、不需要部署额外的 gateway看着像“本地调用”实际走的是 HTTP——这个抽象做得很干净。在 t3code 中前端组件用一个自定义 hook 调用后端逻辑// 客户端组件内 import { api } from ~/trpc/react; function ProjectCreateButton() { const utils api.useUtils(); const createProject api.project.create.useMutation({ onSuccess: async () { await utils.project.list.invalidate(); }, }); const onSubmit (name: string) { createProject.mutate({ name, status: ACTIVE }); }; }onSuccess里调用invalidate()让相关 query 自动重新拉取这是 tRPC 里我最喜欢的功能之一——它替代了传统方案里“更新完手动刷新列表”或者“事件通知”的繁琐写法。数据变更后列表、统计、详情页可以统一失效下一渲染周期自动拿新数据。需要注意的是服务端组件里不能用api.useQuery。在 Server Component 里要调用 tRPC需要用src/trpc/server.ts里导出服务端 callerimport { api } from ~/trpc/server; export default async function ProjectListPage() { const projects await api.project.list({ status: ACTIVE }); // 直接渲染 projects }这个 caller 走的是函数调用路径不经过网络但类型和客户端已经保持一致了。t3code 里绝大部分列表页用了这种方式首屏渲染直接出数据不需要 loading 态。需要交互的按钮区域再单独拆客户端组件用 hooks 处理。3.4 Tailwind CSS 在 t3code 里的实战运用样式这块我没有引入组件库直接 Tailwind 手写。t3code 的核心页面包括登录页、项目列表、项目详情、用户管理全部用 Tailwind 类完成。这里给新手三个我实际用出的心得第一先把设计令牌定好再动手写业务页。在tailwind.config.ts里扩展自定义颜色和字体这样页面之间不会出现同一个语义颜色在不同页面深浅不一的问题。t3code 定义了brand和surface两组色系统一了按钮、卡片、背景的视觉层级。第二页面结构用 flex/grid 组合不要一味用绝对定位。列表页用grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4一行搞定响应式搭配rounded-lg border p-4做卡片可读性和视觉密度比传统 CSS 手写类高很多。第三复杂交互组件用 Tailwind Headless UI。T3 脚手架可选装headlessuit3code 的弹窗、下拉菜单用的都是它。它提供无样式交互逻辑组件Tailwind 负责全部视觉组合起来非常有生产力完全不需要引入整套 antd 或 MUI。4. 常见问题与排查技巧实录4.1 类型没同步”找不到 xxx Router 的调用方法”这是使用 tRPC 后最多人遇到的第一道坎。现象是前端写api.project.list()编辑器报错说Property list does not exist on type TRPCClientErrorLike之类或者干脆提示 router 里没有这个方法。原因几乎都是修改 router 后没有重启 Dev Server导致 tRPC 类型更新没被 Next.js 的热重载拾取。tRPC 的类型是从实际 router 函数推导的而 Next.js 的 fast refresh 对这种跨模块的复杂类型推导支持并不完美。我实测下来最稳妥的做法是改完 router手动按CtrlR强制刷新页面或者干脆重启 dev 进程在 CI 里则确保跑一遍tsc --noEmit让类型检查在提交前兜底。如果你遇到类型报警但不知道错在哪个文件直接看 TypeScript 报错信息中的引用路径多半指向src/trpc/react.ts中的类型接口。顺着链路往上找就能定位到是哪个 router 导出出了问题。4.2 Prisma 迁移冲突与数据库残留t3code 早期改动 schema 频繁有一阵子连续执行了几次prisma migrate dev结果迁移列表出现冲突状态显示缺少某几个迁移文件。这通常是因为多人协作或者本地仓库拉取时迁移目录不同步。我的处理方式是先备份一次当前库数据本地开发阶段直接用 SQLite 文件复制即可。执行prisma migrate reset --force彻底重置本地库让所有迁移从头执行。如果仍不一致检查prisma/migrations/migration_lock.toml确保 provider 一致再删掉本地多余迁移目录重跑migrate dev --name sync。生产环境切数据库时绝对不要这样做。我在上线的 PostgreSQL 实例上恢复数据后刻意用了prisma migrate deploy而不是migrate dev。deploy只应用迁移文件、不生成新迁移适合生产。这条纪律建议牢记本地随便 reset生产必须 deploy。4.3 NextAuth 回调地址与 Session 失效问题在本地开发时 NextAuth 的 GitHub OAuth 回调地址要配成http://localhost:3000/api/auth/callback/github这个在 GitHub OAuth App 设置里。部署后要把生产域名也加上并且.env里的NEXTAUTH_URL要同步更新。我有个朋友部署上线后登录一直 500排查了两小时最后发现是环境变量里的NEXTAUTH_URL没改还在指 localhost反向代理层转发过去的回调地址对不上。另一个 Session 问题是登录后长时间开着页面点击时突然 401。原因是 JWT session 默认有效期到了。t3code 里我根据不同场景调整了策略——管理后台用数据库 session有效期拉到 7 天接口请求则继续用短时效的 JWT。具体在authOptions里配置session: { strategy: database, maxAge: 60 * 60 * 24 * 7, }调整后体验好很多除非用户主动登出否则不频繁被踢。注意数据库 session 策略需要在 schema 里有 Session 模型前面提到用 PrismaAdapter 时已经建过了所以切过去零额外成本。4.4 部署后的性能与构建坑t3code 部署到 Vercel 后有个页面首屏始终很慢打开浏览器的 Network 面板发现某个列表接口返回了全量数据——虽然只有几百条但每次渲染都重复序列化。后来我给列表 query 加了分页参数在前端用infiniteQuery做“加载更多”。这个改动把首屏体积降了一半以上。值得强调的另一个坑是t3code 里任何客户端组件都不要直接 importserver-only依赖。tRPC 的服务端 caller 只能在服务端组件里用客户端组件只能走api.*.useQueryhooks。我一度在共享组件里误用了服务端调用构建时报错Youre importing a component that needs server-only排查后把组件拆成了两半纯 UI 部分放共享数据部分由父组件通过 props 传入。这类问题在团队协作初期很容易出现脚手架生成代码里其实有区分但你写多了自己的文件后要格外小心边界。4.5 常见问题速查表把 t3code 开发中积累的典型报错整理成一个速查表遇到类似问题可以直接对号入座报错或现象常见原因应对办法tRPC 方法在编辑器里不存在修改 router 后热重载未刷新类型重启 dev server 或手动强刷P1001 数据库连接超限PrismaClient 重复实例化使用 globalThis 单例模式NextAuth 登录后立刻 401NEXTAUTH_URL 未指向正确域名检查 .env 与部署环境变量P2010 找不到关系表未按 PrismaAdapter 建表运行 migrate 生成 Account/Session 表部署构建报 server-only 错误客户端组件误用服务端模块拆组件数据通过 props 传入Session 类型缺少 id/role未扩展 next-auth 模块声明增加 declare module 补全类型列表接口返回慢、页面卡全量数据一次性返回改分页或 infiniteQuery生产数据库迁移异常使用了 migrate dev改用 migrate deploy4.6 几个不写进文档但非常实用的细节第一日志要有唯一请求标识。tRPC 的中间件里可以给 context 注入一个 requestId然后统一打进日志排查看链路时非常管用。t3code 我用的是crypto.randomUUID()一行代码后期排障效率翻倍。第二环境变量千万别只写一套。.env里只放本地部署平台单独配置生产变量不要把线上 DATABASE_URL 写在仓库里。t3code 在 git 里保留了.env.example模板DATABASE_URL、GITHUB_ID、GITHUB_SECRET、NEXTAUTH_SECRET全部用占位符新环境克隆下来照着填就能跑。第三定期清理 tRPC 的 catch 分支。我早期在每个 mutation 里都写了 try-catch后来发现其实没必要——tRPC 的异常会在客户端以error.message暴露统一在 hooks 层面处理即可写成千篇一律的 try-catch 反而掩盖了错误栈。把边界错误处理收敛到一个公共层代码会干净很多。5. t3code 后续扩展与个人实操体会5.1 这个架构往后还能加什么如果你要把 t3code 这类 T3 结构往前推一步我建议按下面这条路线走顺序很重要。第一步把 Prisma schema 里的数据模型覆盖到完整业务域第二步利用 tRPC 的 middleware 加文件上传、订阅通知这类横切能力第三步把 NextAuth 升级成多角色权限体系。每一步都不需要动整体架构T3 的边界设计让横向扩展非常平稳。我在 t3code 里其实已经加了两块不属于脚手架默认内容的东西一个是基于 Prisma 的审计日志表另一个是用serverOnly标记的定时任务模块。两个都顺利接上了没有造成任何架构上的倾斜感这也是我最终信任这套组合的原因——它不锁死你的扩展方式只是在核心链路上做极致的类型约束。5.2 关于类型安全的最终心得最后说点个人体会。t3code 这个项目给我的最大经验不是某种具体技术而是“类型系统不是束缚是脚手架”这句话的真实含义。传统开发里你觉得类型声明是多写的工作但在 T3 里类型是自动生成、自动传播的你不需要费心维护它却能在你犯错时第一时间拦住你。t3code 整个开发周期里我几乎没花时间“修复线上类型不匹配导致的数据问题”这在以前是不可想象的。严格来说T3 并不适合所有人——如果你追求极致的后端性能或者需要大量非 TypeScript 生态的中间件那它不一定是最优选。但如果你和我一样想用一套人能完全理解、类型全程贯通、一个人也能快速交付的现代全栈方案t3code 这套组合值得你认真投入。它在中小型业务场景下是真的能让人只关注业务本身而不是跟框架细节搏斗。