☰
T3 Stack 实战:Next.js、tRPC、Prisma 类型安全全栈应用
2026/10/26 17:12:12 网站建设 项目流程

T3 Stack 在圈内火起来差不多两年了,我一直想找个机会完完整整地跑通一遍,而不是停留在看文档、收藏帖子的层面。于是就有了这个叫t3code的项目:不搞花哨的演示,就老老实实基于 T3 Stack 从初始化开始,搭一个带数据库、带 API、带页面交互的全栈应用,把 Next.js、TypeScript、Tailwind CSS、tRPC、Prisma 这一套真正串起来。

如果你也正在纠结“T3 Stack 到底好在哪”“create-t3-app 生成的目录该怎么改”“tRPC 和传统 REST API 相比有没有必要学”,那这篇实战记录应该能省你不少时间。它适合刚接触全栈开发想找一个靠谱起点的朋友,也适合已经在用但一直没弄明白内部调用链路的开发者。我会把每一步的选型逻辑、参数来源、踩过的坑都写清楚,尽量做到看完就能照着复现。

1. 项目整体设计与技术选型思路

1.1 T3 Stack 到底解决了什么痛点

先聊聊为什么会有 T3 Stack 这个组合。做全栈应用最常见的问题不是“某个框架不会用”,而是多个框架拼在一起之后,类型对不上、接口文档靠手写、数据库字段改了前端报错找半天。传统模式里,后端写接口、前端调接口,两边各有一套类型定义,改动一次就要同步一次,漏了就开始蔓延连锁错误。

T3 Stack 的核心思路就是“端到端类型安全”。它在服务端和客户端之间只用一套 TypeScript 类型,从数据库模型、API 路由到前端页面,类型在编译期就互相校验。t3code 这个项目选它为首选方案,是因为我想验证的不是某个框架的单点能力,而是整条链路的开发体验。实际跑下来,最大的感受是:前端调用某个接口时,参数结构、返回类型都是编辑器直接提示出来的,基本不需要来回翻文档。

这套组合里的四个主要成员各有分工。Next.js 负责路由和渲染,TypeScript 提供类型约束,Tailwind CSS 处理样式,tRPC 替代传统的 REST API 定义方式。如果还需要数据库操作,Prisma 作为 ORM 补齐数据层。它们单独拎出来都不算新东西,但组合在一起之后,开发范式就变了:你不再需要为每一个小接口去写 controller、手拼 URL、维护类型声明,类型系统和函数调用天然帮你管住了这些细节。

1.2 为什么选择这套组合而不是传统 REST

直接说结论:如果你的项目是内部应用、前后端都由一个团队维护,tRPC 的开发效率是明显优于手写 REST 接口的。REST 的优势在于通用性和语义清晰,适合对外暴露 API、或者前后端由不同团队维护的场景。但 tRPC 本身就是 TypeScript 函数调用,不需要额外定义 URL 规则、不需要做序列化协议对齐,调用一个后台函数跟调用本地模块一样自然。

t3code 在选型时也保留了对 REST 的兼容。Next.js 的 Route Handler 仍然在项目里承担部分外部回调、webhook 类功能,因为这类场景要求的是标准化 HTTP 接口,而不是类型安全的函数调用。所以我的建议从来不是“tRPC 取代一切”,而是按边界划分:内部数据交互用 tRPC,对外集成用标准接口,这样两边都能拿到自己最想要的能力。

还有一个容易被忽略的点是数据层的设计。Prisma 在这里不只是简化 SQL,它生成的 client 类型直接可以被 tRPC 的 procedure 引用,数据库查询结果返回给前端时,类型已经携带着字段结构。这就意味着数据库某张表加了字段、改了约束,前端引用到这些数据的地方会在编译阶段马上报错,而不是等到运行时才发现。

2. 技术选型的核心原理解析

2.1 Next.js 的 App Router 与页面组织

t3code 用 Next.js 作为整个应用的底座,当前创建的版本默认启用了 App Router。这两者的区别值得花一点篇幅说清楚,因为很多新手在初始化之后并不知道自己用的是哪套路由体系。Pages Router 是 Next.js 早期的方案,路由文件放在 pages 目录下,文件名即为路径。App Router 是后来主推的目录约定,app 目录下的 layout.tsx、page.tsx 决定了页面嵌套和加载行为。

在 t3code 里,我选择了 App Router,主要原因有两个。第一,App Router 原生支持 Server Component,可以在服务端直接拿 Prisma 数据渲染,某些只读页面可以减少一次前后端往返。第二,layout.tsx 可以做全局布局,t3code 的顶部导航、公共容器都在根 layout 里定义,子页面只需要关心自己的内容区。这是 Pages Router 时代必须写一堆 _app.tsx 和 Layout 组件才能实现的效果,现在从目录结构上就帮你规范好了。

但你也要清楚,App Router 的 Server Component 限制比较多,比如不能直接使用 useState、useEffect 这类客户端特性。t3code 在需要交互的地方用 "use client" 声明客户端组件,比如搜索框、表单、筛选按钮。这个边界在一开始就划分好,后面开发就不用反复改结构。

2.2 tRPC 的调用链路是如何设计的

tRPC 是 t3code 里面最值得花精力理解的一个环节,它直接决定了整个项目的数据流组织方式。为了把 tRPC 的原理讲透,我会用一个非常简单的类比:你把自己电脑上定义好的一个函数让远端去执行,函数的入参和返回值都有明确的类型,编译器会帮你校验两边是否对齐。

在 t3code 的项目结构里,tRPC 的链路是这样的:根路由文件把各个业务模块的 router 汇总成一个 appRouter,然后分别给服务端和客户端使用。服务端用这个 appRouter 创建 HTTP handler,处理 /api/trpc 下的请求;客户端则通过 tRPC 的 createTRPCClient 拿到一个类型安全的调用对象,比如 trpc.post.list.query()。整个过程不需要定义接口路径,不需要手写请求函数,tRPC 会根据函数名和方法类型生成对应请求。

需要注意的关键点:tRPC 的“类型安全”并不是运行时替你兜底,而是在编译期通过 TypeScript 类型推导实现的。如果你的前后端不在同一个 TypeScript 工程里,需要额外引入共享类型包才能实现跨项目类型传递。t3code 是单仓库结构,前后端同在一个项目里,所以这种类型推导是天然成立的,这也是单仓库在 tRPC 场景下体验更好的原因。

2.3 Context 与中间件的设计逻辑

tRPC 有一层非常重要的概念叫 Context,它负责在每次请求进入时构建一些基础数据,比如当前登录用户、数据库客户端、请求头信息。t3code 里没做复杂的权限系统,但依然保留了一个最简单的 Context:每次调用 tRPC 接口时,把 Prisma Client 实例注入进去,这样所有 procedure 都可以通过 ctx 访问数据库,而不需要每个文件单独初始化连接。

这个设计的价值在于解耦。数据库连接不应该被页面组件直接使用,而是通过 tRPC 的 ctx 间接传入。后面如果要想加入登录鉴权,只需要在 Context 里增加一个 session 解析逻辑,然后写一个 requireAuth 的中间件做统一拦截。t3code 把这种扩展的“起点”保留好了,而不是把整套权限方案写死。

3. 实操过程:从零开始搭建 t3code

3.1 环境准备与 create-t3-app 初始化

开始之前,先交代一下我本地的环境:Node.js 20+、pnpm 作为包管理器。T3 Stack 官方推荐使用 pnpm,因为它对依赖版本的处理更严格,也更能避免幽灵依赖问题。初始化命令非常简单,直接在目标目录执行:

pnpm create t3-app@latest

执行后会有一系列交互式选项,我选择的配置如下:

  • Next.js 使用 App Router
  • 启用 TypeScript
  • 启用 Tailwind CSS
  • 启用 tRPC
  • 启用 Prisma
  • 认证方案选择None,因为 t3code 第一版先不考虑登录,等数据链路跑通后再接 Auth.js
  • 其余选项按默认来

这一步选型值得多说两句。很多人一上来就全选启用,结果初始代码里塞满了用不到的功能,光删除默认示例就要花不少时间。我的建议是:明确知道自己要什么再选。t3code 的目标是验证核心链路,所以只保留了刚需模块。如果你不确定是否要 Prisma,可以先不勾选,后面用第三方工具自己集成也是可以的。

初始化完成后,项目会自动创建好 .env 文件,里面有 DATABASE_URL 和 NEXTAUTH_SECRET 等变量占位符。注意这里并不会自动生成有效密钥,NEXTAUTH_SECRET 需要你自己跑一下openssl rand -base64 32生成一个随机值填进去。如果你暂时没有启用认证,NEXTAUTH_SECRET 可以先留空,但建议养成补全环境变量的习惯,避免后续启动时报错。

3.2 数据建模与 Prisma 迁移

t3code 的数据模型设计得尽量贴近真实场景,我用了一个非常经典的案例:文章加标签的多对多关系。这个场景既有普通字段查询,又有关系表的联表操作,足够展示 Prisma 的核心能力。schema 文件在 prisma/schema.prisma 下,核心定义如下:

model Article { id String @id @default(cuid()) title String content String published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt tags TagOnArticle[] } model Tag { id String @id @default(cuid()) name String @unique articles TagOnArticle[] } model TagOnArticle { article Article @relation(fields: [articleId], references: [id]) articleId String tag Tag @relation(fields: [tagId], references: [id]) tagId String @@id([articleId, tagId]) }

写完之后执行迁移命令,把 schema 同步到数据库:

npx prisma migrate dev --name init

如果你本地没有 Postgres,我建议先用 Docker 跑一个临时实例,命令大概是:

docker run --name t3code-postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:16

DATABASE_URL 的格式是postgresql://postgres:password@localhost:5432/t3code?schema=public。这里要特别提示:Prisma 对数据库版本的兼容性很敏感,Postgres 16 以上版本在某些 Prisma 版本里会有小坑,如果遇到版本不兼容,优先检查 Prisma 版本是否支持你使用的数据库大版本。

迁移完成后,记得执行npx prisma generate生成客户端类型。这一步很多人容易漏掉,导致编辑器里出现各种类型报错,其实不是代码写错了,而是 Prisma Client 的类型还没重新生成。t3code 在 package.json 里配置了 postinstall 钩子,每次安装依赖后都会自动执行 generate,但手动改了 schema 之后最好还是主动跑一次。

3.3 tRPC 接口与页面调用链路实现

数据模型准备好之后,接着就要在 tRPC 层把查询能力暴露出去。t3code 的 src/server/api/routers/post.ts 文件里定义了一组文章相关的 router,核心代码大致如下:

export const postRouter = createTRPCRouter({ list: publicProcedure.query(({ ctx }) => { return ctx.db.article.findMany({ include: { tags: { include: { tag: true } } }, orderBy: { createdAt: "desc" }, }); }), getById: publicProcedure .input(z.object({ id: z.string() })) .query(({ ctx, input }) => { return ctx.db.article.findUnique({ where: { id: input.id }, include: { tags: { include: { tag: true } } }, }); }), create: publicProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), tagIds: z.array(z.string()).optional(), }) ) .mutation(({ ctx, input }) => { // 创建文章并关联标签 }), });

值得展开讲的是这里用了 Zod 做输入校验。Zod 是 tRPC 的默认搭档,输入参数必须先定义 schema,tRPC 在运行时和编译期都会拿这个 schema 做双重检查。运行时不符合规则就直接拒绝,编译期类型也能推导出入参结构。你不需要在前端再写一份参数校验逻辑,只用一套定义就管住了两端。

页面调用侧的核心逻辑在 src/app/ 目录下。列表页是一个 Server Component,可以直接通过 createCaller 在服务端调用 tRPC:

const caller = createCaller({ db }); const articles = await caller.post.list();

交互性较强的页面,比如创建文章表单,则需要走客户端调用。t3code 里用 a useState 管理表单状态,提交时直接调用trpc.post.create.mutate(),成功后用router.refresh()刷新服务端组件的数据。这个模式的好处是避免了手动管理全局状态库,表单、接口、刷新都被压缩在一个简单的交互闭环里。

3.4 环境变量管理与部署配置

部署前先把环境变量分类整理清楚,这是 t3code 跑通之后我认为最值得强调的实践。项目里其实涉及两类变量:一类是构建期变量,比如 Next.js 的NEXT_PUBLIC_前缀变量,它们会被打入前端 bundle;另一类是运行期变量,比如DATABASE_URL、NEXTAUTH_SECRET,它们只在服务端读取,绝不能暴露到浏览器。

在 Vercel 部署时,需要先在 Project Settings → Environment Variables 里逐个添加这些变量。一个大坑是NEXT_PUBLIC_前缀的变量在本地 .env 和 Vercel 环境里必须保持完全一致,否则前端代码运行时读不到正确的值。另一个坑是很多人会把 DATABASE_URL 写成 localhost,部署到线上之后数据库还没迁移,接口会一直 500。最容易复现的步骤是:先用prisma migrate deploy在线上数据库执行迁移,确认表结构建好,再启动 Next.js 应用。

t3code 的部署配置里还涉及中间件问题。如果使用 Cloudflare Pages 或 Node.js Serverless 部署,需要确认 Prisma 的二进制引擎能否在当前运行时工作。Vercel 默认支持较好,其他平台建议在部署日志里检查是否报PrismaClientInitializationError,如果报错,多半是引擎二进制不匹配,需要切换为@prisma/client的 Data Proxy 模式或在构建命令里执行prisma generate。

4. 常见问题与排查技巧实录

4.1 高频问题速查表

下面这张表整理了 t3code 开发期间最常遇到的现象,以及对应排查路径。很多问题在社区里被反复提问,本质上都是同一类原因。

现象可能原因解决方式
前端调用 tRPC 报 404Next.js API 路由 handler 未正确挂载检查 app/api/trpc/[trpc]/route.ts 是否存在,路径是否正确
类型报错显示 Prisma 类型不存在修改 schema 后未重新生成客户端执行npx prisma generate,重启编辑器
部署后数据库操作失败DATABASE_URL 指向本地地址确认线上环境变量已配置,数据库已迁移
tRPC 请求跨域被拦截前后端不同源确认代理或 CORS 配置,同源部署优先级最高
页面能渲染但数据不刷新Server Component 缓存未失效在 mutation 后调用router.refresh()
NEXTAUTH_SECRET 未定义导致启动失败环境变量缺失生成随机密钥并配置到对应环境
Prisma migrate 报版本不兼容Prisma 与数据库版本不匹配升级 Prisma 版本或调整数据库镜像版本

很多问题的共性都指向同一个点:没有在修改完 schema 或路由后重启开发服务器。Next.js 的 HMR 对组件热更新做得很好,但它不会自动重新加载 Prisma Client 和 tRPC 的 runtime,如果遇到莫名其妙的类型或运行时报错,先重启 dev server 再排查其他问题。

4.2 避坑经验:从 t3code 跑通中学到的五件事

第一件事,不要跳过 t3code 初始化时的模块选择。create-t3-app 给的选项不是装饰,每个模块都对应一套初始代码。如果你根本不需要 tRPC,却因为“先勾上,以后可能用到”选择了它,那你大概率要花半小时删除 sample router、清理依赖、调整路径。真正需要的时候再加,成本比删除低得多。

第二件事,tRPC 的 Procedure 不要写太多逻辑。我在 t3code 早期版本里,习惯把业务逻辑都堆在 procedure 里,比如创建文章那里同时处理标签映射、文章计数、日志记录。结果 procedure 越长,错误定位越难,而且 tRPC 的报错信息有时并不能准确指出具体业务逻辑哪里出了问题。更好的做法是:procedure 只做“参数校验→调用 service 层→返回结果”,业务逻辑全部下沉到独立的 service 函数里,这样测试可以针对 service,不依赖 tRPC 的调用上下文。

第三件事,Prisma 的 include 查询要用在刀刃上。t3code 的文章列表一开始用include: { tags: true }把关联数据全查出来,实际页面只展示数量,大量数据浪费在传输上。改成select: { _count: { select: { tags: true } } }之后,响应体明显小了一截。这属于读接口优化的常规操作,但在 tRPC 场景下更容易被忽略,因为类型提示太友好,让人误以为这样写也没什么代价。

第四件事,理解 Server Component 与 tRPC 的关系。t3code 里同时存在两条调用路径:Server Component 里用createCaller直接调服务端函数,客户端组件里用trpc.xxx走 HTTP。这两者共用同一个 tRPC 路由定义,但类型来源和使用感受完全不同。Server Component 的调用是同步的、本地化的,没有网络消耗;客户端调用是异步的、HTTP 化的。在设计页面时就要想清楚哪些数据适合服务端直取,哪些必须由客户端动态请求,不要在同一个页面上混用造成重复请求。

第五件事,保持依赖版本统一。T3 Stack 的初始化模板在刚发布时是良配,但经过几个月后,Next.js、Prisma、TypeScript 都可能更新了 major 版本,如果你在旧模板基础上手动升级某个包,很容易踩到兼容性坑。t3code 的方案是定期拉取新版 create-t3-app 生成的模板,跟自己的业务代码对比,把基础设施部分的升级单独处理,业务代码不动。这个小习惯让升级这件事变得非常轻松。

4.3 调试 tRPC 请求的工具与方法

t3code 开发过程中,我花了些时间整理 tRPC 的调试方法。tRPC 本质上就是 HTTP POST 请求,所以浏览器开发者工具的 Network 面板就能看到很多信息,但请求体是 JSON 序列化后的数据,不太好直接阅读。这里推荐一个更高效的路径:在 tRPC 服务端日志里打点。t3code 的 trpc route 配置文件里,可以加上一个简单的 logger middleware,输出每次调用的路径、耗时、参数摘要:

const loggerMiddleware = t.middleware(async ({ path, type, rawInput, next }) => { const start = Date.now(); const result = await next(); const durationMs = Date.now() - start; console.log(`[tRPC] ${type} ${path} ${durationMs}ms`); return result; });

这个方法比在浏览器侧看网络请求高效得多,特别是在排查 Server Component 内部调用时,浏览器面板甚至看不到这条请求。让日志直接输出到服务端控制台,加上类型、路径、耗时,足够你定位绝大部分性能问题。

4.4 从 t3code 到真实项目的扩展建议

如果 t3code 让你确认 T3 Stack 适合你的项目,下一步可以考虑这几个扩展方向:接入 Auth.js 做登录鉴权、增加文件上传与静态资源管理、引入 WebSocket 做实时推送,或者把 tRPC 的 router 按业务模块拆分得更细。这些扩展都不会破坏现有链路,因为 t3code 的核心就是“类型安全的数据流”,任何新的数据入口,只要在 tRPC 里加一个 procedure 并定义好 Zod 入参,前端就能立即获得类型提示。

我也在考虑后续给 t3code 增加一个简单的 CI 流水线,在 push 时自动跑prisma migrate diff检查 schema 漂移,跑 tsc 和 eslint 做质量门禁。这种做法对个人项目和团队项目都有意义。如果你也想这么干,注意不要在 CI 里直接连生产数据库执行 migrate dev,那会在非交互环境下把数据库弄得很乱。正确做法是用prisma migrate diff --from-schema-datasource生成 diff 文件,人工确认后再用 deploy 命令执行。

说到底,t3code 教会我的不是某个框架的新语法,而是“数据的类型安全可以贯穿始终”这种开发方式的改变。当你习惯了在数据库模型里改一个字段、全链路类型同步更新、编辑器编译器替你挡住低级错误之后,就很难再回到手写接口文档和手动同步类型的老路上了。写这篇记录的时候,我还在继续往 t3code 里补充功能,如果你也照着跑了一遍并且踩到了新坑,欢迎按同样的思路把它记下来——每个坑都是下一步改进的方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询