t3code 实战:用 T3 Stack 打造类型安全的全栈应用

发布时间:2026/10/26 8:40:10
t3code 实战:用 T3 Stack 打造类型安全的全栈应用 项目名叫 t3code熟悉 T3 Stack 的朋友第一眼应该就能猜个大概。我是先被这个名字骗进来的以为是某个编译器或者第三方工具结果翻了一圈源码才发现这其实是一套把 TypeScript 全栈实践串起来的项目骨架。前后折腾了三周多第一版因为选型不清晰被我整个推翻重写第二版才真正跑顺。这段时间踩了不少坑也把 tRPC、Prisma、Next.js 这些组件之间的关系理清楚了。这篇文章不是把官方文档抄一遍而是以 t3code 这个实战项目为线索讲清楚这套技术栈为什么这么配、核心链路怎么打通、以及实际开发中最容易翻车的几个地方。适合那些接触过 Next.js 但还没完整跑过全栈类型安全的团队或者刚入门 TypeScript、想找一个端到端项目练手的人。文章里的代码细节和配置文件我都会给到你可以直接照着搭。1. t3code 是什么从项目名到 T3 Stack 的由来1.1 T3 Stack 不是某个单一框架很多刚接触的人都会问T3 Stack 是不是一个新框架其实不是。T3 是 TypeScript、Tailwind CSS、tRPC 这几个首字母的缩写而“3”对应的是一个组合关系TypeScript 管类型安全Tailwind 管界面样式tRPC 管 API 调用。这个组合最早由 Theo 等人在社区里推广后来被整理成了 create-t3-app 脚手架一条命令就能拉起一个完整项目。t3code 这个项目名本质上是“T3 Stack code”的简写。我在搭建的时候没有去魔改它而是尽量遵循官方推荐的目录约定再在其上补充一些适合团队协作的代码边界。它默认带了 Next.js 作为应用框架Prisma 负责数据库建模和查询还有 NextAuth 做登录鉴权而这些组件之间通过 TypeScript 类型系统串联起来。1.2 t3code 这个项目到底干了什么我做 t3code 的初衷是做一个内部用的极简工作台包含任务记录和随笔管理两个核心模块。听起来功能不复杂但麻雀虽小五脏俱全要处理用户登录、数据库建表、CRUD 接口、前端页面渲染还要保证多人协作时接口参数不写错、数据库字段不裸奔。之所以选中 T3 Stack是因为我想要的是一个“单仓库、单语言、类型全链路打通”的体验前端调用某个接口时入参和返回值的类型直接从前端代码就能推断出来不需要像传统 REST 流程那样先翻接口文档再在前后端各定义一遍类型。这种体验在 t3code 里跑通之后团队新人上手的速度会明显加快因为编译器会帮人挡掉大量低级错误。1.3 适合哪些人参考如果你是后端出身、平时用 Java 或 Go 写接口对 TypeScript 还不太熟t3code 可以帮你看到一个“全栈都站在同一套类型系统上”的具体长什么样。如果你是前端出身、对数据库操作不太熟悉Prisma 那种声明式的模型定义方式也比较容易接受。总之只要你想跳过“前后端各写一份类型定义再手动同步”的工序这篇文章都值得看完。2. 选型逻辑为什么这组技术搭在一起特别顺2.1 为什么是 tRPC 而不是 REST 或 GraphQL这是我在项目启动前思考最久的问题。先说结论对于 t3code 这种内部工具型应用tRPC 的开发和维护成本是最低的。传统 REST 接口的思路是“资源 动词”比如 GET /tasks、POST /tasks。它很成熟绝大部分系统都能用。但在全栈 TypeScript 项目里REST 有一个明显的割裂点前端不知道后端某个接口到底要什么参数、返回什么字段需要额外维护 OpenAPI 文档或者在代码里共享类型。只要前后端类型不同步运行期就很容易出现低级错误比如把一个数字当字符串传过去后端没报错但数据处理完才发现不对。GraphQL 能解决一部分类型问题但它带来的是另一套复杂度你要学 schema 定义语言、写 resolver、处理缓存和数据加载器。对一个小型内部工具来说这些负担远超收益。tRPC 的思路完全不一样。它不引入新的协议而是直接把后端函数变成可被前端安全调用的过程。前端调用task.getAll()后端就是getAll()这个函数中间的类型推导由 TypeScript 自动完成。如果后端把返回值的字段改了前端代码的编译会直接报错把很多运行期问题提前到写代码阶段。对比项RESTGraphQLtRPC类型共享需要额外维护通过 schema 相对安全天然由 TypeScript 推导学习成本低中高低缓存策略简单直接需要专门解决结合 React Query适合场景开放 API、多端复用数据聚合复杂的大型应用全栈 TypeScript 内部应用2.2 Next.js 和 Prisma 在链路里的定位Next.js 在 t3code 里承担的是“应用壳层”的角色。它的 App Router 提供了文件路由、服务端渲染和 API 路由前端页面和后端逻辑可以在同一个仓库里共存。但注意Next.js 本身并不知道数据库的存在它只是把页面请求和函数调用串起来。Prisma 负责的是让 TypeScript 直接“操作”数据库。你在 schema.prisma 里定义模型比如model Task { id String id default(cuid()) }然后 Prisma 会根据这个模型生成客户端类型。这样一来查询数据库的代码就像写 TypeScript 函数一样字段名错了或者类型不匹配编辑器里会直接出现红色波浪线。这里多说一句我见过有人抱怨 Prisma 生成的客户端太“重”启动慢。其实对于 t3code 这种规模的应用性能差异几乎无感。真正的好处是开发效率提升改完模型跑一次prisma generate类型立刻同步省掉了很多手写 SQL 映射的辛苦。2.3 Tailwind 和 TypeScript 的配合细节Tailwind 在 t3code 里主要解决的是样式一致性问题。它不是一个组件库而是一套原子化 CSS 方案。写页面的时候直接在 className 里写flex items-center justify-between这种类名不需要频繁切换 CSS 文件。有人会问Tailwind 和 TypeScript 有什么关系关系在于编译期检查。Tailwind 的类名是字符串如果拼错了页面不会报错只是样式不对。但配合 TypeScript你可以在关键业务组件中封装一些带类型定义的工具类组件把类名约束在有限集合内减少手滑写错的概率。这不是强绑定但在 t3code 里我把它当作一种工程规范来用。3. 从零搭建 t3code初始化、模型与目录规范3.1 初始化create-t3-app 的配置项选择搭建的第一步是拉取官方脚手架。我用的是 create-t3-app命令如下npm create t3-applatest t3code过程中会问你选哪些模块。我在 t3code 里选择了 TypeScript、Tailwind、tRPC、Prisma、NextAuth暂时没有选打包工具配置里的独立模块。创建完成后目录大致长这样t3code/ prisma/ schema.prisma src/ app/ layout.tsx page.tsx server/ db.ts api/ routers/ trpc.ts styles/ globals.css这个结构的好处是“接口逻辑和服务端代码集中在 server 目录页面组件放在 app 目录”。新人一开始可能不习惯但按这个约定写代码不会出现功能逻辑散落各处的情况。3.2 数据模型设计从一个任务表说起t3code 的第一个核心模块是任务记录。prisma/schema.prisma里的模型我定义得比较克制model User { id String id default(cuid()) name String? email String? unique emailVerified DateTime? image String? tasks Task[] notes Note[] } model Task { id String id default(cuid()) title String done Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt userId String user User relation(fields: [userId], references: [id]) } model Note { id String id default(cuid()) content String createdAt DateTime default(now()) userId String user User relation(fields: [userId], references: [id]) }为什么用cuid()而不是自增 id因为对于内部工具cuid 不需要等数据库生成主键插入时就能确定 id后续做关联和路由跳转更顺手。真实项目如果对主键长度敏感再换 uuid 也不麻烦。3.3 目录结构的代码边界约定光有官方目录还不够我在 t3code 里加了一个约定凡是数据库相关代码只能出现在src/server/db.ts和各个 router 文件里页面组件不能直接访问 PrismaClient。这样做是为了让数据访问路径清晰可查后续如果要拆分出独立 API 服务改动范围也很明确。路由层的命名也有约定每个 router 对应一个业务域。比如task.ts只处理任务相关的操作note.ts只处理随笔相关的操作公共业务放在root.ts里做聚合。这样文件之间不会有隐式依赖新人看代码时不需要猜。4. 核心链路打通从数据库到页面的完整流程4.1 环境变量和数据库连接初始化之后第一件事是配置环境变量。create-t3-app 会生成一个.env.example我直接复制成.env并填上本地数据库连接DATABASE_URLpostgresql://user:passwordlocalhost:5432/t3code?schemapublic NEXTAUTH_SECRETyour-secret NEXTAUTH_URLhttp://localhost:3000连接池和 Prisma 的关系这里提一嘴Prisma 默认通过连接池管理数据库连接。本地开发时连接数不多直接使用默认配置就行。如果上线后并发高可以在连接串里加connection_limit5之类的参数。但不要为了压测随便调大数据库服务器撑不住的话过早开高并发连接反而会把数据库压垮。写完配置后依次执行npx prisma migrate dev --name init npx prisma generate第一条命令会创建表并生成本地迁移记录第二条命令会更新 Prisma Client 的类型定义。我在这一步犯过一个低级错误就是改了 schema 但忘记跑 generate导致编辑器里一直提示task.userId不存在。这个坑不是框架的问题而是流程遗漏后面会专门说。4.2 编写第一个 tRPC 路由以任务模块为例router 的代码大致是import { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; export const taskRouter createTRPCRouter({ getAll: protectedProcedure.query(async ({ ctx }) { return await ctx.db.task.findMany({ where: { userId: ctx.session.user.id }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1) })) .mutation(async ({ ctx, input }) { return await ctx.db.task.create({ data: { title: input.title, userId: ctx.session.user.id, }, }); }), });这里有两个关键点。一是protectedProcedure它要求请求必须携带有效登录信息否则直接拒绝。它其实是中间件包了一层把 session 里的用户信息挂到了ctx上。二是zod做入参校验类型安全不只体现在编辑器提示上运行期校验也没省。4.3 前端调用与类型推导页面组件里调用这个接口的写法如下import { api } from ~/trpc/react; export default function TaskList() { const { data: tasks, refetch } api.task.getAll.useQuery(); const createTask api.task.create.useMutation({ onSuccess: () refetch(), }); return ( ul {tasks?.map((task) ( li key{task.id}{task.title}/li ))} /ul ); }表面上看起来这只是个 Hook但背后的类型推导链条是taskRouter的定义被传递到api.task上前端输入task.时编辑器会自动列出getAll、create这些可用方法并且自动推导出入参和返回类型。如果后端的返回类型改变了前端代码会立刻编译报错。4.4 完整链路的验证我第一次跑通这条链路时最强烈的感受是“没有中间环节可以被遗忘”不需要写接口 mock、不需要手工维护接口文档、不需要在前后端同步字段名。整个链路从数据库表、后端函数到前端调用类型是一致的。这也正是 t3code 这类项目的价值所在。验证方式很简单起一个本地服务npm run dev然后用浏览器登录在页面上创建一个任务再去数据库里确认记录存在。如果一切顺利你会发现整条链路涉及的代码量比传统 REST 方式少了不少。5. 实际开发中最容易翻车的几个地方5.1 Date 序列化瞬时对象和响应类型的断层这是我在 t3code 里遇到的第一个“隐藏坑”。Prisma 模型里的createdAt是DateTime类型在 Node.js 里对应的是 JavaScript 的Date对象。但当你把它返回给前端时经过 JSON 序列化后它变成了字符串。如果前端代码里直接用了task.createdAt.toLocaleDateString()开发环境可能没问题因为 Next.js 服务端渲染时它确实还是 Date 对象。但等到前端做纯客户端渲染、或者数据被缓存重新水合后createdAt就变成了字符串一调用.toLocaleDateString()直接报错。解决思路有两个一是在前端统一用String()转一下再显示二是在 tRPC 的返回对象上用 zod 做一次输出解析把 Date 显式转成 ISO 字符串。我推荐后者因为更严谨不会因为调用时机不同出现行为不一致。5.2 鉴权中间件的注册顺序t3code 里同时存在公共接口和登录接口。刚开始我把根 router 配置成全部需要鉴权结果登录页面本身也调用了接口导致死循环。查了半天发现不是逻辑问题而是中间件结构问题。tRPC 的做法是区分publicProcedure和protectedProcedure。我后来重新设计成登录相关的 router 用公开过程任务和随笔的 router 用受保护过程。同时在受保护过程中再去校验 session这样接口之间的边界就清晰了。如果你像我一样前期图省事把整个 root 包了中间件后面加白名单逻辑反而更绕。5.3 Prisma migrate 的 dev 和 deploy 差异本地跑prisma migrate dev很顺手一执行就会自动创建迁移文件并同步数据库。但上了测试环境或生产环境后请务必用prisma migrate deploy它只会应用已经存在的迁移文件不会自动创建新迁移也不会因为 schema 和数据库不一致而尝试“修复”。我在这上面栽过跟头在本地多个分支间切换时某个分支的 schema 改了但迁移文件没建全。跑migrate dev时居然提示我“检测到漂移”问我要不要重置数据库。如果手一抖点了 yes本地数据就没了。后来我把所有环境统一成“先 review 迁移文件再 deploy”这种情况就再没出现过。5.4 热更新时的类型残留问题Next.js 开发模式的热更新很快但 tRPC 的类型推导偶尔会出现“滞后”。表现为你改了后端 router 的返回字段前端编辑器的类型提示还是旧的甚至报错说某个属性不存在而重启 dev server 之后又好了。这不是 bug而是 TypeScript 语言服务和热更新之间的缓存问题。遇到这种情况不用急着删 node_modules先尝试重启 TypeScript 编辑器服务或者直接重启 dev server。我在团队里给的建议是改完 router 定义后先跑一次npm run typecheck确保类型编译确实通过了再切回浏览器看效果。6. 让 t3code 走得更远的工程化思考6.1 什么时候不适合用 T3 Stack虽然我个人很喜欢这套技术组合但它不是银弹。如果你的项目需要开放 API 给第三方使用或者需要面对多种客户端tRPC 的设计就没那么合适了毕竟外部系统不可能为了调用你的函数去引入 tRPC 客户端。同样如果团队里前端的 TypeScript 基础比较薄弱推行 t3code 这种模式会遇到不小的阻力因为编译器提示报错可能比运行时错误还多初期会感觉“到处都是红线”。我选择 t3code 的场景是内部工作台用户量不大快速迭代优先类型安全是加分项。如果你的项目是长期对外服务需要控制 API 协议兼容性那还是老老实实用 REST 加 OpenAPI 更稳妥。6.2 依赖更新和小版本踩坑T3 Stack 涉及的依赖比较多Next.js、Prisma、NextAuth、Tailwind 每个库都有自己的发布节奏。我的建议是不要所有依赖一把梭升级而是按模块小步更新每次升级完都跑一遍核心流程登录、任务创建、随笔查询。印象比较深的一次是 Next.js 大版本升级后App Router 的页面参数类型变了导致一堆组件类型报错。这种升级成本和收益不成正比尤其在项目进入维护期后。所以没有明确需求时锁定依赖版本按团队节奏统一升级比追求“永远最新”更省心。6.3 团队协作的约定和未来扩展目前 t3code 的代码量不大但我在代码注释里写下了一条团队约定任何新增的业务模块都要先写 schema、再写 router、最后写页面禁止从页面倒推去改数据库结构。这样可以避免出现“页面功能看起来正常但数据库结构乱成一团”的情况。如果后续要做 App 端t3code 的架构也给你留好了退路tRPC 可以子集化把部分接口通过官方适配层转成 REST 或 WebSocketPrisma 的模型定义和数据库结构是稳定的不会因为 API 层变化而推翻重来。也就是说t3code 不是一个只能 demo 的玩具项目它更像一个可以持续演化的工程起点。说了这么多还是那句话技术选型没有绝对的正确答案关键是团队能在同一套模式里高效协作。t3code 对我来说最大的价值不是“用了什么新技术”而是让“类型安全”这个原本停留在口头上的概念真正从数据库一路贯彻到了页面。这套组合我已经用顺手了后续如果继续扩展大概率还是会在它的基础上做增量而不是另起炉灶。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询