T3 Stack实战:Next.js+tRPC+Prisma全栈开发指南

发布时间:2026/10/27 23:02:08

T3 Stack实战:Next.js+tRPC+Prisma全栈开发指南 上礼拜帮一个朋友看他的外包项目对方上来就一句“就你一个人写两礼拜能上线不”。我说能别用你们公司那套几十个微服务的架构了直接整套T3走起。后来想了想这几年我经手的全栈项目里如果是单人或小团队作战我几乎默认就会落到Next.js tRPC Prisma Tailwind这套组合上。之前零零散散写过一些片段这次干脆把这些实践经验整理成一个代号就叫t3code。说白了t3code不是什么开源框架也不是某个官方工具而是我自己对“T3 Stack”这套技术组合的实践总结。T3 Stack本身指的是TypeScript、Tailwind CSS、tRPC这三个名字里都带T的技术再加上Next.js和Prisma基本把现代全栈项目的底子给铺完了。这篇文章适合正在用或准备用T3这套组合的开发者看也适合那些想搞懂“为什么这么多人说tRPC香”“类型安全到底怎么个安全法”的人。下面这几块内容全部来自我实际跑项目、改代码、上线部署时踩过的坑和沉淀下来的方法。1. 为什么我死磕T3这套组合它解决的真正问题是“契约成本”1.1 T3名字的来历和它包含的生态T3这个名字最早是Theo在社区里推起来的全称是“The T3 Stack”字面意思是TypeScript、Tailwind、tRPC这三个以T开头的技术栈的组合后来实践中又加上了Next.js和Prisma构成了一个完整的全栈应用底座。很多人第一次看到这个组合会有点懵Next.js本身就能写API路由为什么还要塞一个tRPCPrisma只是个ORM和tRPC有什么关系Tailwind又只是样式方案凭什么和后面几个并列我的理解是T3真正想解决的不是“某个功能怎么做”而是“前后端协作时那份契约谁来维护”。传统模式里前端要先等后端把接口文档写好然后按文档联调后端改了字段名前端可能半天之后才发现。T3的思路是用TypeScript把前后端拉进同一个类型系统里接口的请求参数、返回结构全程由编译器盯着改一处相关联的位置立刻报错。1.2 和小团队REST方案的真实对比我在一个小团队项目里对比过REST和tRPC两种方案的开发体验。REST方案我们用了Swagger/OpenAPI维护文档每次新增接口要先写接口定义、再写Controller、再生成客户端SDK三个步骤里任何一步没同步前端拿到的类型就可能是旧的。整个流程不是不可以用但对小团队来说这个维护成本占比太高了。T3方案里tRPC的router就是接口定义本身。你在服务端写一个procedure客户端那边立刻就能拿到对应的hook参数类型、返回值类型全都推出来了。有一次我重构一个用户信息字段把nickname改成了displayNamePrisma模型一改前端调用处、校验逻辑、渲染组件里凡是引用了旧字段的地方全部标红。这种“改错就让编译失败”的体验试过一次真的回不去。1.3 它不适合什么场景当然T3也不是银弹。如果你们团队已经有大量沉淀好的REST API或者系统里要频繁对接第三方服务那tRPC更多是内部粘合剂强行把一切塞进tRPC反而别扭。另外如果你的项目需要处理文件流、大量WebSocket长连接、或者要对请求做非常细致的灰度路由tRPC默认的上限会比较难满足Better to设计独立服务。一句话总结T3最适合的场景是“一个产品一个TypeScript全栈仓库一个干活的人或一个两三人小团队”。它两礼拜上线的能力本质上是靠类型系统和约定把沟通成本省下来的。2. 从create-t3-app开始初始化选项与项目骨架解读2.1 初始化交互项到底怎么选T3官方提供了脚手架工具create-t3-app初始化命令很简单npm create t3-applatest my-t3code-app # 或者 pnpm create t3-applatest my-t3code-app执行之后它会问几个交互选项我第一次跑的时候每个都凭感觉选后来项目深了才发现有些选项直接影响后续开发方式。我现在的选择是固定的见下表选项我的选择说明包管理器pnpm依赖安装更快磁盘占用也更小TypeScript选上不选就没意义了Tailwind CSS选上样式方案几乎不用额外配置tRPC选上核心中的核心Prisma选上数据库ORM和tRPC配合最顺NextAuth按需需要登录就用不需要就先不选Drizzle不选和Prisma二选一我统一用Prisma这里提醒一下Prisma和Drizzle是竞品关系脚手架允许你选其中一种项目里同时用俩纯属给自己找麻烦。NextAuth如果你暂时用不到可以在初始化时跳过后面需要了再补并不难。2.2 目录结构里三层分法脚手架跑完后的目录结构大致是这样src/ app/ # Next.js App Router 页面和路由 server/ api/ routers/ # 业务路由比如 post.ts、user.ts root.ts # 汇总所有路由 trpc.ts # tRPC服务端实例和procedure定义 db.ts # Prisma Client 单例 trpc/ react.tsx # 客户端React hooks入口 server.ts # Server Components里调用tRPC的入口 shared.ts # 服务端和客户端共享的类型定义 prisma/ schema.prisma # 数据模型文件第一次用的人最容易困惑的是为什么有trpc/react.tsx和trpc/server.ts两个入口。其实这不复杂React组件里用它走hooksServer Component或Server Actions里要用服务端直连方式调用时走另一个。两个入口都导出同一个appRouter类型保证两端类型永远同步。我实战中养成的习惯是业务逻辑尽量放在routers/下的文件里一个业务域一个文件页面组件里只做UI和交互不要到处写查询逻辑。这个习惯在T3项目里特别重要因为tRPC让前后端调用变得太简单很容易写出一堆散落在组件里的请求逻辑等你要复用的时候才后悔。2.3 环境变量和数据库先跑通新项目最容易踩的一个坑是环境变量没配全就急着跑dev server。create-t3-app默认会在.env里生成一个DATABASE_URL如果直接跑npm run dev大概率会看到Prisma连不上数据库的错误。我建议初始化完成后先干三件事打开.env把DATABASE_URL改成你能用的库地址。开发阶段图省事可以用SQLite字符串长这样DATABASE_URLfile:./db.sqlite运行npm run db:push或npx prisma db push把schema同步到数据库。运行npm run dev打开首页确认能跑起来。这里有个取舍要说清楚开发环境用SQLite非常省事但如果你知道自己最终要部署到Vercel这类平台最好从一开始就用Postgres免得最后因为SQLite和Postgres行为差异而返工。我个人推荐直接上Neon或Supabase的免费Postgres实例本地开发连远程库部署时同一套环境变量直接复用。3. 类型安全不是玄学Prisma、zod到tRPC的一条链路3.1 用一张note表来说明schema怎么定很多人把T3的类型安全理解成一个模糊的概念实际上它是有一整条具体链路的Prisma模型决定了数据库字段类型zod决定了API参数校验tRPC把这两者绑定到一个procedure上最后用TypeScript推导给客户端。我拿一个最基础的速记本应用来说。先定义一张note表generator client { provider prisma-client-js } datasource db { provider sqlite url env(DATABASE_URL) } model Note { id String id default(cuid()) title String content String? createdAt DateTime default(now()) updatedAt DateTime updatedAt }改完schema后跑npx prisma db pushPrisma会自动生成Client类型。此时你在代码里写ctx.db.note.create参数里title是必填string、content是可选string创建时间和更新时间不用你管typescript全都帮你盯着。3.2 tRPC router与客户端hook的调用接下来在服务端建一个note router// src/server/api/routers/note.ts import { z } from zod; import { createTRPCRouter, publicProcedure } from ../trpc; export const noteRouter createTRPCRouter({ list: publicProcedure .query(async ({ ctx }) { return ctx.db.note.findMany({ orderBy: { createdAt: desc }, }); }), create: publicProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().optional(), }) ) .mutation(async ({ ctx, input }) { return ctx.db.note.create({ data: { title: input.title, content: input.content, }, }); }), });然后在root router里挂上去// src/server/api/root.ts import { createTRPCRouter } from ./trpc; import { noteRouter } from ./routers/note; export const appRouter createTRPCRouter({ note: noteRouter, }); export type AppRouter typeof appRouter;客户端调用就非常直接了use client; import { api } from ~/trpc/react; export function NoteList() { const utils api.useUtils(); const { data: notes, isLoading } api.note.list.useQuery(); const createNote api.note.create.useMutation({ onSuccess: () { utils.note.list.invalidate(); }, }); return ( div {notes?.map((note) ( div key{note.id}{note.title}/div ))} /div ); }这一步是T3体验最精髓的地方api.note.create.useMutation这个hook里输入参数类型和返回值类型完全由服务端router定义推导出来你在客户端写createNote.mutate({ title: xx })时如果是content写成了context编译直接报错根本不用等到请求发出。3.3 为什么改字段名会在编译期报错表面上看这只是一层类型代理但它省掉的东西非常多。传统REST请求你改了后端某个字段名前端哪怕漏了接口还是能调通只是返回里少个字段。tRPC不一样——它通过类型把“服务端真实返回”暴露给了客户端前端一旦引用了不存在的属性编译阶段就失败了。用生活化的比喻解释REST像是供应商给你一份印刷版的价目表他那边改了价格你手上这份还是旧的等你按旧价格下单才发现货不对板。tRPC像是两家公司共享同一个在线数据库供应商一改价目你这边表格当场更新价格对不上根本没法下单。实际项目里我用这个特性吃过红利全局搜索旧字段名所有引用的地方一目了然根本不需要靠人工去问“这个接口还有谁在用”。4. 用AI批量生成T3业务代码这不是偷懒是流程4.1 给AI喂上下文时我用了什么prompt既然这篇实践代号叫t3code除了T3本身我也要聊聊怎么让AI高效地写出符合T3风格的代码。花点时间在prompt上是值得的。一个良好的prompt比让它自由发挥稳定得多。我常用的模板大致是这样的这是一个 Next.js App Router 项目使用 T3 Stack。 项目约定 - Prisma 模型定义在 prisma/schema.prisma模型名 Note 的字段为... - tRPC 路由定义在 src/server/api/routers/ 下每个文件导出 xxxRouter - 服务端实例从 src/server/api/trpc.ts 中导入 createTRPCRouter 和 publicProcedure - 客户端调用统一走 src/trpc/react.ts 的 api 对象不要生成 pages/api 目录 - 输入校验用 zod写在 .input() 里 新需求给 Note 增加一个 favorite 字段并实现 toggle 接口。AI接收到这种约束后生成的代码基本可以直接用。原因不复杂T3项目的模式高度统一router怎么写、procedure怎么定义、invalidate怎么刷新翻来覆去就那么几套。模式越一致AI生成结果的可靠度就越高。这其实是T3额外的红利——它牺牲了一点自由度换来了用AI填充业务的稳定性。4.2 AI在T3项目里最容易踩的四个坑AI生成代码不是完全没坑的总结一下我在复用它时踩过的几个典型问题。第一个坑是它经常会顺手生成旧的Pages Router结构比如src/pages/api/trpc/[trpc].ts。在App Router项目里这个文件不应该存在tRPC的HTTP处理器位置完全不一样。如果AI给你生成了这种文件别多想删掉检查版本就对了。别问我为什么知道我删过三回。第二个坑是参数类型和数据库字段不匹配。比如Prisma里DateTime类型AI默认就给了z.string()。数据库存的是Date你这里校验出string后面做排序和比较时全是隐性问题。所以我对AI生成结果有个硬性要求zod schema必须对着Prisma模型手写不能靠猜。第三个坑是它在Server Component里调用tRPC时容易引用客户端的api对象。App Router的RSC里根本不能直接用hooks要用src/trpc/server.ts导出的server caller。AI如果分不清这两个入口写出来的代码要么构建报错要么运行时炸。第四个坑更隐蔽一点它动不动就把整个ctx.db.note.findMany()的结果直接返给客户端一个帖子列表把正文、内部备注全带出去了。正确做法是加select白名单或者至少用omit删掉敏感字段。4.3 我的验收清单AI生成的代码我提交前一定会过一遍这个清单有没有引用pages/api或旧版App Router目录zod的input类型和Prisma字段类型是否一一对应Server Component里有没有误用client hook返回给前端的数据是否经过了select过滤是否需要登录的接口用了publicProcedure而不是protectedProcedure这套清单跑完AI生成的代码基本能当人工写的用。不过我得强调AI只是帮你把80%的样板代码写了剩下20%的业务边界、权限判断、数据过滤必须自己看仔细。无脑提交AI代码迟早线上见。5. 部署上线和真实业务中的坑5.1 数据库迁移不该在应用启动时自动做第一次部署T3项目到Vercel时我图省事在build脚本里加了prisma db push结果构建阶段Prisma要连数据库Vercel的构建环境网络策略一限制构建直接失败。后来我学乖了把迁移和构建分开。正确的姿势是本地或CI里跑npx prisma migrate dev --name xxx生成迁移文件然后部署前跑npx prisma migrate deploy让生产数据库应用迁移。应用启动时只做Prisma Client生成不做迁移。这是很多T3新手上线时翻车的头号原因。如果你用的是Neon这种Serverless Postgres记得环境变量里至少要有两个值DATABASE_URLpostgresql://... DIRECT_URLpostgresql://...DATABASE_URL走连接池DIRECT_URL走直连很多Prisma命令比如prisma migrate deploy需要直连地址。只配一个DATABASE_URL本地开发没问题部署上去就会遇到一会在migrate时连不上的怪问题。5.2 Server Components与tRPC调用不要互相死锁T3项目使用App Router时有一个很隐蔽的坑在Server Component里调用tRPC server caller而tRPC context里又去读headers()这两者同时出现构建时不报错但运行时可能直接抛出死锁或缓存警告。解决思路是如果页面主要是静态内容优先在服务端用trpc/server.ts的caller直取数据如果页面交互性强、数据时常变化直接用客户端hooks配合React Query缓存。两头都想兼顾的话小心不要在同一个渲染周期里反复创建caller实例最好把caller放进request scope里复用。这里我多说一句tRPC 11的API和10有一些差异设置server caller的方式也会不一样。查文档时务必先确认版本别看错了又怪框架不好用。5.3 请求合并和其他可以白拿的性能优化tRPC默认用了httpBatchLink意思是同一时间片内的多个请求会合并成一个HTTP请求发给后端。这个特性你在本地感觉不明显一旦上了生产列表页同时请求三四个接口时效果立竿见影地少了一大半请求数。另外React Query的缓存策略值得花时间调一调。默认的staleTime是0意味着每次进页面都会重新拉数据。我一般把不常变的数据的staleTime设成30秒或1分钟页面响应速度会明显变好。T3脚手架的client初始化中能直接配置比如api实例里设一个全局默认省得每个查询都重复写。文件上传、大数据流这类需求就别硬塞tRPC了我试过把上传接口也做成mutation结果就是请求体巨大、进度条还不好做。最后老老实实单独用对象存储的直传接口tRPC只负责生成临时凭证体验好很多。5.4 安全和权限protectedProcedure的价值T3脚手架集成NextAuth后tRPC的context里可以用session。我通常会把session里的用户id直接挂在context上protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const note await ctx.db.note.findUnique({ where: { id: input.id }, }); if (!note || note.userId ! ctx.session.user.id) { throw new Error(无权操作); } return ctx.db.note.update({ where: { id: input.id }, data: { ... }, }); });如果接口明明要求登录却用了publicProcedure一旦上线轻则数据裸奔重则被恶意刷接口。我每次建新router时都会默认先用protectedProcedure只有在明确“这个接口任何人可调”时才改成publicProcedure。宁可先严后松也别先松后严。最后分享一个实用的小习惯。我在每个tRPC实例上加了个极简的日志中间件把procedure名和耗时打印出来线上排查问题的时候非常省事const loggerMiddleware t.middleware(async ({ path, type, next }) { const start Date.now(); const result await next(); console.log([tRPC] ${type} ${path} ${Date.now() - start}ms); return result; });就这个几行的中间件配合Vercel的日志面板已经帮我定位过好几次“哪个接口慢”“哪个接口被刷”的诡异问题了。T3这套组合不难套路摸熟了之后绝大部分项目都能在很短时间里跑起来。把它玩透比追一堆新工具实在得多。
延伸阅读

更多相关文章

2026/10/27 23:02:08

算法工程师十年技术栈演变与核心技能升级

1. 算法工程师的角色变迁2015年那会儿我刚入行,算法工程师这个岗位还带着几分神秘色彩。那时候我们主要用Python写些简单的机器学习脚本,调调scikit-learn里的模型参数就能解决大部分问题。记得第一次用随机森林做用户分群时,团队里的人都觉得…

2026/10/27 23:02:08

从代码腐化到 t3code:TypeScript 工程化编码规范实践

先说个背景。年初我接手了一个跑了快两年的 TypeScript 全栈项目,代码量四万多行,看起来一片繁荣。真正开始迭代才发现,处处都是any、隐式类型、三四种命名风格共存,一个组件里塞了两千行业务逻辑。最离谱的是,同一个数…

2026/10/27 23:02:08

opencode 深度解析:终端 Agent 生态位、模型接入与 Skill 实战

如果你用过 Claude Code 再回头看 opencode,会有一种“这工具怎么越做越像个活物”的感觉。上篇讲完了基础安装和日常用法,这篇我打算把大家问得最多的几个层面一次性聊透:opencode 在整套 AI 编码工具链里到底处于什么生态位、它的模型服务面…

2026/10/28 0:07:14

Gemini 生成文件入门:在对话里直接产出 Word、Excel 和 PDF

适合人群:经常让 AI 帮忙写方案、整理表格、做学习计划,但每次都要把回答复制到 Word 或表格里再调格式的同学和上班族。本文介绍 Gemini 应用里的“生成文件”功能:在对话中直接说明要什么格式,Gemini 会把结果做成可下载的文件&…

2026/10/28 0:07:14

Dify 入门:用 Docker 自部署并搭建知识库问答助手

适合人群:想在自己电脑或服务器上搭一个“能根据内部文档回答问题”的 AI 助手,会一点命令行、装过 Docker,但不想从零写 RAG 代码的同学和开发者。 Dify 是一个开源的 AI 应用开发平台,可以在网页界面里配置模型、知识库、聊天助…

2026/10/28 0:07:13

Cline 入门:在 VS Code 里用 Plan/Act 模式让 AI 帮你写代码

适合人群:已经装了 VS Code,想试试让 AI 直接在项目里读代码、改文件、跑命令,但又担心 AI 乱改代码的编程初学者。本文介绍开源编程智能体 Cline 的基本用法:安装、接入模型、用 Plan/Act 模式完成小项目、用检查点回退。 Cline …

2026/10/28 0:02:13

基于MATLAB的DTW+VAD语音匹配实战:从序列对齐到孤立词识别

简介:动态时间规整(DTW)算法是一种经典的序列匹配方法,专门用于处理长度不同但形态相似的时序数据;它通过构造二维代价矩阵,利用动态规划搜索最小失真路径,使序列可在时间轴上伸缩对齐&#xff…

2026/10/28 0:02:13

C# 身份证图片信息识别源码:从照片到结构化字段的落地路径

简介:这份资源是面向 C# 与 WinForm 开发者的身份证图片信息识别源码工具,适合需要在本机快速实现证件信息提取的初学者与中级开发者。它调用 Windows 自有接口完成图片识别,再解析出姓名、年龄、出生年月日、身份证号码、民族及住址等关键字…

2026/10/27 14:24:03

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/27 0:59:21

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/28 0:02:13

Python股票自动交易系统源码解析:从行情获取到风控的完整链路

简介:这份资源是面向高校计算机、金融相关专业毕业设计场景的完整项目源码包,主题为基于Python的股票自动交易系统,适合需要完成毕设选题、课程设计或自学金融量化交易开发的学生与开发者参考。压缩包共收录687个文件,整体约14.93…

2026/10/28 0:02:13

视酷酷信IM源码部署指南:Java 17+MySQL 8.0.33实战配置

简介:这是一套2024年亲测可部署的视酷酷信IM即时通讯系统源码,面向具备Java、OC、C#及Vue开发能力的技术人员,解决私有化即时通讯平台快速搭建与二次定制需求,尤其适用于需集成商城、保障数据主权的中小型企业或独立开发者。资源共…

2026/10/28 0:02:13

Python爬取百度迁徙数据:接口解析、批量采集与避坑指南

简介:这份资源是一个用Python爬取百度迁徙数据的脚本压缩包,面向具备Python基础、希望入门网络爬虫与数据解析的开发者,解决按日期获取全国热门城市人口迁徙数据的问题。包内共1个文件,为单个py脚本,压缩包约1KB&#…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑