news 2026/10/27 16:21:24

T3 Stack实战:tRPC+Prisma打造全栈TypeScript应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.js+tRPC+Prisma做一个正经的全栈应用,或者已经装了脚手架但不知道每一层怎么串联,这篇应该能帮你省下不少试错时间。

2. 用create-t3-app搭起第一套项目:选项取舍与目录结构

2.1 脚手架选项怎么选

T3栈的入口基本就是create-t3-app,一条命令就能把整个骨架拉下来:

npx create-t3-app@latest my-t3-app

这里略微提醒一句,npx拉取的是最新版本,如果你在IDE内置终端里装,Node版本最好在18以上,不然Prisma的引擎和Next.js的编译链会闹脾气。

交互式命令行会问你要不要装tRPC、Prisma、Tailwind、NextAuth等等,我第一遍点的时候全选了,结果项目里躺了一堆用不上的依赖。实际做项目时,我的建议是分场景取舍:

选项什么时候选什么时候不选
tRPC所有前后端交互都走这个,必选纯静态站点、没有数据交互
Prisma需要持久化数据,必选只用本地Mock或第三方API
TailwindCSS想要快速出样式、统一设计变量重度依赖组件库,如Ant Design
NextAuth需要账号体系,可以后面补用自建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一口气把关联数据拉出来很爽,但要注意N+1问题。一个列表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如果字段不存在,编译期直接报错。从数据库到组件,没有任何一处手写接口类型。这就是tRPC+Prisma组合的杀手锏。

5. TailwindCSS的样式开发与性能控制

5.1 原子化CSS的真实开发体验

作为一个常年写CSS的人,我一开始对Tailwind是持怀疑态度的——不就是内联样式换个写法吗?真正在T3项目里用下来才发现,它最大的价值不是省代码,而是把"样式决策"压缩到了最小单位。写一个卡片组件,不用纠结类名叫card-container还是card-wrapper,直接在JSX里堆类名:

<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow"> <h2 className="text-lg font-semibold text-gray-900">{post.title}</h2> <p className="mt-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}-500`,JIT是扫不到这个字符串的,编译出来这个类就不存在。解决方法是把完整的类名写进配置的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里的执行顺序应该是:

  1. 安装依赖
  2. 跑prisma generate
  3. 跑prisma migrate deploy
  4. 构建next build
  5. 部署产物

顺序不能乱。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_limit=5。
  • 用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服务,速度很快。
  • 关键页面组件的交互测试:用Vitest+Testing Library,mock掉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到部署上线,半天就能感受到这套组合拳的威力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 16:18:11

基于JSP+SSM的农特产品电商系统开发实践

1. 项目概述&#xff1a;汉中农特产品销售系统的设计与实现汉中地处秦岭南麓&#xff0c;独特的地理环境孕育了丰富的农特产品资源。这个基于JSPSSM框架的农特产品销售系统&#xff0c;正是为了解决当地特色农产品线上销售渠道匮乏的问题而设计。系统采用B/S架构&#xff0c;整…

作者头像 李华
网站建设 2026/10/27 16:17:10

手机AI动漫创作:开源工具链与移动端优化方案

1. 项目概述&#xff1a;当AI导演遇上手机创作去年夏天&#xff0c;我在B站刷到一条用AI生成的《灌篮高手》续集漫剧&#xff0c;短短3天播放量破百万。更惊人的的是&#xff0c;作者简介里写着"全程用手机制作&#xff0c;零美术基础"。这个案例彻底颠覆了我对内容创…

作者头像 李华
网站建设 2026/10/27 16:15:39

大模型推理并行策略:DP、TP、PP、SP与EP详解

1. 大模型推理并行策略概述在当今AI领域&#xff0c;大模型推理已经成为技术前沿的核心挑战之一。当模型参数量突破十亿甚至千亿级别时&#xff0c;单张GPU的显存容量和计算能力往往难以满足需求。这时&#xff0c;并行计算策略就成为了解决问题的关键钥匙。我曾在多个实际项目…

作者头像 李华
网站建设 2026/10/27 16:15:05

从零手写GCN:PyTorch实现图卷积神经网络与半监督节点分类

简介&#xff1a;本资源为基于PyTorch框架手动搭建GCN图卷积神经网络的完整项目包&#xff0c;面向计算机、人工智能、数据科学等专业学生及需要完成课程设计、毕业设计或大作业的开发者。内容围绕Cora与Citeseer两个图数据集&#xff0c;覆盖节点分类与链路预测两大任务&#…

作者头像 李华
网站建设 2026/10/27 16:14:42

t3code 深度解析:Electron 桌面端与 CLI 工具链的跨平台开发实践

1. 从 t3code 这个关键词说起&#xff1a;它到底指什么第一次看到 "t3code" 这个词&#xff0c;很多人会一头雾水。它不像 "vscode" 那样有明确的官方定义&#xff0c;也不像某个知名框架那样有完整的文档站。从热词组合来看&#xff0c;t3code 更像是一个…

作者头像 李华
网站建设 2026/10/27 16:14:41

XPath 2.0核心功能解析与实战应用指南

1. XPath 2.0 核心功能解析XPath 2.0作为XML路径语言的重大升级版本&#xff0c;在数据处理能力和表达式复杂度上都实现了质的飞跃。与1.0版本相比&#xff0c;最显著的特征是引入了强类型系统和序列处理模型。在实际项目中&#xff0c;我经常需要处理包含数百万节点的XML文档&…

作者头像 李华