尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
T3 Stack实战:tRPC+Prisma打造全栈TypeScript应用
1. 搞懂t3code之前先说说全栈TypeScript的痛点如果你关注前端技术社区有一阵子应该没少刷到过T3这个词。从t3.gg这个个人网站出发Theo带着他的一套全栈TypeScript组合拳硬是在Next.js生态里杀出了一条极具辨识度的路子。后来大家约定俗成把这套组合叫T3 Stack也就是t3code这个标签背后真正指的东西——Next.js、TypeScript、tRPC、TailwindCSS、Prisma一套从数据库一直摸到UI层的完全TypeScript化开发方案。先说清楚一件事T3不是框架不是轮子更不是一个需要你完整安装的运行时。它是一组工具的选型思路一个怎么把这些东西拼到一起最顺手的实践集合。真正让它在开发者中间传开的原因是它解决了全栈TypeScript开发里最让人头疼的三个问题第一前后端类型割裂。写REST接口的时候前端调fetch拿到的数据和后端res.json()返回的数据本质上没有任何类型关联。后端把字段名改一个前端不报错运行起来才发现undefined这种问题单靠约定根本治不住。第二样板代码太多。REST要定义路由、写controller、配校验、搞文档GraphQL要写schema、resolver、fragment。你还没开始写业务逻辑光是把接口层铺好就已经心力交瘁。第三数据库的类型盲区。ORM虽然能帮你生成一些类型但查询出来的对象、关联关系、筛选条件很多时候依然要靠手写interface或者any兜底一不留神就失去类型保护。T3这套组合的设计哲学就是既然全栈都是TypeScript那就让类型从数据库一路畅通到组件里中途不做任何转换、不写任何胶水代码。写的时候你觉得舒服重构的时候你更会觉得值——改一个Prisma模型的字段名前端组件里直接亮红这种体验是传统前后端分离给不了的。这篇文章不是T3官方文档的翻译更像是我自己从零搭项目、上线、踩坑之后的复盘。如果你正准备用Next.jstRPCPrisma做一个正经的全栈应用或者已经装了脚手架但不知道每一层怎么串联这篇应该能帮你省下不少试错时间。2. 用create-t3-app搭起第一套项目选项取舍与目录结构2.1 脚手架选项怎么选T3栈的入口基本就是create-t3-app一条命令就能把整个骨架拉下来npx create-t3-applatest my-t3-app这里略微提醒一句npx拉取的是最新版本如果你在IDE内置终端里装Node版本最好在18以上不然Prisma的引擎和Next.js的编译链会闹脾气。交互式命令行会问你要不要装tRPC、Prisma、Tailwind、NextAuth等等我第一遍点的时候全选了结果项目里躺了一堆用不上的依赖。实际做项目时我的建议是分场景取舍选项什么时候选什么时候不选tRPC所有前后端交互都走这个必选纯静态站点、没有数据交互Prisma需要持久化数据必选只用本地Mock或第三方APITailwindCSS想要快速出样式、统一设计变量重度依赖组件库如Ant DesignNextAuth需要账号体系可以后面补用自建JWT或第三方登录服务2.2 目录结构里藏着的数据流秘密初始化完的目录第一眼看上去和普通Next.js项目没太大区别但src/server与src/app的分界是T3的精髓src/ ├── app/ # Next.js App Router页面层 ├── pages/api/trpc/ # tRPC的唯一HTTP入口 ├── server/ │ ├── api/ │ │ ├── routers/ # 业务路由比如postRouter、userRouter │ │ └── root.ts # 聚合所有router │ ├── db.ts # Prisma客户端实例 │ └── auth.ts # NextAuth配置如果选了 ├── trpc/ │ ├── server.ts # 服务端tRPC上下文与初始化 │ ├── client.ts # 客户端调用封装 │ └── react.tsx # React Hooks桥接 └── styles/很多新手在这里会犯一个认知错误以为server/api/routers里能随便写任意服务端代码。不是的tRPC的router只负责接口这一件事重逻辑应该拆到router之外的service或lib里router保持薄薄一层职责清晰后面测试也好写。另外要注意t3 code的官网模板默认用App Router而不是老的Pages Router。App Router下的Server Component可以直接调用服务端逻辑甚至不需要走tRPC的HTTP层但客户端交互表单提交、按钮点击、无限滚动还是得挂在trpc/react.tsx导出的Hooks上。理解这两条路径的差别后面写页面时才不会迷茫——能Server端做的事就在Server端做不能的才交给tRPC。2.3 上下文Context里到底放了什么tRPC的createTRPCContext是串联NextAuth、Prisma和请求信息的关键。初始化代码大概长这样export const createTRPCContext async (opts: { headers: Headers }) { const session await getServerAuthSession(); return { db, session, headers: opts.headers, }; };这个context会在每个请求到达时执行一次并且注入到所有procedure的ctx参数里。这意味着你在任何一处业务代码都能通过ctx.db访问数据库、通过ctx.session?.user判断登录态。我在实际项目里几乎每个router都会用到这两个字段所以这里的初始化质量直接影响后面所有接口的编写体验。3. tRPC把接口层彻底类型化的实践3.1 router、procedure与query/mutation的基础概念tRPC的核心抽象只有三个router一组接口的集合、procedure一个接口分query和mutation、middleware中间件。用代码说话更直观// src/server/api/routers/post.ts import { createTRPCRouter, publicProcedure } from ../trpc; import { z } from zod; export const postRouter createTRPCRouter({ latest: publicProcedure .query(async ({ ctx }) { return ctx.db.post.findMany({ orderBy: { createdAt: desc }, take: 10, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1).max(100), content: z.string().min(1), })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: input }); }), });query对应GET型操作读数据mutation对应写操作增删改。注意input用了zod做运行时校验——这是T3栈全链路类型安全的起点。zod既能在运行时拦住非法请求又能让input的类型在前后端同时被推导出来客户端调用时连参数类型都是提示齐全的。3.2 一个完整业务接口的编写过程校验、鉴权、错误处理光会写hello world级别不够我拿一个带鉴权的删除接口举例把细节讲透delete: protectedProcedure .input(z.object({ id: z.string().uuid() })) .mutation(async ({ ctx, input }) { const post await ctx.db.post.findUnique({ where: { id: input.id }, }); if (!post) { throw new TRPCError({ code: NOT_FOUND, message: 文章不存在, }); } if (post.authorId ! ctx.session.user.id) { throw new TRPCError({ code: FORBIDDEN, message: 只能删除自己的文章, }); } await ctx.db.post.delete({ where: { id: input.id } }); return { success: true }; });这里的protectedProcedure来自一个中间件如果ctx.session为空直接抛UNAUTHORIZED。中间件在定义router之前统一声明一次写好全局生效const isAuthed t.middleware(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: ctx.session } }); }); export const protectedProcedure t.procedure.use(isAuthed);几个容易踩的坑我想重点提一下TRPCError的code不全是HTTP状态码前端拿到的是统一的error.message所以在message里写详细原因比依赖状态码靠谱。权限校验一定要在数据库操作之前做顺序反了就会出现先查库再判断权限慢且不安全。zod的uuid()校验很严格如果前端传的是自增数字ID记得改用z.number().int().positive()不然会一直报Invalid input。3.3 对比REST和GraphQL什么时候选它最划算我身边有朋友问既然有REST和GraphQL为什么还要用tRPC我的答案很直接如果你前后端都是TypeScript且部署在一台服务器上Next.js应用本身就同时承载前后端tRPC是成本最低的方案。REST的问题在于类型边界要靠工具生成OpenAPI codegen而且每一个资源都要考虑URL设计、HTTP方法语义项目一大文档和代码就经常对不上。GraphQL的Schema更灵活但引入Apollo一套东西学习曲线和运行时复杂度都不小小团队很难hold住。tRPC的逻辑很简单没有独立Schema文档没有codegen类型就是代码本身。前端引入的是同一个类型定义调用时IDE直接给全提示连接口文档过期这种问题都不存在。代价就是你被绑死在TypeScript生态里了——如果哪天有第三方用Python/Go来调你的接口tRPC这套就不那么友好了一般建议用tRPC做内部接口外部真正对外开放的API再用REST单独包一层。4. Prisma与数据库让SQL也变成类型安全的体验4.1 schema建模与迁移的工作流细节Prisma在这个栈里扮演的是数据库访问层的角色。第一次用的时候我最喜欢的是它的schema文件一眼就能看完整套数据模型model User { id String id default(cuid()) email String unique name String? posts Post[] createdAt DateTime default(now()) } model Post { id String id default(cuid()) title String content String author User relation(fields: [authorId], references: [id]) authorId String published Boolean default(true) createdAt DateTime default(now()) }改完schema之后的固定动作是npx prisma db push # 开发环境直接同步到数据库 npx prisma migrate dev # 开发环境生成迁移文件并应用 npx prisma migrate deploy # 生产/CI环境执行已有迁移这里我强烈建议用migrate dev而不是db push哪怕是在本地。db push虽然快但不会生成迁移历史时间一长生产环境的库和本地schema没法对账部署时一脸懵。迁移文件是团队的资产能回滚、能review别嫌麻烦。4.2 关联查询、分页、筛选的常见写法Prisma的查询API是我用过的ORM里最接近直觉的。一对多关联查询const userWithPosts await ctx.db.user.findUnique({ where: { id: userId }, include: { posts: true }, });需要分页和筛选时组合条件写起来也很清晰const posts await ctx.db.post.findMany({ where: { published: true, title: { contains: keyword, mode: insensitive }, }, include: { author: { select: { name: true } } }, orderBy: { createdAt: desc }, skip: (page - 1) * pageSize, take: pageSize, });有几个细节值得留意mode: insensitive只对PostgreSQL和MySQL有效SQLite不支持本地开发如果用的SQLite关键词搜索会变成大小写敏感测试和线上行为不一致。include一口气把关联数据拉出来很爽但要注意N1问题。一个列表10条文章每条文章都include作者Prisma会把它生成一条带JOIN的查询问题不大但如果关联的是两层以上的嵌套最好用select按需取字段减少传输量。大表分页建议用cursor游标而不是skip/take否则页码越深越慢。Prisma的cursor做的是基于唯一索引的定位性能稳定得多。4.3 类型推导如何贯穿前后端Prisma生成类型的方式是prisma generate它会把schema里所有模型变成prisma/client导出的TypeScript类型。T3栈里tRPC的procedure返回值不会做二次断言而是直接推导ctx.db的结果类型。这带来一个非常爽的体验前端postRouter.latest.useQuery()拿到的data就是Post[]且Post的字段和author关联关系全部有类型你在前端写data.author.name如果字段不存在编译期直接报错。从数据库到组件没有任何一处手写接口类型。这就是tRPCPrisma组合的杀手锏。5. TailwindCSS的样式开发与性能控制5.1 原子化CSS的真实开发体验作为一个常年写CSS的人我一开始对Tailwind是持怀疑态度的——不就是内联样式换个写法吗真正在T3项目里用下来才发现它最大的价值不是省代码而是把样式决策压缩到了最小单位。写一个卡片组件不用纠结类名叫card-container还是card-wrapper直接在JSX里堆类名div classNamerounded-lg border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow h2 classNametext-lg font-semibold text-gray-900{post.title}/h2 p classNamemt-2 text-sm text-gray-600 line-clamp-2{post.content}/p /div习惯了这种写法之后回到传统CSS反而觉得累起名字、找文件、切上下文。尤其T3这类全栈项目逻辑层代码已经不少样式层面能少一个心智负担是一个。5.2 主题定制与暗色模式落地Tailwind在T3项目里默认配置了tailwind.config.ts要加主题变量很直接import type { Config } from tailwindcss; export default { darkMode: class, theme: { extend: { colors: { brand: { 500: #6366f1, 600: #4f46e5, }, }, }, }, } satisfies Config;暗色模式我现在统一用darkMode: class而不是默认的media。原因是用户手动切换明暗的诉求在现代Web应用里太常见了靠系统偏好一锤定音不现实。配合Next.js的next-themes库几行就能搞定持久化和闪屏问题。样式文件里只要出现dark:bg-gray-900这类变体就会自动因html上的dark类切换。5.3 JIT编译与打包体积的注意事项Tailwind的新版本默认JIT编译也就是只生成你在代码里用到的类。但有一个坑如果你在数据库里存了颜色值然后在前端动态拼接类名比如bg-${color}-500JIT是扫不到这个字符串的编译出来这个类就不存在。解决方法是把完整的类名写进配置的safelist或者干脆用内联样式处理动态颜色。另外T3项目的页面如果是纯SSR且没多少按钮弹窗Tailwind的CSS体积一般能压在10KB以内但如果引入了第三方组件库比如Headless UI要注意按需导入否则样式表和JS都会被撑大。6. 部署上线与踩坑实录6.1 环境变量、迁移命令在CI/CD中的执行顺序T3项目部署到Vercel或者自己的服务器环境变量是第一个杀手。特别提醒.env和.env.example的约定要遵守好新人最容易犯的错是把DATABASE_URL写成localhost就提交了或者线上环境没有配NEXTAUTH_SECRET导致登录一直失败。正常CI/CD里的执行顺序应该是安装依赖跑prisma generate跑prisma migrate deploy构建next build部署产物顺序不能乱。next build过程中Next.js会尝试静态渲染一部分页面如果你的Server Component里调用了prisma但这时还没generate构建直接报错。migrate deploy必须在build之前跑因为构建时可能就需要连数据库查数据。6.2 生产环境的数据库连接池与SSR并发本地开发连数据库毫无压力一上生产就开始报too many connections这个问题我见过太多次了。Prisma默认的连接池大小是num_cpus * 2 1在Serverless环境Vercel函数里每个函数实例都会单独创建连接池并发一高数据库连接数直接爆掉。解决思路有三个按优先级排序数据库端设置合适的max_connections并把Prisma连接池上限调小比如?connection_limit5。用PgBouncer这类连接池代理专门解决Serverless短连接问题。避免在Server Component里高频执行数据库查询把重查询挪到tRPC的query里方便统一控制和缓存。6.3 我实打实遇到过的几个坑第一个坑NextAuth的session在tRPC context里是异步的。getServerAuthSession()返回的是Promise如果在createTRPCContext里忘了await后面所有protectedProcedure都会拿到空的session页面一直跳登录排了半天才发现是这种低级问题。第二个坑zod的error默认不友好。前端提交非法表单时默认的报错信息是英文的一长串路径描述。我后来统一在入口加了一层格式化中间件把zodError转成字段级别的中文提示体验立刻不一样。第三个坑Prisma在Next.js构建时的警告。如果你在代码顶层直接new PrismaClient()开发时热更新会导致实例反复创建官方推荐用globalThis缓存const globalForPrisma globalThis as unknown as { prisma?: PrismaClient }; export const db globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV ! production) globalForPrisma.prisma db;这段代码几乎是T3项目的标配直接抄进src/server/db.ts就行。7. 进阶方向与学习路径建议7.1 性能优化从查询优化到加载态设计基础跑通之后我建议把性能优化分成两层来做。数据层优先看tRPC的query缓存。tRPC的react客户端配合react-query默认就有缓存能力但很多人没设置staleTime导致同一接口在多个组件里调用时重复请求。按业务设置五秒到一分钟的staleTime能显著降低数据库压力。展示层学会拆分useQuery的数据粒度。比如帖子详情页标题和正文是强关联的一起查没问题但侧边栏的热门标签是低频变化数据建议单独一个query设置更长的缓存时间。T3文档里推荐的按数据变化频率设计接口粒度这个思路越早领会后面优化越省力。7.2 测试策略类型安全不等于没有bug类型安全解决的是调用错的问题解决不了逻辑错的问题。T3项目里我建议至少覆盖三层测试zod校验层输入边界、异常输入这是最便宜、最有效的测试。tRPC router集成测试用createCallerFactory直接调用router不需要起HTTP服务速度很快。关键页面组件的交互测试用VitestTesting Librarymock掉tRPC的query即可。// router集成测试示例 import { createTRPCContext } from ../src/server/api/trpc; import { appRouter } from ../src/server/api/root; test(未登录用户创建文章应报错, async () { const ctx await createTRPCContext({ headers: new Headers() }); const caller appRouter.createCaller({ ...ctx, session: null }); await expect(caller.post.create({ title: hi, content: hello })) .rejects.toThrow(UNAUTHORIZED); });7.3 从T3出发下一步的技术扩展T3栈是一个很好的起点但业务复杂到一定程度会有几个自然的扩展方向引入消息队列或后台任务Prisma只管同步操作异步任务建议交给别的队列服务别硬塞进tRPC的mutation里。文件上传本地磁盘不可靠尽早接入对象存储tRPC只管返回上传凭证。多服务拆分如果团队开始写Python/Go服务tRPC只做内部接口层外部API单独用REST或gRPC暴露。实时功能tRPC本身不提供WebSocket可以用socket.io或SSE互补注意鉴权要复用同一套session。说实话T3这套技术选型并不完美它也牺牲了对外部生态的兼容性换取的是全栈TypeScript的极致内聚。我个人实操下来的体会是中小型全栈项目用它开发速度是真的快迭代一个版本可能只需要别人一半的时间项目变大之后也不需要推翻重来该补的模块化、缓存、队列按需加就行。如果你还在犹豫要不要入坑我建议直接拿一个真实的小需求跑一遍完整流程——从建模型、写router、调UI到部署上线半天就能感受到这套组合拳的威力。
RELATED

相关推荐

STM32驱动Si4703 FM收音机芯片:从I2C时序到搜台避坑实战

STM32驱动Si4703 FM收音机芯片:从I2C时序到搜台避坑实战

简介:一套完整的基于STM32微控制器与Si4703 FM收音机芯片的嵌入式项目资源,面向有一定单片机基础、希望学习FM收音机驱动或I2C/SPI通信开发的电子爱好者与工程师,也可用于嵌入式课程设计或毕业设计参考。资源包共15个文件,约97KB&…

📅 2026/10/27 16:21:26
Qwen3.6 Plus多模态能力测试与优化实践

Qwen3.6 Plus多模态能力测试与优化实践

1. Qwen3.6 Plus多模态能力初探 最近在测试Qwen3.6 Plus的多模态功能时,发现这个模型在图像理解、跨模态推理方面展现出不少亮点。作为一款国产大模型,它的多模态能力到底能达到什么水平?今天我就用实际测试案例带大家一探究竟。 测试环境准…

📅 2026/10/27 16:21:26
51单片机智能浇花系统:从传感器选型到Proteus仿真调试全攻略

51单片机智能浇花系统:从传感器选型到Proteus仿真调试全攻略

简介:一套面向51单片机课程设计与毕业设计的智能浇花系统完整方案,主控采用STC89C52,配合土壤湿度传感器、DHT11温湿度传感器、ADC0832转换芯片、LCD1602液晶显示、水泵控制与按键设置电路,支持手动、自动两种浇灌模式。自动模式下…

📅 2026/10/27 16:21:26
MORE NEWS

更多资讯

📰

非线性光学仿真核心:相位匹配与RK4求解从倍频到OPO/OPA

简介:面向非线性光学仿真与数值计算的完整工程资料包,适合光学工程、物理及相关专业的学生与研究人员,侧重二次谐波产生、参量下转换、四波混频等典型非线性效应,覆盖光场传播、相位匹配、材料极化率与转换效率计算等核心问题。压…

📰

inSSIDer 2.0无线勘测实战:信道规划、干扰排查与数据复核

简介:inSSIDer 2.0 是一款面向 Windows 平台的免费 Wi-Fi 分析工具,适合家庭用户、网络运维初学者及小型办公场景中需要排查无线问题的使用者。它能实时监测周边无线网络的信号强度,分析各频段频道拥挤程度并给出换道建议,识别开放…

📰

Perona-Malik扩散模型Matlab修改版:保边去噪算法与实现

简介:Perona-Malik 扩散模型的修改版 Matlab 代码,面向图像处理、计算机视觉方向的大学生与科研人员,适用于课程设计、期末大作业和毕业设计中的去噪与边缘保持实验。代码基于偏微分方程热扩散思想,在原始模型上调整了扩散函数与迭…

📰

MATLAB调Q光纤激光器仿真:刚性方程建模与数值求解实战

简介:本资源是一套面向本科及硕士阶段科研学习者的调Q光纤激光器Matlab仿真教学包,聚焦光学与激光物理建模,适用于光电信息、激光技术、非线性光学等方向的课程设计、课题入门与算法验证。压缩包共8个文件,含5张关键仿真结果图&am…

📰

Python爬取搜狗微信搜索:公众号数据采集与反爬实战指南

简介:面向微信公众号数据采集入门者与爬虫开发者,这套资源围绕搜狗微信接口实现公众号基础信息与历史文章抓取,覆盖接口调用、参数处理、返回解析与异常规避等关键环节,可帮助读者快速搭建一个轻量级采集原型。包内共三个文件&…

📰

副队长JavaScript教程(49)-主动抛出异常与错误类型

点此查看 所有教程、项目、源码导航 文章目录1. 背景2. throw 抛出异常2.1 throw 的作用2.2 基础示例2.3 什么时候需要 throw3. JS 内置错误类型3.1 Error3.2 ReferenceError 引用错误3.3 TypeError 类型错误3.4 RangeError 范围错误4. 异常处理常见坑4.1 不要无脑捕获所有异常…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬