做全栈项目这几年,我最大的体会是:能省心的架构真的不多。前端框架选型、后端接口定义、数据库表结构、类型安全,每个环节都藏着坑。直到我在社区里看到T3 Stack相关的讨论,又在这基础上接触到围绕它沉淀的代码集合项目“t3code”,才感觉找到了一个真正能闭眼开干的组合。
这篇就来详细拆一拆t3code这个项目。它本质上是一个围绕 T3 Stack(Next.js + tRPC + Prisma + Tailwind CSS,外加 NextAuth.js)构建的代码模板和最佳实践合集,解决的问题非常具体:全栈项目启动慢、前后端类型不统一、API 文档维护成本高、状态管理越写越乱。如果你正准备上手一个中大型 Next.js 全栈项目,或者想理解“类型安全到底能给工程化带来什么”,这篇内容基本能覆盖你从入门到实战的绝大部分需求。
1. 内容整体设计与思路拆解
1.1 T3 Stack 到底是什么,t3code 又是什么
T3 Stack 不是一个框架,它是一套组合拳:Next.js 负责页面和接口,tRPC 负责前后端接口调用,Prisma 负责数据库操作,Tailwind CSS 负责样式,NextAuth.js 负责认证。这个组合的核心就一句话:让 TypeScript 类型从数据库一路贯通到浏览器组件。
t3code 这个项目,可以理解成把这个组合固化下来的“最佳实践模板库”。它不是单一文件,而是一个集合了初始化脚本、目录结构约定、完整示例代码、常见业务场景封装的开源参考实现。简单说,别人已经帮你趟过雷,把 T3 Stack 从零到一跑通的完整过程写成了一套可复用的代码。
有了它,你不需要再对着十几篇文档手动拼装,也不用纠结“tRPC 的 query 到底怎么和 Next.js 的 server component 配合”。直接基于 t3code 的骨架起步,把重心放在业务上就好。
1.2 为什么选这条技术路线,而不是传统 REST + Redux
在 t3code 出现之前,很多全栈项目的典型写法是:Next.js 写页面,后端单独起一个 Express/NestJS 服务,前后端通过 REST API 通信,前端再用 Redux/Zustand 管理服务端状态。这套方案能跑,但有几个痛点非常折磨人:
第一,类型断裂。后端定义了一个User接口,前端为了使用它,往往在types目录里再复制一份。后端改了字段名,前端编译期完全不报错,直到接口返回 undefined 或运行时抛错才能发现问题。
第二,API 文档维护成本高。REST 接口多了以后,Swagger/OpenAPI 这类工具要么手动维护,要么生成器的配置本身就很复杂。团队协作时,“这个接口到底接收什么参数、返回什么结构”全靠口口相传。
第三,服务端状态管理冗余。Redux 这类工具本质上是给前端做“本地状态”用的,但很多团队把它用在了“服务端数据缓存”上。于是每个页面都要写 action、reducer、selector,代码量和心智负担直线上升。
t3code 走的是另一条路:tRPC 让前端直接调用后端函数,并且是类型安全的函数调用。函数签名变了,前端立刻编译报错。数据缓存的活交给了 tRPC 内置的 React Query 机制,服务端状态不需要 Redux 来管。这套设计等于从源头上消灭了一整类 bug。
1.3 t3code 的目录结构和职责划分
t3code 的目录规划非常清晰,核心是这样一套约定:
src/ ├── pages/ # Next.js 页面路由 ├── server/ │ ├── api/ # tRPC 路由注册 │ ├── auth/ # NextAuth 配置 │ ├── db/ # Prisma Client 实例 │ └── trpc/ # tRPC 上下文、路由创建工具 ├── styles/ # 全局样式 └── utils/ # 公共工具函数这套结构最大的好处是:服务端代码和客户端代码有了物理边界。凡是涉及数据库、鉴权、敏感逻辑的,都收敛到server目录下。前端页面不能直接 importserver/db,只能通过 tRPC 定义的 router 去访问数据。这种约束是代码层面的规范提醒,比“不要直接查数据库”这种口头约定靠谱得多。
和传统“前端目录 + 后端目录”的结构比,t3code 的模式更贴近“泛前端”全栈的思维:代码在一个仓库里,但通过目录职责维护了清晰的边界。小项目不需要 microservices 那样过度拆分,但也不至于把所有逻辑都堆在页面组件里。
2. 核心细节解析与实操要点
2.1 tRPC:类型安全的核心引擎
tRPC 是 t3code 里最值得花时间理解的部分。它做的事情用一个类比来概括就是:前端调用后端函数,像在调用本地函数一样,甚至不需要手动写 URL。
传统 REST 调用是这样的:
const res = await fetch('/api/user/123'); const user = await res.json(); // user 在这里是 any,需要手动断言类型tRPC 的调用是这样的:
const user = trpc.user.get.useQuery({ id: 123 }); // user 的类型自动从后端 router 推断出来,完全不需要手动声明关键在于,tRPC 利用 TypeScript 的类型推断能力,把后端 router 的输入输出类型“投影”到了前端。服务端改动一个字段类型,前端编译立刻报错。这种体验一旦习惯了,就再也回不去了。
在 t3code 里,定义一个新的 API 只需要三步:在server/api/routers里写一个子 router,在server/api/root.ts里注册,然后在页面里通过trpc.xxx.yyy.useQuery使用。
2.2 Prisma:数据库层的最佳实践
Prisma 在 t3code 里的地位相当于“数据基石”。它不只是一个 ORM,更重要的是提供了数据库 schema 的可视化定义和迁移能力。
model User { id String @id @default(cuid()) name String? email String @unique createdAt DateTime @default(now()) }Prisma 的好处,用我实际踩坑的经验来说,最突出的有两点:
一是schema 即类型源。prisma generate会根据 schema 自动生成完整的 TypeScript 类型。这个类型不仅用于后端,通过 tRPC 还能贯通到前端。整个链路的数据结构,从数据库到 UI,源头只有一个地方,那就是 schema 文件。
二是迁移流程可追溯。prisma migrate dev每次改动表结构都会生成带时间戳的迁移文件。不会出现“开发环境改了表结构,生产环境忘记同步”的翻车现场。这比直接用db.push暴力同步要安全太多。
2.3 NextAuth.js:认证集成没有想象中难
认证这块,t3code 的做法是直接集成 NextAuth.js。刚开始接触这个库的时候,我踩过一个很大的坑:以为要自己写 JWT 签发和校验逻辑。实际上 NextAuth 已经把 OAuth、JWT、Credentials 等常见认证方式都封装好了。
t3code 里的认证配置放在了server/auth.ts,通过getServerSession在 tRPC 的 context 里注入当前用户信息。这样在 tRPC 的 procedure 中,就能方便地判断用户是否登录、是否有权限:
export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { ...ctx, user: ctx.session.user } }); });这个protectedProcedure是 t3code 很实用的封装。需要登录才能访问的接口,直接用这个 procedure,不用每个接口都重复写鉴权判断。没登录的用户调用接口,会统一得到UNAUTHORIZED错误,前端通过 React Query 也能统一捕获。
2.4 Tailwind CSS:样式方案为什么选它
Tailwind 属于那种“用之前觉得是智商税,用之后回不去”的工具。t3code 选它不是因为它最花哨,而是因为它最适合这套架构的节奏。
核心原因在于:Tailwind 是“原子化 CSS”,所有样式类都是确定性的,不太容易出现“改一个全局样式,其他页面跟着跑偏”的情况。配合组件级代码组织,样式的作用域被天然地隔离在组件里。
实际操作中,我的建议是:不要把所有样式都堆在页面里。t3code 里比较合理的做法是——组件内部用原子类,组件对外暴露可配置的 className prop。这样既保留了 Tailwind 的灵活,又维持了组件接口的整洁。
3. 实操过程与核心环节实现
3.1 从 t3code 起步:5 分钟起一个新项目
现在我们来走一遍实际操作。t3code 提供的初始化脚本基本能做到“复制即用”:
# 克隆 t3code 模板库 git clone https://github.com/t3code/t3code-starter.git my-app # 进入目录 cd my-app # 安装依赖 npm install # 复制环境变量示例 cp .env.example .env # 启动开发环境 npm run dev这几步完成之后,一个包含 Next.js 页面、tRPC 接口、Prisma 数据库、Tailwind 样式、NextAuth 认证的基础全栈应用就跑起来了。相比手工搭,至少省掉了三个小时的配置时间。
3.2 数据库接入:以 SQLite 起步,无痛切换到 PostgreSQL
t3code 的默认配置里,Prisma 的 provider 可以根据环境灵活切换。本地开发用 SQLite 是最省事的,一行 schema 改动就能切换:
datasource db { provider = "sqlite" url = "file:./dev.db" }如果已经安装了 PostgreSQL,切过去也很快:
# 生成一个新的迁移 npx prisma migrate dev --name init我的建议是:本机开发用 SQLite,测试和线上环境用 PostgreSQL。这样既保证了本地零成本启动,又不会因为“本地没问题、线上有问题”这种经典翻车情况而措手不及。
3.3 定义你的第一个 tRPC Router
理解了基础结构,我们来实现一个贴近真实业务的场景。假设做一个待办事项应用,需要一个“根据用户 ID 查询待办列表”的接口。
先定义 Prisma schema:
model Todo { id String @id @default(cuid()) title String done Boolean @default(false) userId String createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) }然后在server/api/routers/todo.ts里定义 router:
import { z } from 'zod'; import { createTRPCRouter, protectedProcedure } from '../trpc'; export const todoRouter = createTRPCRouter({ list: protectedProcedure .query(async ({ ctx }) => { return ctx.prisma.todo.findMany({ where: { userId: ctx.user.id }, orderBy: { createdAt: 'desc' }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1) })) .mutation(async ({ ctx, input }) => { return ctx.prisma.todo.create({ data: { title: input.title, userId: ctx.user.id, }, }); }), });在server/api/root.ts里注册:
export const appRouter = createTRPCRouter({ todo: todoRouter, });就这么简单,前端页面已经可以拿到类型安全的数据接口了。不需要额外写控制器、序列化层、API 文档。tRPC 把这一整套的样板代码都省了。
3.4 前端页面怎么调用后端接口
在 React 组件里调用 tRPC 接口,体验接近“本地函数调用”:
import { trpc } from '../utils/trpc'; export default function TodoList() { const { data: todos, isLoading } = trpc.todo.list.useQuery(); const addTodo = trpc.todo.create.useMutation({ onSuccess: () => { // 数据更新后自动触发重新拉取 utils.todo.list.invalidate(); }, }); if (isLoading) return <p>加载中...</p>; return ( <div> {todos?.map((todo) => ( <div key={todo.id}> <span>{todo.title}</span> </div> ))} </div> ); }这里最方便的是invalidate机制。写入操作完成后,调用一次invalidate,tRPC 底层的 React Query 就会自动重新拉取该接口的数据。你不需要自己维护“数据变了要手动刷新页面”的状态逻辑,这也是 t3code 用 React Query 而不是 Redux 的原因之一。
3.5 认证流程闭环:登录、获取会话、保护接口
接着把认证流程串起来。NextAuth 的配置在 t3code 里已经封装好,你只需要在server/auth.ts里配置 provider:
以 GitHub 登录为例:
import GitHubProvider from 'next-auth/providers/github'; export const authOptions = { providers: [ GitHubProvider({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }), ], };前端登录按钮绑定signIn,退出绑定signOut,会话状态用useSession获取。tRPC 里用之前提到的protectedProcedure保护需要登录的接口。整条链路非常顺:登录后发起 tRPC 请求,tRPC context 里自动带上 session 用户信息,后端 procedure 校验权限后返回数据。
3.6 环境变量管理与安全配置
环境变量这块,t3code 采用 Next.js 标准的.env文件体系,推荐按环境隔离:
.env:统一默认配置,仅限服务端读取.env.local:本机私有配置,所有环境覆盖.env.production:生产环境配置
**一个很容易踩坑的地方是:Next.js 默认会把.env里的变量注入到浏览器端吗?答案是不会。**只有以NEXT_PUBLIC_前缀开头的变量才会暴露给客户端。t3code 里凡是要在前端用到的配置,比如 API 地址,才会加上这个前缀;数据库地址、密钥这类敏感信息永远只留在服务端。
4. 常见问题与排查技巧实录
4.1 tRPC 报错:Unexpected token or 类型不匹配
这个问题多半出在 zod 校验和实际传入参数不一致。tRPC 依赖 zod 做运行时校验,和 TypeScript 的静态类型是两套体系。TypeScript 校验在编译期,zod 校验在运行期。前端传参漏了字段、字段类型写错,tRPC 会直接抛一个Input validation failed。
排查方法:先确认前端的 input 对象和后端的 zod schema 是否严格匹配。常见的情况是后端增加了必填字段,前端没有同步更新,然后前端还是旧类型,编译没报错——不对,如果类型更新了,编译期会报错,这个反而是 tRPC 的优势。
4.2 Prisma 连接超时或数据库同步失败
P1001这类错误代表 Prisma 连不上数据库。大多数情况不是代码问题,而是环境变量没配对。检查一下.env里DATABASE_URL是不是真的指向了正在运行的数据库实例。
还有一种比较隐蔽的情况:本地用的是 SQLite,线上用的是 PostgreSQL,两者 schema 不完全一致,prisma migrate deploy会报迁移文件冲突。我的经验是:开发环境和生产环境的数据库 provider 尽量保持一致,真的要用 SQLite 起步,上线前也要把迁移记录重新整理一遍,而不是直接沿用旧的迁移链。
4.3 NextAuth 的 session 拿不到用户信息
常见表现是:调用protectedProcedure时一直抛UNAUTHORIZED,但明明已经登录了。
排查步骤:
- 确认前端
SessionProvider是否正确包裹了应用根组件。 - 确认 tRPC context 里的 session 字段是通过
getServerSession获取的。如果用的是getToken,逻辑会复杂很多。 - 确认 NextAuth 的 secret 是否设置。生产环境没有
NEXTAUTH_SECRET,JWT 签名会失败。
最典型的坑是:在 tRPC context 里直接调用getSession()而不是getServerSession()。两者在 Next.js 的 server component 环境下行为有差异,getServerSession更稳定,而且能直接复用 NextAuth 的配置。t3code 里已经是正确的写法了,但如果你自己改过代码,要特别注意这一点。
4.4 引入 tRPC 后 React 组件一直重新渲染
这个问题的根源通常是trpc客户端实例不稳定。在 t3code 的正确用法里,src/utils/trpc.ts的createTRPCNext应当在模块顶层导出,而不是在组件内部创建。如果你在组件里每次渲染都新建一个 trpc 客户端实例,React Query 会认为这是一个新客户端,导致缓存失效并反复触发请求。
检查思路:确认全局只有一个 trpc 实例,所有组件都从同一个模块导入。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 快速排查方向 |
|---|---|---|
| tRPC 接口返回 500 | zod 校验失败 | 检查 input 参数类型与 schema |
| Prisma 报 P1001 | 数据库连接串错误 | 检查 DATABASE_URL 和数据库状态 |
| 登录后仍被拒绝访问 | NextAuth context 注入失败 | 检查 tRPC context 中的 session 获取方式 |
| 部署后页面样式丢失 | Tailwind 构建异常 | 检查 content 配置是否包含对应目录 |
| React Query 无限重试 | 接口抛错未统一捕获 | 检查 ErrorBoundary 和错误重试配置 |
4.6 独家避坑经验:t3code 部署到服务端的细节
最后分享几个我实际操作中踩过的坑。这些细节,官方文档基本不会提,但在生产环境里极其关键。
第一,Next.js 的 standalone 输出模式一定要开。在next.config.js里设置output: 'standalone',这样next build会生成一个最小化的生产依赖目录,配合node server.js启动,比默认模式省掉不必要的 node_modules 打包。t3code 的模板默认没有开启这个选项,但生产部署强烈建议打开。
第二,Prisma 在构建时要确认引擎文件。在 Docker 构建时,Prisma 的 query engine 需要和运行环境匹配。如果构建环境和运行环境都是 Linux,通常没问题。但如果在 macOS 上构建镜像跑到 Linux 上运行,就会遇到 binary 不匹配的问题。解决方案是在 Dockerfile 里显式执行npx prisma generate,并且用prisma generate --no-engine加上正确的 engine 环境变量。
第三,环境变量的安全问题。NextAuth 的NEXTAUTH_SECRET和环境变量的注入,在构建时和运行时别混在一起。Vercel 平台会自动处理,但自建服务器的话,一定要确认.env.production里的变量真的被打包了,还是只存在于服务器环境中。如果不确定,可以在服务端加一个调试接口验证环境变量是否读到了。
5. t3code 的实际工程影响与扩展方向
5.1 对团队协作方式的改变
t3code 这种模板带来的最大影响,其实是协作模式的改变。以前前后端联调,免不了出现“前端等后端接口”,或者“后端文档没更新,前端对着旧文档开发”的情况。
有了 t3code 的类型贯通,接口即函数,函数签名即文档。前端可以非常确定地知道接口需要什么入参、返回什么结构。两个模块如果类型对不上,编译期就能发现,根本留不到联调阶段。这种对团队协作效率的提升,是潜移默化但实实在在的。
另外,t3code 的目录结构天然地为新人做了引导。新接手的同事只需要看server/api/routers下的文件,就能理解项目的接口设计。这对团队的 onboarding 成本降低很明显。
5.2 t3code 适合在什么规模的业务上用
我个人的判断是:t3code 特别适合中小型全栈应用和内部工具。比如管理后台、内容平台、数据可视化面板,这种业务重在前端界面和交互,后端不复杂,一套 tRPC 全覆盖就够了。
如果你的业务已经有非常庞大的后端逻辑、多服务调用、消息队列,t3code 这套就不太合适了。它更适合做“单体全栈应用”的底座,而不是通用微服务方案。
但如果只是做一个创业项目 MVP、一个开箱即用的内部系统、或者一个需要快速迭代的产品原型,t3code 的效率优势会非常明显。花在配置上的时间省下来,全部都能投入到业务逻辑里。
5.3 后续可以怎么扩展
tRPC 官方已经支持了server/client的分离部署,但 t3code 这样的全栈单仓还是最顺手的形态。你可以从这几个方向延伸:
- 加入 WebSocket 订阅:tRPC 支持 subscription,适合做实时通知。
- 引入测试:Vitest + Testing Library 覆盖前端,Prisma 的集成测试用真实测试库。
- 接入 Redis 缓存:对频繁读取的数据,可以做一个缓存中间件包一层 tRPC procedure。
- 迁移到 Turborepo:如果业务膨胀成多个应用,可以把 t3code 的核心逻辑抽成公共包,配合 Turborepo 做多应用管理。
5.4 我对 t3code 的总体体会
在真实项目里用熟了以后,我的一个明显感受是:t3code 这种方案,不是靠某个单点技术强,而是整套组合的匹配度极高。Next.js 负责渲染和路由、tRPC 负责端到端类型、Prisma 负责数据层的安全、Tailwind 负责样式的心智负担控制、NextAuth 负责认证的标准化,每一块都在自己的位置上解决一类具体问题,不重叠、不冲突。
它适合的,是那种“想快速把产品做出来,但又不希望代码质量糊成一团”的开发者。
最后再分享一个小技巧:如果你基于 t3code 启动新项目,第一件事不要急着写业务,先把 Prisma 的 schema 定义完整,因为它是后面所有类型的源头。schema 搭好了,后面写 tRPC router 和前端页面都很快。削好铅笔再画图,比边写边改要舒服得多。