ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

全栈类型安全新范式:用T3 Stack与tRPC告别前后端联调之痛

全栈类型安全新范式:用T3 Stack与tRPC告别前后端联调之痛 以前搭全栈项目最让我头疼的从来不是功能复杂度而是前后端之间那层反复扯皮的类型定义。后端改了字段名前端编译直接一片红更别提写一整套 API 封装、手搓类型声明、再对着文档排查接口字段的日常。后来接触到 t3code 这套基于 T3 Stack 的脚手架工作流我最大的感受是它不是在帮你写代码而是在重构你组织全栈代码的方式——把“前后端各写一套”变成“从一个类型源头自动派生出整个调用链路”。这篇文章我就用实际经历聊聊t3code 到底解决了我哪些真实痛点里面的技术选型是怎么协作的以及从初始化到部署我用它做完两个项目后沉淀下来的经验和几个躲不开的坑。1. 为什么我盯上了 t3code从手写后端到全栈类型安全的转变1.1 全栈项目最消耗精力的是“共识维护”一提到全栈项目很多人第一反应是“技术栈多”“要学的东西杂”。但我真正做了几个商用规模的 Web 应用之后发现技术栈复杂只是入门门槛真正的隐性成本在于前后端对数据结构的共识怎么维护。传统方案里前端通常通过 REST API 和 Vue/React 页面打交道我需要维护一份 API 文档或者用 OpenAPI/Swagger 描述接口后端改了 User 模型的字段前端必须同步修改 interface。项目一大了这个同步过程就会出现三类问题改了后端字段但前端没跟上线上页面白屏或者取到 undefined接口文档更新不及时前端按旧文档联调浪费半天时间某个字段从 string 变成 number类型没有编译期报错导致运行期才暴露异常。t3code 的出现有意思在它换了个思路与其费力维护前后端的类型共识不如让后端类型直接生成前端调用代码的类型签名。也就是说前端真正调用的数据是从后端那个唯一的类型源头“长”出来的。1.2 t3code 到底是什么虽然现在网上也有各种以 t3 开头命名的项目但我在实际开发中使用的 t3code本质上是一套面向全栈 TypeScript 项目的脚手架和模块初始化工具。它围绕 T3 StackNext.js TypeScript tRPC Prisma Tailwind CSS把项目的基础骨架、数据模型、认证体系、API 路由和前后端调用关系一次性装配起来我拿到手就能直接把精力放到业务逻辑上。我自己的体会是t3code 最适合这几类人独立开发者想用一份 TypeScript 代码同时打通数据库、后端逻辑和前端页面小型全栈团队希望减少前后端联调时的来回沟通成本正在搭建 MVP 的创业项目需要快速迭代数据结构且不想被接口设计和类型维护拖慢速度。如果你平时只是写纯前端或者纯后端t3code 的收益不大但如果你做的是那种“一个人要包圆前后端数据库”的活它真的能帮你省下很多重复劳动。1.3 它和“低代码平台”完全不是一回事第一次听到这类工具时我也担心过这会不会又是一套低代码平台把我绑定到某个私有协议上以后想迁移都没法迁移实际研究过后我可以明确说t3code 不搞任何私有的运行时协议它输出的就是一个标准的 Next.js 项目里面有正常的 Prisma schema、tRPC router、页面组件和 API 流程。换句话说它帮我完成的是“装配和初始化”不是“托管和绑架”。项目一旦生成源码都在我手里所有逻辑都由我自己控制该写测试写测试、该做性能优化做性能优化没有任何黑盒。1.4 从“写最少代码”到“改最少代码”我用传统方式做一个带用户系统的内容管理后台大概要经历设计数据库表结构、写后端 CRUD 接口、逐一手写路由和校验、生成前端 API 封装、在页面上手动消费数据、遇到类型不一致再回头改。整个流程中真正属于业务逻辑的代码可能只占 40%剩下 60% 都在做“翻译”和“适配”工作。t3code 把我从这种状态里拉出来之后我明显感觉到工作模式变了。从前是“写最少代码完成任务”现在更像是“尽量少改代码地演化业务”。数据库加个字段Prisma 迁移一执行类型自动更新后端在 tRPC 的 router 里加一个 query前端立刻能在编辑器里拿到带类型提示的调用方法甚至参数结构都是自动推导的。这种“一处修改处处类型安全”的体验用一次就回不去。2. t3code 的底层构成T3 技术栈四件套是怎么协作的2.1 四个核心依赖各管哪一段t3code 生成的骨架里最核心的是四个依赖。很多人把它们当成单纯的技术组合但我在实际使用中更关注它们各自承担的角色以及彼此之间的接缝在哪里。一是Next.js负责应用框架层。它同时提供 React 页面渲染、API Route、服务端渲染和静态生成能力。在 T3 Stack 的语境里Next.js 不仅是一个页面框架更是把前后端放在同一工程里的容器。二是TypeScript整个项目的类型系统基底。如果没有 TypeScript后面讲的“端到端类型安全”完全不成立所以我在 t3code 里很少看到 .js 文件所有业务代码和配置文件都以 .ts 或 .tsx 为主。三是tRPC负责前后端调用层的协议。它不需要我们手写 REST 路由也不需要生成 GraphQL schema而是直接通过函数调用的方式在前端调用后端方法并且自动获得完整的类型推导。它的实现原理不复杂就是依赖 TypeScript 的泛型把 router 类型直接传递到前端 hooks 上。四是Prisma负责数据模型和数据库访问。它用一个 schema 文件定义模型然后生成类型安全的客户端。在 t3code 的项目里Prisma 的 model 是整个应用数据结构的直接描述者。至于 Tailwind CSS我一般把它看成锦上添花的样式方案它解决的是 UI 层开发效率的问题不影响类型闭环。2.2 类型闭环是怎么走通的要真正理解 t3code 的价值我建议你先搞清楚这条类型闭环的路径Prisma schema 定义模型 ↓ Prisma Client 生成类型 ↓ tRPC router 里用这些类型处理输入输出 ↓ 前端通过 tRPC hooks 调用 router ↓ 编辑器根据推导自动提示类型和参数我把这条链路拆开讲一下。假设我在prisma/schema.prisma里定义一个Todo模型model Todo { id String id default(cuid()) title String completed Boolean default(false) createdAt DateTime default(now()) }执行npx prisma migrate dev之后Prisma 客户端会生成对应的Todo类型。接着在 tRPC router 中加入一个返回所有待办事项的查询import { router, publicProcedure } from ../trpc; import { prisma } from ../db; export const todoRouter router({ getAll: publicProcedure.query(() { return prisma.todo.findMany(); }), });前端页面里我不需要重新定义任何 interface直接调用import { api } from ~/utils/api; export default function TodoList() { const { data: todos } api.todo.getAll.useQuery(); return ( ul {todos?.map((todo) ( li key{todo.id}{todo.title}/li ))} /ul ); }这里你会发现一个有意思的地方todos的类型是由后端 Prisma 查询结果推导出来的而不是我手动声明的。哪怕哪天我往模型里加了字段前端代码里todo.someNewField也马上会有类型提示如果我删了某个字段前端用到它的地方会直接编译报错。这条闭环的意义在于它把“类型正确性”从一种约定变成了一种编译期保证。我不用再担心“前端不知道后端字段改过”这种事因为类型系统会第一时间拦住我。2.3 和 REST / GraphQL 方案的直观对比我知道有人会问我用 REST 加 OpenAPI 也能生成类型用 GraphQL 也能自动推导为什么非要用 t3code 这一套我做了一张粗浅的对照表完全基于我的日常体感方案类型安全性联调成本学习曲线适用场景REST 手写类型靠自觉高低传统前后端分离团队REST OpenAPI 生成类型中中中接口规范严格的团队GraphQL Codegen高低高需要灵活查询的大前端团队t3codeT3 Stack高很低中全栈 TypeScript 项目、独立开发者GraphQL 虽然查询能力很灵活但为了拿到这种灵活性我得维护 schema、写 resolver、配 codegen还要考虑客户端缓存策略对一个人干全栈来说其实不轻。t3code 里 tRPC 的模型更“纯函数式”前端调用后端的体验几乎等于调用本地函数而且不用额外跑代码生成器类型推导是实时的。当然 tRPC 也有自己的适用范围——如果前后端完全分离且由不同团队维护或者需要对外提供公共 APItRPC 反而不合适。它更多是内部后端和前端之间的一种高效沟通方式。选型永远要基于项目形态t3code 适合的是“一个代码库搞定全栈”的场景。3. 跑通第一个 t3code 项目从初始化到 CRUD 的完整链路3.1 初始化项目时该选哪些模块t3code 的起点是create-t3-app这个命令。我在终端执行npx create-t3-applatest my-app交互式命令行会问你项目名称、要不要启用 NextAuth.js、Prisma、Tailwind CSS、tRPC 这些模块以及使用 npm 还是 pnpm 等包管理器。我第一次跑的时候很老实把能勾选的全都勾上了。后来实际开发才意识到不必为了齐全而全选要根据业务预判来选。我现在的选择习惯是一定会选 TypeScript、Prisma、tRPC、Next.js这是核心需要用户登录时才会勾选 NextAuth.js如果只是内部系统且不涉及用户体系不选它反而省事Tailwind CSS 纯看项目 UI 需求如果团队已经定了组件库或者用其他 CSS 方案就不必让它进入代码库部署平台选择 Vercel 时脚手架会顺便把本地开发和生产环境的配置处理好选 Docker 自托管的话后面要自己留意数据库连接。初始化完成后项目的目录结构大概是这样的my-app/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── pages/ │ ├── server/ │ │ ├── api/ │ │ │ └── trpc/ │ │ ├── auth.ts │ │ └── db.ts │ └── utils/ │ └── api.ts ├── .env └── package.json这里src/server/db.ts是 Prisma Client 单例src/utils/api.ts是前端 tRPC hooks 的封装入口src/server/api/trpc.ts定义了 router 创建和 procedure 的基础。我建议拿到项目后先花十分钟把这三个文件读一遍理解它们之间的引用关系后面写业务会顺很多。3.2 从 schema 到数据库迁移我拿一个最简单的博客系统举例。先在prisma/schema.prisma里定义 Post 和 User 模型model User { id String id default(cuid()) email String unique name String? posts Post[] } model Post { id String id default(cuid()) title String content String published Boolean default(false) authorId String author User relation(fields: [authorId], references: [id]) createdAt DateTime default(now()) updatedAt DateTime updatedAt }然后执行npx prisma migrate dev --name init这个命令会做两件事在数据库里创建对应的表同时生成最新版的 Prisma Client。我提醒一句本地数据库连接串记得先写进.env很多人第一次跑 migrate 失败都是因为环境变量没配置。3.3 编写 tRPC router 并注册接着我在src/server/api/root.ts或其他你自定义的 router 聚合文件里添加一个 postRouterimport { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { prisma } from ~/server/db; export const postRouter createTRPCRouter({ list: publicProcedure.query(() { return prisma.post.findMany({ orderBy: { createdAt: desc }, }); }), byId: publicProcedure .input(z.object({ id: z.string() })) .query(({ input }) { return prisma.post.findUnique({ where: { id: input.id }, include: { author: true }, }); }), create: publicProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), authorEmail: z.string().email(), }) ) .mutation(async ({ input }) { const user await prisma.user.upsert({ where: { email: input.authorEmail }, update: {}, create: { email: input.authorEmail, name: input.authorEmail.split()[0] }, }); return prisma.post.create({ data: { title: input.title, content: input.content, authorId: user.id, }, }); }), });这里你可能注意到我用了 zod 做输入校验。tRPC 原生建议用 zod 作为 schema 校验库它和 tRPC 的 input 结合非常好。输入校验不仅保证了后端接口的参数安全还让前端调用时能拿到准确的入参类型提示算是类型闭环里的重要一环。写完之后必须把 postRouter 注册到根 router 上否则前端根本访问不到import { createTRPCRouter } from ~/server/api/trpc; import { postRouter } from ./post; export const appRouter createTRPCRouter({ post: postRouter, }); export type AppRouter typeof appRouter;3.4 前端页面消费 API前端页面直接对应src/pages/index.tsx我用一个列表页展示所有文章import { api } from ~/utils/api; import Link from next/link; export default function Home() { const { data: posts, isLoading } api.post.list.useQuery(); if (isLoading) return divLoading.../div; return ( div {posts?.map((post) ( Link key{post.id} href{/post/${post.id}} h2{post.title}/h2 p{post.content}/p /Link ))} /div ); }刷新页面后数据就能正常渲染。我没写任何额外的 axios 封装没有手动拼 URL没有定义 response interface甚至连useQuery的泛型都不用传类型全是从后端推导过来的。这个“少写代码但类型更强”的体验就是我前面说的核心价值。如果想做创建文章的交互也很直接const createPost api.post.create.useMutation({ onSuccess: () { utils.post.list.invalidate(); }, }); createPost.mutate({ title: 新文章, content: 正文内容, authorEmail: testexample.com, });用invalidate让列表重新请求一次数据自动刷新前端逻辑保持得非常干净。3.5 双向绑定与 SSR 模式的一点提醒t3code 默认会用 Next.js 的 SSR/SSG 能力来预取 tRPC 数据。也就是说服务端会先主动调用 tRPC 方法把数据塞进页面 props 里浏览器端再用同一个 hook 做水合。这套机制让首屏加载很快也不会有白屏闪烁。但这里的隐性前提是服务端调用和客户端调用共享同一套 tRPC 逻辑。如果你在 procedure 里访问了只存在于浏览器端的window或者localStorage服务端执行时就会直接报错。我建议你从第一天开始就养成习惯把涉及浏览器环境变量的逻辑放到独立的工具函数里并且用typeof window ! undefined判断环境不然部署到 Vercel 上很容易踩 SSR 相关的坑。4. 接入认证和数据库之后我踩过的那些坑4.1 NextAuth.js 和 Prisma 的适配器问题t3code 脚手架虽然默认集成了 NextAuth.js但真正接入登录功能时我遇到的第一坑是适配器adapter的配置。NextAuth 要跟 Prisma 配合必须用next-auth/prisma-adapter并且在src/server/auth.ts里正确传参。如果适配器没配好最常见的报错是调用登录接口时数据库无法写入 session 记录。我当时的排查过程是这样的先看服务端日志确认是 session 表写入失败再看auth.ts里的配置发现adapter没有被传入。修正后的核心代码大致是import NextAuth from next-auth; import CredentialsProvider from next-auth/providers/credentials; import { PrismaAdapter } from next-auth/prisma-adapter; import { prisma } from ~/server/db; export const authOptions { adapter: PrismaAdapter(prisma), providers: [ CredentialsProvider({ name: credentials, credentials: { email: { label: Email, type: email }, password: { label: Password, type: password }, }, async authorize(credentials) { // 这里是校验逻辑 }, }), ], session: { strategy: jwt, }, secret: process.env.NEXTAUTH_SECRET, };接下来还有一个容易忽视的点如果用默认的strategy: jwt就不太依赖数据库里的 session 记录这时 PrismaAdapter 需要同步维护的其实是 User、Account、Session 等模型。我在 schema 里如果漏了这些模型登录时也会因为数据库缺表而出错。解法很简单把官网文档里提供的 NextAuth 相关模型复制到schema.prisma再跑一次迁移问题基本就解决了。4.2 环境变量缺失导致的“幽灵报错”t3code 初始化后项目根目录会生成一个.env文件里面通常会预留DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL这些变量。我最开始忽略了一点本地开发和生产环境需要不同取值。比如本地.env里DATABASE_URL指向localhost的 PostgreSQL生产环境在 Vercel 后台配的却是线上数据库地址。如果你在本地跑prisma migrate deploy时用错了环境变量有可能把迁移应用到线上或者本地库上那将是灾难。我的建议是给不同环境配独立的.env文件如.env.local、.env.production并且永远只在项目目录里执行数据库迁移命令。虽然这属于老生常谈但团队协作时真的有人会把线上连接串写到公共配置里导致泄露。4.3 protectedProcedure 的正确姿势t3code 脚手架通常会提供一个createTRPCContext函数把当前请求的 session 塞进 context。我后来在写需要登录才能调用的接口时没用publicProcedure而是基于 context 判断后再抛错export const protectedProcedure t.procedure.use(({ ctx, next }) { if (!ctx.session || !ctx.session.user) { throw new Error(Unauthorized); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }); });这个中间件做完之后我想获取当前用户时直接读ctx.session.user就行。前端调用时配合useSession()判断登录态体验整体很顺。我踩过的坑是如果 session 类型没和数据库的 User 模型对齐前端拿到的session.user.id可能被推导成string | undefined导致一些危险的隐性 bug——比如在创建 Post 时把 undefined 写进 authorId。所以建议大家拿到 session 后先做一次显式判空再往下走。4.4 CRUD 校验放在服务端不要在客户端省事tRPC 的好处是输入校验天然写在服务端 procedure 里但我见过有些同事图方便只在前端表单里做了校验后端换个请求工具直接把非法字段传进去。说实话因为 tRPC 的类型推导太舒服了人很容易产生“前端类型安全等于接口安全”的错觉。实际上类型安全不等于安全。zod 校验那层防线必须留在服务端。我在 t3code 项目里的习惯是所有涉及写入的操作一律写 zod input schema并且把z.string().min(1)这类边界条件写全。这样即使有人绕过前端调后端数据也会被拦截在这层不会污染数据库。5. 部署与优化从开发机到生产环境的最后一公里5.1 部署到 Vercel 前要处理的事情t3code 生成的 Next.js 项目直接对接 Vercel 很方便。我第一次部署时只把 GitHub 仓库连到 Vercel然后添加环境变量就完事了。但后来发现有几个细节要提前准备好否则很容易在线上出现诡异问题。第一Prisma Client 的生成时机。Vercel 构建时会跑prisma generate但只在构建环境里生成如果你本地的 Prisma Client 版本和线上不一致有可能导致运行时错误。我一般会在package.json的postinstall脚本里加上postinstall: prisma generate这样每次装完依赖都会从 schema 重新生成客户端不会出现版本漂移。第二数据库迁移怎么执行。Vercel 的构建环境不是跑迁移的好地方我推荐用 GitHub Actions 或一个独立的环境去跑prisma migrate deploy。也就是说代码部署和数据库迁移分成两个流程代码先发布迁移单独控制节奏。这样做的好处是一旦迁移失败不会把应用构建直接拖死。第三NEXTAUTH_URL 必须和生产域名一致。NextAuth 在做回调时会校验这个变量我见过不少案例是由于把它留成http://localhost:3000导致线上登录后跳转回本地地址表现为“登录成功但立刻又跳回登录页”。这个坑排查起来其实不难但确实很耗时间。5.2 Serverless 环境下的数据库连接池Vercel 的 Serverless 函数是“按请求拉起、跑完即回收”的模型。如果我在每个函数实例里都创建一个新的 Prisma Client 连接很快会把数据库连接数打满尤其在使用 PostgreSQL 时特别明显。t3code 生成的src/server/db.ts里帮我做了一个全局单例import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma: PrismaClient }; export const prisma globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV development ? [query, error, warn] : [error], }); if (process.env.NODE_ENV ! production) globalForPrisma.prisma prisma;这个模式的核心意图是在本地开发时热更新会导致模块反复加载如果不做全局缓存每次刷新都会创建新的 Prisma 实例部署到 Serverless 环境后同一实例内部复用连接。我在生产环境跑了一段时间后数据库连接数一直保持在一个健康水位。如果项目流量上来还可以在数据库连接串里加上连接池参数比如 Prisma 官方建议的?pgbouncertrue配合 PgBouncer 使用。要不要这样做取决于实际并发量我自己的经验是中小型应用把单例模式做好就够用了等连接数告警了再上连接池也不迟。5.3 首屏性能调优的几个实际操作虽然 t3code 的默认体验已经不错但生产环境对性能的要求更高。我做过的几个有效优化排在这里按需取数据Prisma 的select和include要按页面需要来避免一次findMany把几十个字段全捞出来。Serverless 函数的执行时间就是钱数据量小一点响应自然快。开启 Next.js 的缓存策略对于不常变的数据可以用staleTime控制 tRPC 查询的缓存时长。例如文章列表五分钟内不重新请求api.post.list.useQuery(undefined, { staleTime: 5 * 60 * 1000, });严格区分动态和静态页面博客首页、关于页这种内容基本不变的尽量用静态生成用户中心这种强个性化的页面再走 SSR 或客户端渲染。能静态就不动态能让边缘缓存接住就让它接住。5.4 日志、监控和错误上报做全栈项目时日志是最容易被忽略但又最重要的一环。t3code 脚手架只给了基础结构不包含任何监控。我后来接了一款错误监控工具把 tRPC 的 errors 统一上报因为 tRPC 的错误信息非常规范有code、message、path这些字段非常适合直接对接到监控平台上。我在src/server/api/trpc.ts的 errorFormatter 里做了一次全局拦截export const t initTRPC.contexttypeof createTRPCContext().create({ errorFormatter({ shape, error }) { // 在这里上报错误 return shape; }, });这样线上如果出现 Zod 校验失败、数据库异常、未授权访问我都能第一时间从监控后台看到具体是哪个 procedure 出了问题而不需要等用户反馈。6. 回顾 t3code 这段时间的实际体验用 t3code 做了完整项目之后我有个很直接的感觉它把全栈开发里的“隐形税”砍掉了一大半。以前我要花很多时间维护前后端接口文档、类型声明、API 封装现在这些成本降到了接近零写业务代码的密度变得很高。对我来说它最大的价值不在代码生成而在那套类型闭环带来的心智负担减轻。当然它也不是银弹。如果你的团队前后端完全分离后端需要面向第三方开放 API那 tRPC 这套“服务端和客户端强绑定”的方式反而会成为负担。另外T3 Stack 的生态相对年轻很多企业级基础设施的兼容方案需要自己探索比如前面说的连接池、多环境数据库迁移、CI/CD 流程都需要你额外搭建。但如果你是单兵作战或者小团队想做一个类型安全、可快速迭代的 Web 应用t3code 这套工作流非常值得试一下。刚开始上手时多花一点时间把 Prisma schema、tRPC router、NextAuth 的关系理解透后续开发体验的提升会大大超出你的预期。最后再分享一个小技巧。我用 t3code 时习惯把项目里所有数据库写入操作收敛到mutation里并且做好输入校验和结果类型声明。这样一旦业务逻辑要调整改动范围会非常小不会在十几个页面里到处找散落的 set 调用。这种习惯配合 t3code 的类型推导让整个项目即使经过几个月的迭代依然保持着比较好的可维护性。如果你正准备上手一个新的全栈项目我建议你从最小闭环开始——先不急着一次性引入全部模块把列表查询和一条数据写入跑通感受一下“类型自己长脚走完全栈”的体验再做后续扩展也不迟。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表