首页 / 资讯中心 / 文章详情

t3code:用TypeScript、tRPC和Prisma实现全栈端到端类型安全

t3code:用TypeScript、tRPC和Prisma实现全栈端到端类型安全 ★ FEATURED ARTICLE
最近有个项目叫 t3code我拿到手第一反应是这名字取得太“预告片”了。T3 指的是社区里那套以 TypeScript、Tailwind、tRPC 为核心的全栈技术组合code 就直接告诉你这是个代码仓库。但真正让我愿意花一整天去扒它的原因是它把“端到端类型安全”这件事做到了从数据库到页面组件全程贯穿而不是像很多脚手架那样只是搭了个壳。如果你也是那种受够了前后端联调时反复对字段、对类型、对接口文档的开发者t3code 这套玩法值得认真摸一遍。这篇就按我实际操作的顺序把这个项目从选型逻辑到部署细节完整拆开。1. 项目概述t3code 到底是做什么的1.1 一句话理解 t3codet3code 不是某个公司出的商业产品它本质上是 T3 Stack 技术栈的实战代码集合。T3 Stack 最早来自 create-t3-app 这个脚手架核心组合是 Next.js TypeScript Tailwind CSS tRPC Prisma可选配 NextAuth.js 做认证。t3code 则是在这个底座上把真实业务里会碰到的用户体系、数据建模、接口封装、服务端渲染、部署配置都串了起来形成一份可以跑起来的全栈代码库。我初步扫了一遍它的主要模块发现它解决了一个特别实际的问题全栈应用里最高频的 30 个重复操作比如“创建一个带权限的接口”“从数据库拿数据渲染到页面上”“处理表单提交错误”都被封装成了可直接复用的模式。你不需要每次从零思考路由怎么设计、鉴权怎么拦截、数据库模型怎么建直接按 t3code 的约定往里面填业务就行。1.2 适合谁不适合谁说句实在话这个项目对人群的筛选是比较明确的。如果你是刚学会 React、想试试全栈开发t3code 对你来说可能有点重因为它默认你已经知道 Next.js 的 App Router、知道 tRPC 为什么能省掉 REST API 的维护成本、知道 Prisma 的 migration 流程。反过来如果你已经写过几个全栈项目但一直被“接口类型对不上”“数据库字段改了前端一片飘红”“认证逻辑散落在各个页面”这类问题折磨那 t3code 几乎就是为你准备的。它不适合用来快速拼一个 demo更适合当一份持续的代码底座。我自己用了两个月之后最深的感受是它把很多团队靠文档和自觉才能守住的规范直接固化到了项目结构里。当类型是自动推导的、API 路径是自动生成的、数据库字段是单一来源的很多低级 bug 根本不给你出现的空间。1.3 我为什么愿意长期维护这个项目维护 t3code 的过程里我反复被验证一个观点全栈开发真正的效率瓶颈不在写代码而在上下文切换。前端写页面时要去看接口返回什么后端写接口时要回头查数据库字段这种切换一次两次没事每天几十次就是在持续消耗精力。t3code 把所有数据链路统一到 TypeScript 类型系统里之后我从页面里直接就能知道数据库里有什么从 router 里直接就能知道页面会收到什么这种确定性是我愿意持续维护它的核心理由。2. T3 技术栈选型解析为什么偏偏是这几个工具2.1 Next.js 作为地基的理由T3 Stack 把 Next.js 放在最底层不是因为它最流行而是因为它在“服务端能力”和“前端体验”之间给了最顺滑的切换方式。t3code 里大量用到 App Router 的 Server Component 和 Route Handler尤其是数据获取场景可以直接在服务端调用 tRPC 查询数据库然后把结果传给页面组件。这个模式避免了以往 React 应用常见的“先 loading 再请求再渲染”三步走页面首屏拿到的就是完整数据。相比 Vite React 那种纯客户端方案Next.js 在 t3code 里承担了两个额外职责一是 API 路由的统一出口二是服务端渲染时的数据预取。举个我实际改过的例子在做一个公开的列表页时我只需要在 Server Component 里直接await postRouter.list()数据就已经在 HTML 里了用户根本看不到任何 loading 态。2.2 TypeScript不是语言而是契约很多项目把 TypeScript 当成“加了类型标注的 JavaScript”但在 t3code 里它的地位完全不同。这里的 TypeScript 不是写给别人看的注释而是全栈共享的契约。前端调用trpc.post.list.useQuery()返回值的类型是从数据库模型直接推导出来的后端改一个字段前端编辑器立刻报错。我经常跟人打个比方这相当于前后端之间有一根永远对齐的数据管道信号不会丢也不会串。最直观的收益是删除代码时的安全感。在传统项目里你删掉一个接口字段得靠全局搜索一个个确认调用方还不一定找得全。在 t3code 里字段删掉所有用到它的地方全部报红你只要跟着编译错误走一圈就完成了重构。这不是什么黑魔法就是类型系统一直在做的事只是 t3code 把它的覆盖面拉到了全栈。2.3 tRPC 和 Prisma一个省接口一个省模型tRPC 是 t3code 里最值得细说的一个选型。传统开发要写 REST 接口得定义 URL、定义请求方法、定义响应结构、写接口文档然后前端还要照着文档再写一遍类型定义。tRPC 的思路是不走 HTTP 语义那套直接在服务端定义函数叫 procedure前端像调用本地函数一样调用它。由于服务端和前端都跑在同一个 TypeScript 类型系统里返回类型直接封装进调用链省掉了整个接口维护层。Prisma 则负责把数据库表结构变成 TypeScript 类型。它在 schema 文件里描述模型然后自动生成类型安全的查询客户端。t3code 里的数据链路是这样的Prisma Schema 定义模型tRPC Router 读取模型并暴露函数前端组件调用函数并自动获得类型。三个环节每两个之间都靠类型对齐这就是“端到端类型安全”的实际含义也是 t3code 和普通 MERN 项目最本质的区别。3. 实操搭建从初始化到跑出第一个页面3.1 用 create-t3-app 初始化项目如果你也想复现 t3code 这套结构我建议直接用官方脚手架起步没必要手搭。运行下面这条命令npm create t3-applatest它会进入一个交互式选择界面让你勾选需要的模块。我每次都会勾上 NextAuth、Prisma、Tailwind、tRPC。这里有个小建议如果暂时用不到认证也别急着去掉 NextAuth因为 t3code 里的很多鉴权模式都要靠它演示后续想加回来反而麻烦。创建完成后的目录结构会清晰标明各模块的职责最核心的两个目录是src/server和src/app。前者存放数据库访问、API 路由、鉴权逻辑后者存放页面和组件。这种分层让我在维护项目时几乎不用猜代码位置——数据库相关的一律进 server 目录页面相关的一律进 app 目录想污染都找不到地方。3.2 数据建模与初始化迁移接下来做数据建模。t3code 里我定义了一个典型的Post模型带作者关联和创建时间model Post { id String id default(cuid()) title String content String published Boolean default(false) author User relation(fields: [authorId], references: [id]) authorId String createdAt DateTime default(now()) updatedAt DateTime updatedAt }写完模型后执行迁移npx prisma migrate dev --name init这条命令会在数据库里建出真实表结构同时生成 Prisma Client。注意它还会自动生成 TypeScript 类型之后你在 tRPC router 里写逻辑时Prisma.Post这个类型就已经带着完整的字段约束了。我自己的习惯是每改一次 schema 就立刻执行迁移避免模型和代码之间出现偏差。3.3 用 tRPC 写出第一个带类型安全的路由有了模型之后我直接在 router 里暴露一个查询函数。t3code 里典型的做法是定义一个postRouter把所有与 Post 相关的操作收拢在一起// src/server/api/routers/post.ts import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ../trpc; export const postRouter createTRPCRouter({ listPublished: publicProcedure.query(({ ctx }) { return ctx.prisma.post.findMany({ where: { published: true }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), }) ) .mutation(({ ctx, input }) { return ctx.prisma.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });这里有个细节值得展开publicProcedure表示无需登录就能调用protectedProcedure则会强制校验 session不合法直接抛错。这层鉴权不是加在页面上而是在 API 层意味着任何人绕过界面直接调接口也会被拦下来。安全性不由前端决定这比很多项目只在页面上藏按钮要扎实得多。3.4 前端组件里直接消费接口t3code 前端调用接口的方式是它最有感染力的地方。我在页面组件里这样写use client; import { api } from ~/utils/api; export default function PostList() { const { data, isLoading } api.post.listPublished.useQuery(); if (isLoading) return div加载中.../div; return ( ul {data?.map((post) ( li key{post.id} h2{post.title}/h2 p{post.content}/p /li ))} /ul ); }没有手动拼接 URL没有定义 interface 描述响应结构没有处理 400/500 错误码的分支。data的类型直接继承自 Prisma 模型字段名写错了编辑器当场报错。这也是我第一次用 t3code 时最震撼的地方前后端联调竟然真的可以做到零文档、零猜谜。4. 核心机制拆解认证、鉴权与数据流4.1 NextAuth 在 t3code 里的角色认证是绝大多数全栈应用绕不开的环节。t3code 里集成的是 NextAuth.js配置集中在src/server/auth.ts。你可以直接对接 GitHub 或 Google 登录也可以扩展 CredentialsProvider 实现邮箱密码登录。我基于它写过一个本地账号体系核心只是在 providers 数组里加了一个自定义 provider然后在 authorize 回调里校验数据库里的用户。t3code 的做法有个聪明的地方session 信息不是在前端手动读取而是通过 tRPC 的 context 注入到每个请求里。这意味着你的业务代码只要写ctx.session.user就能拿到当前登录用户完全不需要在组件里再做useSession()再传参这一套。session 状态和业务数据在同一个请求上下文里逻辑连贯度提升非常明显。4.2 protectedProcedure 如何做到全局鉴权鉴权最怕的是“漏网之鱼”。常规项目往往是在某一个页面里做个判断if (!session) redirect(/login)但接口没受保护懂技术的人直接发请求就能绕过去。t3code 里的 protectedProcedure 是在 tRPC 层统一做校验任何 procedure 只要声明为 protected没有合法 session 一律拒绝执行。export const protectedProcedure t.procedure.use(({ ctx, next }) { if (!ctx.session || !ctx.session.user) { throw new Error(未登录); } return next({ ctx: { session: { user: ctx.session.user }, }, }); });这段中间件逻辑看起来简单但它把所有鉴权决策收敛到了一个文件里。后续如果需要增加管理员角色、VIP 会员之类的身份校验只需要在这个中间件或者再包一层 procedure 里扩展而不是跑去每个页面里补判断。我实际修改过一次权限模型只改了两个文件就完成了所有接口的角色控制。4.3 类型安全的完整闭环怎么运转把 Prisma、tRPC、React 组件串起来之后数据流的完整路径是Prisma Schema 是唯一真相它决定数据库长什么样Prisma Client 根据 Schema 生成类型tRPC router 的返回值带着这些类型前端api对象又是从 router 类型自动推导的。只要底层的 Prisma 模型不变中间所有环节的类型就是自洽的如果有人硬改中间某一步的类型定义TypeScript 会毫不犹豫地报错。这个闭环的实际价值我在重构一个老接口时体会最深。当时我要把 Post 的published字段从布尔值改成三态枚举原本以为要改十几个文件。实际动手才发现我只需要改 Prisma Schema 和与之对应的 zustand 输入校验其余所有用到这个字段的页面全部自动响起了类型警报顺着编译错误逐个处理就行。这次重构的用时比预想少了一半不止。5. 实操中的高频问题与排查技巧5.1 Prisma 类型不更新前端一直报错这是我刚上手 t3code 时第一个遇到的坑。改了 schema 文件执行了 migrate但前端依然看不到新类型编辑器一直提示找不到属性。原因在于 Prisma Client 是生成到node_modules/.prisma目录的你的 TypeScript 服务可能还在跑旧缓存。解决办法是执行生成命令并重启 TS Servernpx prisma generate然后在 VSCode 里按CtrlShiftP输入 “TypeScript: Restart TS server”。大多数情况下新鲜生成的类型会在重启后立刻被识别。如果还是不行删掉.next目录再跑next dev基本能解决所有缓存类怪问题。5.2 Tailwind 样式不生效类名堆上去没反应这个问题通常和content配置有关。Tailwind 需要扫描源码文件来生成对应的 CSS如果配置里没有包含新加的目录那类名就变成了“死代码”。t3code 默认配置是扫描./src/**/*.{ts,tsx}正常情况下不会漏但如果你把组件拆分到了项目目录外的共享包就要手工补充路径。检查tailwind.config.tsexport default { content: [./src/**/*.{ts,tsx}, ../../packages/**/*.{ts,tsx}], };顺便提醒一句T3 项目里经常会有人为了让某个样式比较特殊而直接在style标签里写原生 CSS结果发现优先级和 Tailwind 冲突。这类问题排查起来比配置问题痛苦得多我的建议是别绕开 Tailwind直接用它表达设计。5.3 tRPC 调用返回 404路由明明写对了tRPC 的 API 路径是通过appRouter自动挂载到 Next.js Route Handler 的。你不需要手动设置任何 URL但这意味着一旦你改了 router 的名字或者目录结构旧的访问路径会失效。页面发起请求时如果拿到 404先别急着眼花缭乱找路由检查一下src/app/api/trpc/[trpc]/route.ts是否存在。这个文件相当于 tRPC 和 Next.js 之间唯一的桥接点它被删掉或者路径变了整个 tRPC 功能都会失效。我之前有一次为了梳理目录清理了src/app/api下面看似没用的文件结果所有接口直接变成 404恢复文件后立刻正常。熟悉这个机制之后这类问题基本一眼就能定位。5.4 部署到线上后数据库连不上本地跑得好好一部署到 Vercel 就报数据库连接失败这是 t3code 用户最容易踩的坑。根因通常是环境变量没有正确同步到部署平台。注意你的.env文件默认被.gitignore排除不会进仓库所以必须在部署平台的后台手动添加DATABASE_URL、NEXTAUTH_SECRET等变量。另外一个容易被忽略的点是数据库 IP 白名单。如果你的数据库厂商开启了访问控制需要把 Vercel 的部署区域 IP 加进去。我自己用 Neon 时遇到过这个问题测试环境能连生产环境偶发断开后来才发现是连接池配置和 IP 白名单组合的问题。把这些变量都核对一遍再部署第二遍基本就通过了。5.5 常见问题速查表现象大概率原因处理方式类型不更新Prisma Client 缓存npx prisma generate 重启 TS ServerTailwind 类名无效content 路径没覆盖检查 tailwind.config.ts 的 contenttRPC 返回 404Route Handler 缺失确认[trpc]/route.ts存在页面一直 loadingquery 抛错未捕获打开控制台看请求错误检查 session部署后数据库报错环境变量缺失或白名单未加在部署平台补齐变量并检查数据库白名单热更新后接口丢了模块缓存问题重启next dev必要时删.next6. 部署与性能优化让 t3code 跑得稳也跑得快6.1 用 Vercel 完成一键部署t3code 作为 Next.js 项目部署到 Vercel 的体验几乎是无缝的。只要把代码推到 GitHub然后在 Vercel 里导入仓库它会自动识别这是 Next.js 应用并执行构建。你需要做的只有一件事在环境变量面板里把本地.env里的所有键值补充上去。构建之前建议先在本地跑一遍生产构建确保没有未解决的编译错误npm run build如果构建通过部署流程大概率没有意外。但如果发现构建时间过长去看一下是不是 Prisma 在每次构建时都在生成客户端。生产环境下可以让 Prisma 使用缓存生成减少一部分耗时。6.2 数据库连接方式的选择t3code 默认的数据库需要你自行把DATABASE_URL指向一个真实的 PostgreSQL 实例。我试过本地 Docker 和云数据库两种方式个人经验是云数据库会让开发和联调省心很多尤其是多人协作时大家共用一套库比各自维护数据库版本要方便。用 Neon 或者 Railway 这类服务时注意看一下连接串里是否区分 direct 和 pooler 模式。Prisma 在有连接池的场景下需要用到?pgbouncertrue参数否则连接容易超时。这个细节不写进文档的话够你排查一个下午。6.3 性能优化tRPC 的下一个关注点t3code 的类型安全让它非常好维护但性能上也要有几个意识。第一个是 database query 本身Prisma 的查询一定要用好include和select避免一次拉回整个表的大字段。优先按页面真正渲染的内容去裁剪查询返回减少传输体积。第二个是 React Query 的缓存策略。tRPC 默认基于 React Query你可以给 query 配置staleTime让不太变化的数据减少重复请求api.post.listPublished.useQuery(undefined, { staleTime: 60 * 1000, });比如列表页数据每 60 秒内不重新请求体验上页面更加跟手后端压力也小不少。这些优化在数据量大之后差距会非常明显。7. 扩展思路从 t3code 还可以长出什么7.1 从单体到模块化保持类型安全的边界t3code 给了一个坚实的起点但它绝不是终点。如果你要在这个底板上做更大的项目可以考虑把 tRPC router 按业务拆分成独立子应用再用 npm workspace 管理共享类型包。把 Prisma schema、类型定义、工具函数抽到一个独立包里这样前后端各自引用同一个数据真相不会出现两套类型不一致的可笑局面。我目前就在尝试把 t3code 扩展成一个包含用户中心、内容管理和后台统计的中型应用结构基本还是沿用它的分层思想只是把每个业务域都塞进了各自的 router 和页面目录里。目前跑下来加新功能时几乎不会惊动旧代码。7.2 它背后的工程哲学最后说点虚的但我觉得是这类项目真正的价值。t3code 教会我的不是某个具体库怎么用而是一种“让结构替你挡问题”的工程哲学。每当你发现自己在重复处理某类低级问题比如接口类型对不上、数据权限漏校验、字段命名不统一就该停下来在结构层面加一道闸。把这些检查交给编译器、交给类型系统、交给统一的中间件你才有精力去写真正有业务价值的代码。
阅读完成 · 觉得有帮助?
咨询建站