☰
t3code全栈实战:用T3 Stack构建类型安全应用的全过程解析
2026/10/28 6:12:50 网站建设 项目流程

最近我把压箱底的 t3code 项目重新整理了一遍,这个项目名听起来像个密码,实际上是一套基于 T3 Stack 的全栈实战代码仓库,核心就是 TypeScript、Tailwind CSS 和 tRPC 这三件套的组合拳。如果你已经玩过一阵子 Next.js,又受够了前后端各写一套类型、接口文档全靠脑补的日子,那 t3code 这个思路会给你一种“原来全栈还能这么写”的清爽感。这篇东西不打算讲晦涩的理论,就按我把项目从零推到上线的真实过程,把每一个关键决策、每一条命令、每一处坑都摆出来,给想快速上手类型安全全栈开发的朋友一条能直接抄的作业路径。

1. t3code 的项目定位与 T3 技术选型思路

1.1 T3 Stack 三个核心成员分别解决什么问题

先聊清楚 t3code 这个名字里 T3 到底指什么。T3 是 The TypeScript Trifecta 的缩写,三件套分别是 TypeScript、Tailwind CSS 和 tRPC。三样东西各管一摊:TypeScript 负责给整个项目装上类型系统,让变量、函数、接口之间的关系从“约定”变成“强制”;Tailwind 负责把样式从“写 CSS 文件”变成“在模板里堆类名”,开发速度提升非常明显;tRPC 则负责干掉前后端之间重复的类型定义,让 API 的输入输出类型直接从服务端“长”到客户端。

很多朋友第一次听 tRPC 会觉得陌生,我最初也是这样。它和 REST、GraphQL 不是一个层级的东西,它更像一种“过程调用”的通讯方式——前端像调用本地函数一样调用后端方法,而不是拼接 URL、处理 JSON 序列化。t3code 项目之所以选 tRPC 而不是 REST,核心就一个字:省。省掉手写类型,省掉接口文档维护,省掉一半以上的网络层样板代码。REST 本身没问题,但当你项目里有几十个接口、几百个类型字段的时候,前后端同步成本会失控。GraphQL 能解决一部分问题,但 schema、resolver、fragment 的学习曲线又太陡,在小中型项目里有点杀鸡用牛刀。tRPC 正好卡在中间:你要的全部类型安全,它给了;你要的极低学习成本,它也给了。

这里要补充一个容易误解的点:T3 Stack 并不是只能在这三样里打转。我在 t3code 里还引入了 Prisma 和 NextAuth.js,一个是数据库 ORM,一个是鉴权方案。T3 的官方理念叫“先做加法再做减法”,意思是默认选型倾向于实用主义,不需要为了纯粹而砍掉开发效率工具。所以 t3code 的完整技术栈实际是 Next.js 负责应用框架和渲染、Prisma 负责数据模型与迁移、NextAuth 负责登录会话、tRPC 负责 API 层、Tailwind 负责样式,外加 TypeScript 把所有部分粘在一起。这个组合在真实项目里跑起来非常顺,因为我写每一个功能时,类型都在背后默默盯着我,报错信息几乎能告诉我下一步该干什么。

1.2 为什么 t3code 坚持“类型安全优先”的路线

类型安全这个词听起来很抽象,我打个比方。前后端协作就像两个人隔着一条河来回递东西,REST 方案是两个人先约定“我用 JSON 格式递给你,字段名我写文档里了”,然后每次递东西都要靠默契;tRPC 方案则是在河上直接修了一座铁路桥,火车车厢的宽度在两边完全一致,运什么货、货多重,轨道系统早就标好了。你在后端定义了一个createPost({ title, content }),前端拿到这个方法时,参数类型、返回值类型全都自动提示出来,写错了代码直接就红波浪线,根本轮不到运行时报错。

这个“类型安全优先”的路线对项目的长期维护影响非常大。t3code 刚开始规模小,几个人都觉得自己记忆力很好,不需要“多管闲事”的类型系统。但等到功能叠到二三十个,数据表扩展到十来张,你会发现自己开始频繁翻看接口定义、复制粘贴字段名,一不留神就把userId拼成userID。这类低级错误在运行时才暴露,排查成本远高于写代码时让编译器拦住。t3code 在服务端路由里把 query 和 mutation 的输入用 zod 做校验,类型直接推到前端后,字段拼错、类型传错的概率降到几乎为零,层与层之间的“翻译官”角色彻底消失。

还有一层容易被忽略的好处:前端开发体验。以前写页面调接口,我得先去后端代码里查返回结构,经常要开着两个编辑器窗口来回切。t3code 里,我写一个组件需要数据,直接api.post.all.useQuery(),返回的data自带完整类型,编辑器里一按点号就能弹出所有可用字段。配合 VSCode 的自动补全,写业务代码变成“填空”而不是“默写”,效率提升是肉眼可见的。另一个项目里同事没用 tRPC 还在手写类型,同样的功能他多写了将近三百行代码,这就是最直观的差距。

2. 从零初始化 t3code:环境准备与代码生成

2.1 脚手架创建与交互选项,一条命令生成项目骨架

初始化 t3code 不需要手工搭目录,官方脚手架 create-t3-app 已经把配置文件、基础目录、路由示例全部铺好了。我用的命令是:

npm create t3-app@latest t3code

执行之后会进入交互式选择界面,让你勾选需要的模块。我当时依次勾选了 Next.js、Tailwind CSS、tRPC、Prisma、NextAuth.js,还顺手勾了 ESLint 和 Prettier。这里有一个细节值得讲清楚:如果完全没接触过 tRPC,可以先把 tRPC 和 Prisma 都选上,因为 t3code 的核心演示功能“发帖/看帖”正好需要数据库和 API 两层配合,脱离任何一层都展示不出类型联动的优势。如果你只是想试试水,可以先不选 NextAuth,登录体系后面手动加也不是不行,但脚手架自动生成的 auth 相关文件能省掉很多配置时间,所以我建议一开始就全选。

初始化完成后,项目目录会生成一个src/文件夹,里面默认分好几个区域:src/pages或src/app放页面组件,src/server/api放 tRPC 路由和上下文,src/server/db.ts负责 Prisma 客户端实例,src/trpc/react.tsx封装了 React 侧的 Provider 和 hooks。第一次看到这些文件时可能会有点晕,但它们各自职责非常清晰,本质上是官方把最佳实践直接固化成了一个模板。我那天初始化完第一次启动npm run dev,浏览器打开首页看到一个能点击的计数器,心里想的是“行,链路通了”。这个计数器虽然简单,但它已经完整跑了一遍数据库写入和 tRPC 查询,相当于一个可以随时拿刀修改的最小闭环。

2.2 先定义数据库模型还是先写页面?我的答案是先定数据

在 t3code 里我实现的核心功能是“用户注册登录,登录后可以发帖、看帖”。这种功能的数据结构非常简单,但仍然需要先把数据模型定清楚再动手写业务代码,否则后面改字段会非常痛。Prisma 的 schema 文件位于prisma/schema.prisma,我当时定义了 User 和 Post 两张表,以及它们之间的一对多关系。代码看起来是这样:

model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] } model Post { id String @id @default(cuid()) content String createdAt DateTime @default(now()) authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) }

这张 Post 表特意加上了onDelete: Cascade,意思是一个用户注销后,他发的帖子自动删除。这个细节在实际项目里经常被忘掉,结果就是删除用户时报外键约束错误。定义好模型之后执行迁移:

npx prisma migrate dev --name init

这个命令会做两件事:生成 SQL 迁移文件,并同步生成 Prisma Client。此后在整个后端代码里,我只要import { db } from "~/server/db"就能拿到类型安全的数据库客户端,查询用户、创建帖子的所有字段都有编辑器提示。这里我强烈建议别在没想清楚模型关系时就往下写页面,数据库是地基,地基斜了,上面楼层越高越难救。

2.3 调试前的环境变量与本地基础配置

脚手架生成的.env文件里预留了几个关键变量,我在本地开发时配置如下:

DATABASE_URL="file:./db.sqlite" NEXTAUTH_SECRET="openssl rand -base64 32 生成的内容" NEXTAUTH_URL="http://localhost:3000"

本地环境我用 SQLite 而不是 MySQL 或 PostgreSQL,原因很简单:零安装、零维护,文件就在项目目录里,删了重来也不心疼。DATABASE_URL指向file:./db.sqlite后,Prisma 会自动在prisma/目录下生成 SQLite 数据库文件。NEXTAUTH_SECRET是 NextAuth 的会话签名密钥,千万不能提交到 Git 仓库,用openssl rand -base64 32生成一串随机字符填进去即可,正式环境建议放到托管平台的 environment secrets 里。

还有两个容易被忽略的细节:NextAuth 在本地默认会把会话 Cookie 名取为next-auth.session-token,如果 CDN 或反向代理改了 Cookie 同名策略,登录状态就可能掉线,这一点在部署后尤其要留意;另外.env文件必须放在项目根目录,不能放进src/,否则 Prisma 和 NextAuth 都读不到环境变量。那天我重新拉了一个分支,.env还没复制过来就急着npm run dev,Prisma 直接报错Environment variable not found: DATABASE_URL,这类问题十有八九都是环境变量配置位置或命名对不上导致的。

3. 从用户注册到发布内容的完整链路实战

3.1 基于 NextAuth 接入邮箱登录,三十分钟搞定

登录功能听起来复杂,但在 t3code 里 NextAuth 已经预置了大部分逻辑。我打开src/server/auth.ts,看到脚手架自动导出了一个getServerAuth函数,内部配置了 Prisma 作为用户数据存储。接入邮箱登录,只需要修改配置里 providers 数组,加上 Email provider:

import EmailProvider from "next-auth/providers/email"; export const authOptions = { providers: [ EmailProvider({ server: process.env.EMAIL_SERVER_URL, from: process.env.EMAIL_FROM, }), ], adapter: PrismaAdapter(db), session: { strategy: "database" }, };

这里PrismaAdapter会自动把用户、账号、会话映射到我们之前定义好的 User、Account、Session 表里。Email 登录的流程是用户填邮箱、收到魔法链接、点链接自动登录,不需要维护密码,非常适合中小型应用。我在本地测试时没有真实邮件服务器,就在.env里配了一个 Ethereal 的测试账号,开发时完全够用。如果你懒得配邮件,也可以先用 Credentials provider 加用户名密码登录,但 NextAuth 的 Credentials 方案不支持数据库 session,只能用 JWT,后续做“查看登录用户发布的帖子”这类功能时逻辑会绕一些,我更推荐直接 Email。

这一步做完,我顺手在 NavBar 组件里加了登录/登出按钮,用useSession()获取会话状态,登录后显示用户头像和昵称,未登录就显示登录链接。整个交互在十分钟内调通,体感非常顺畅。

3.2 用 tRPC 写第一个查询和变更:从 hello 到 createPost

脚手架默认生成的一个 router 文件叫src/server/api/routers/post.ts(我后来自己改的),里面已经有一个hello例子。把概念理清楚,核心就是 router 里挂 procedure,procedure 分两种:publicProcedure所有人都能调,protectedProcedure必须登录才能调。t3code 里我写了两个核心 API:

import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; export const postRouter = createTRPCRouter({ all: publicProcedure.query(async ({ ctx }) => { const posts = await ctx.db.post.findMany({ orderBy: { createdAt: "desc" }, include: { author: true }, }); return posts; }), create: protectedProcedure .input(z.object({ content: z.string().min(1).max(280) })) .mutation(async ({ ctx, input }) => { const post = await ctx.db.post.create({ data: { content: input.content, authorId: ctx.session.user.id, }, }); return post; }), });

注意create用的是protectedProcedure,这个函数在内部会先校验会话,没有登录的用户直接抛错,根本不会进入数据库写入逻辑。content字段用 zod 限定为 1 到 280 个字符,输入不合法就直接被拦截。这里其实藏着 tRPC 一个很大的优势:输入校验的位置统一放在服务端,前端根本不传错数据。回头对比 REST 方案,我在很多项目里见过前端传了个空字符串给后端,后端没校验直接入库,最后页面渲染出来一堆空白卡片,排查半天才发现是数据脏了。tRPC + zod 把这个问题从根上解决了。

接着在src/server/api/root.ts里把postRouter挂到根路由上:

export const appRouter = createTRPCRouter({ post: postRouter, });

然后在页面里就能直接用了。

3.3 前端调用秒变本地函数,两端类型自动同步

tRPC 最爽的一刻,是我在页面组件里写调用的时候。以帖子列表为例,代码干净到让人怀疑是不是真有后端:

import { api } from "~/trpc/react"; export default function PostList() { const { data: posts, isLoading } = api.post.all.useQuery(); if (isLoading) return <div>加载中...</div>; return ( <div> {posts?.map((post) => ( <div key={post.id}> <p>{post.content}</p> <span>作者:{post.author.name}</span> </div> ))} </div> ); }

这里api.post.all直接从后端 router 推导出来,posts数组里的每一项是什么结构,编辑器里自动补全清清楚楚。如果要发帖,也是同样直接调用:

const utils = api.useUtils(); const { mutate, isLoading } = api.post.create.useMutation({ onSuccess: () => utils.post.all.invalidate(), });

onSuccess里调用invalidate()会让帖子的查询自动刷新,新发的帖子立刻出现在列表里,不用手写刷新逻辑。第一次体验这个联动的开发者几乎都会愣一下:这就完了?后端刚加的字段,前端立刻能点出来?对,这就完了。所有的 HTTP 封装、类型生成、状态管理,tRPC 一口气全包了。我经常和团队说,tRPC 不是给你多了一种 API 风格,而是把前后端之间的“翻译层”整个抽走了,你只需要专注业务代码本身。

不过这里要提醒一个细节:tRPC 的类型同步依赖服务端与客户端的代码能共享定义,所以 router 文件里不能出现只存在于 Node 环境的东西,比如直接访问process.env.DATABASE_URL这种代码。否则类型推导虽然能过,运行时打包会出错。官方脚手架那套server/和trpc/的目录分隔就是专门为这个设计的,建议尽量保持不动。

4. 部署与上线:t3code 的最后一公里

4.1 迁移数据库与准备种子数据

开发环境跑通后,部署到线上的第一步是搞定生产数据库。我在 t3code 的部署流程里用的是托管平台的 PostgreSQL,而不是本地 SQLite,原因很简单:线上需要一个多实例都能访问的共享数据库,文件型数据库扛不住多进程并发。切换数据库只需要改.env里的DATABASE_URL,然后在本地执行:

npx prisma migrate deploy

migrate deploy会用已有迁移文件把线上数据库结构同步到位,它和migrate dev的区别是前者只应用迁移不生成新迁移,适合 CI/CD 环境。执行完我还会跑一下种子脚本,插入几个测试用户和帖子,方便部署后立刻验证页面渲染是否正常。种子文件放在prisma/seed.ts,里面用db.user.create批量创建数据,这个文件不参与生产请求路径,只在调试阶段用得上。

数据库层还有一个必须注意的点:连接池。很多托管数据库对连接数有限制,而 Next.js 在服务端渲染时会并发建立大量数据库连接。Prisma 官方建议在生产环境用连接池 URL 格式,比如 PostgreSQL 平台给的?pgbouncer=true参数。我在 t3code 初期没用这个参数,上线后一压测就看到连接数打满导致 500 报错,排查了半天才意识到是连接池没开。这个坑特别典型,建议你部署前就把连接池 URL 配好。

4.2 构建项目与选择托管方式

t3code 的构建命令是官方固定好的:

npm run build

这个命令会先跑一遍类型检查,再执行next build。类型检查这一步特别重要,因为 tRPC 的类型错误在开发环境里大多是编辑器提示,真正build的时候才会因未提交的类型错误直接失败。我第一次构建时,因为一个组件里把post.author.name写成了post.authorName,编译直接报错,把问题拦在了上线之前。

托管方式我试过两条路,都有参考价值。第一条是 Vercel,全程零配置:把 Git 仓库连到 Vercel,导入项目后它会自动识别 Next.js,设置好环境变量点部署就行,前后不到十分钟。第二条是自托管 VPS,用 Docker 跑一个容器,将构建产物用next start启动。自托管需要自己处理 Nginx 反向代理、HTTPS 证书、进程守护等事,多花不少精力,但对某些对部署环境有特殊要求的项目来说是必须的。我这次 t3code 最终放在一台轻量服务器上,Docker Compose 里同时起了 Next.js 容器和 PostgreSQL 容器,配置大概是:

services: web: build: . ports: - "3000:3000" environment: DATABASE_URL: ${DATABASE_URL} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} NEXTAUTH_URL: ${NEXTAUTH_URL} db: image: postgres:16 volumes: - pgdata:/var/lib/postgresql/data

这里要特别说明NEXTAUTH_URL在生产环境必须填线上域名,比如https://t3code.example.com,否则魔法链接里的回调地址会指向 localhost,用户根本登不进来。这个变量是初学者最容易忽略的配置,没有之一。

部署完成后,我习惯先用 curl 测一下首页状态码,再手动跑一遍登录、发帖、看帖三个流程。如果页面能正常渲染、发帖成功跳回列表,说明整个链路在生产环境已经活起来了。

5. 常见问题与排查技巧实录

5.1 类型不同步:改了 router 但前端还是旧类型

这是 t3code 开发中最常见的现象。你在后端给post.all的返回值加了一个新字段,回头看前端,编辑器提示里还是旧结构。多数情况下重启一下 dev server 就行,因为 Next.js 的类型生成有缓存。官方脚手架用的是.next/types缓存目录,我遇到这种情况直接用:

rm -rf .next npm run dev

如果重启还不生效,检查一下你的路由文件是不是真的在主appRouter上挂好了。记得刚开始我把postRouter写到了root.ts里但忘了导出,前端api.post直接不存在,编辑器爆红一片。这种错误其实更容易排查——找不到属性,大概率是挂载路径写错了,去root.ts看一眼马上就会明。

还有一个小技巧:升级 tRPC 大版本后,比如从 v10 升到 v11,很多 API 的调用方式会变化,类型定义也换了。这时直接跑一遍npm run build看类型报错,比在编辑器里一个文件一个文件翻更高效。

5.2 Prisma 模型改了,但业务代码不认新字段

我在 t3code 里曾想把 Post 模型加一个title字段,改完schema.prisma之后,直接在业务代码里访问post.title,编辑器提示类型不存在。正确的顺序应该是先改 schema,再执行npx prisma migrate dev --name add_post_title,让 Prisma Client 重新生成类型定义。这里的逻辑是:Prisma Client 的类型是从 schema 自动推导的,schema 改了客户端类型没重生成,代码里自然访问不到新字段。

这个顺序问题很多人都会踩,而且报错信息还不直观,只看得到一团类型错误。我的排查套路是先跑:

npx prisma generate

如果还不生效,再看node_modules/.prisma/client的时间戳是否更新。印象中还有一次是migrate dev命令卡住,提示数据库有 drift——原来是我手动在 SQLite 里改过表结构,后来我把开发数据库删了重建才恢复干净。用 Prisma 管理表结构时尽量别手工碰数据库,schema 才是唯一权威来源。

5.3 登录状态刷新就丢:Cookie 与会话配置排查

t3code 部署后我遇到过最诡异的问题:本地登录后一切正常,部署到线上只要一刷新页面就回到未登录状态。打开浏览器 DevTools 一看,next-auth.session-token这个 Cookie 每次刷新都会变甚至消失。查了资料才知道,NextAuth 默认的 session Cookie 有几个关键参数没配置,导致浏览器不保存或者保存了但在跨域情况下不发送。

解决方案是在authOptions里显式设置 Cookie 策略:

cookies: { sessionToken: { name: `next-auth.session-token`, options: { httpOnly: true, sameSite: "lax", path: "/", secure: process.env.NODE_ENV === "production", }, }, },

secure字段在开发环境必须设成 false,因为 localhost 走的是 HTTP 而不是 HTTPS,浏览器默认不会保存 secure cookie。部署到 HTTPS 环境后,这个字段会自动打开。另外同一时间,我还发现NEXTAUTH_URL没设对,导致 NextAuth 生成的回调地址是http://localhost:3000,点击魔法链接后落到错误的域名。把线上域名填上后,会话稳定保持,问题彻底解决。

5.4 一个容易被忽略的部署细节:构建时数据库连接失败

还有一次部署失败的经历值得分享。我在服务器上用 Docker 跑项目,docker build阶段执行prisma generate时需要读取DATABASE_URL,但那个阶段环境变量还没注入,导致构建直接失败。这其实是 Next.js + Prisma 项目里的经典坑。解决方案是把prisma generate移到构建之后的启动阶段执行,或者确保docker build过程中可以通过 ARG/ENV 拿到数据库 URL。我用的方案更简单:在 Dockerfile 里先复制package.json和prisma/schema.prisma,安装依赖时执行一次prisma generate,这时提供一个临时占位符 URL,比如:

DATABASE_URL="file:./dev.db" npx prisma generate

因为generate只需要 schema 结构,不需要真正连数据库,占位符完全够用。启动容器时再用真实环境变量替换即可。这个操作在官方文档里没有直接写明,但实际部署几乎人人都会碰到,属于新手必踩的经典门坎。

6. 我的一些个人操作体会

这一路把 t3code 从脚手架推到线上,我最深的感受是:工具链的“类型安全”从来不是一蹴而就,而是每一层都做对之后叠加出来的复利。数据库模型有 Prisma 守着,API 入参出参有 tRPC + zod 守着,会话状态有 NextAuth 守着,前端组件有 TypeScript 守着。这些守卫单独看都不稀奇,但它们连成一条线以后,整个项目就像被一条看不见的轨道托着,想写出“脏代码”反而变得很难。

我也试过在别的项目里把 tRPC 换成 REST,结果没到一个星期就开始想念这种“函数直达”的开发体验。你写后端时加一个字段,前端同事不需要问接口文档,编辑器自己就知道;你写前端时调一个方法,参数错误在保存那一刻就炸红。对于三五人的小团队,尤其对于一个人要同时维护前后端的独立开发者,这套组合带来的心智负担下降非常可观。

最后再分享一个小技巧:如果你也想搭一个 t3code 类似的项目,但又不知道先写什么功能,直接从“用户发帖、首页看帖”开始准没错。它覆盖了数据库、鉴权、API、UI 渲染、类型联动的全部关键路径,做完这一条链路,你对整个 T3 Stack 的掌控力会上一个台阶。等跑通之后,再往里面加个关注、点赞、评论功能,几乎就是重复劳动了。工具只是手段,项目才是目的:把类型安全变成肌肉记忆之后,再回去写普通全栈项目,你会发现自己已经没法忍受没有类型保护的日子了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询