
说实话第一次看到t3code这个词我愣了一下。它不是某个冷门的Linux发行版也不是什么新出的编程语言而是过去两年全栈开发圈里被反复讨论的 T3 Stack 在实际项目里落地时的“代码形态”。如果你关注过 Theo 的 T3.gg 社区那你大概率已经听过 Next.js、TypeScript、tRPC、Prisma 这几个名字被绑在一起重复轰炸如果你还没听过那这篇文章正好帮你把这一整套组合讲透。t3code本质上就是“用 T3 技术栈写出来的应用代码长什么样、为什么这么写、以及实际跑起来会遇到哪些坑”。它想解决的问题非常具体前后端类型割裂、API 文档维护成本高、数据库模型与接口层各自为政。适合想搭一个全栈项目、但不想在类型安全上妥协的开发者参考。这篇东西不是照搬官方文档而是把我自己从脚手架初始化到上线部署这一路遇到的设计取舍、类型推断原理、报错排查经验整理出来。你会看到为什么 T3 社区反复强调“端到端类型安全”也会看到npx create-t3-applatest之后那几十个文件分别承载什么职责。就算你还没决定要不要上车先看完这套组合拳怎么打再回去写你的 REST API心里也会更有底气。1. t3code 到底是什么T3 技术栈在解决什么问题1.1 T3 Stack 不是新框架而是一套组合方案先纠正一个容易混淆的认知T3 Stack 不是某个公司发布的统一框架它是一个社区约定。T3 这个名字来自 Tier 3由 Theo 等人在 T3.gg 社区里总结出来的一套全栈应用技术组合核心成员包括Next.js 作为前端和 API 层框架TypeScript 作为全栈类型系统tRPC 作为前后端接口通信工具Prisma 作为数据库访问层Tailwind CSS 做样式NextAuth.js 做认证可选但经常出现t3code这个标题我理解就是在强调这套技术栈下的编码实践。它不是说你必须把上面所有东西都塞进项目里而是当你选择用这套组合时代码应该怎么组织、类型如何打通、数据流如何流转。T3 社区有个著名的原则叫“safe by default”意思是默认配置尽量安全把类型安全、错误处理、环境变量校验这些事前置到脚手架阶段让你少踩坑。我见过不少新手拿到 create-t3-app 生成的工程后一头雾水因为里面既有src/server/api/routers又有src/trpc还有.env里一堆DATABASE_URL、NEXTAUTH_SECRET。这很正常我第一次跑通的时候也不知道这些目录之间是怎么协作的。这篇文章后面的章节会一层层拆开讲让你搞明白每块代码为什么长这个样。1.2 端到端类型安全从数据库到 UI 一条链路不留死角如果你以前写过传统 CRUD 应用大概经历过这种状态后端定义好接口、写文档、前端对着文档重新声明一遍类型后端改了字段名前端如果没同步改就等着运行时出错或者更倒霉——类型没错但是传参错了报错信息绕几层才找到根因。T3 的核心理念就是终结这种“两头各维护一份类型”的窘境。用仓库管理的例子来类比。数据库表结构是仓库里的库存清单Prisma 根据 schema 生成类型后仓库清单就有了标准格式。tRPC 在服务端定义接口时输入参数用 zod 校验、输出数据由 Prisma 自动推断相当于把仓库清单固定成唯一可信版本。前端调用接口时tRPC 通过 TypeScript 类型把服务端的“目录”直接暴露给客户端你写错字段名、少传必填参数IDE 里就立刻标红不需要等运行时报错。这样的设计带来的实际收益非常直接。第一接口文档基本可以不用手写了类型本身就是活的文档第二前端和后端之间的沟通成本大幅降低改动字段名会影响哪些调用点编译器帮你在保存那一瞬间就找出来第三运行时错误大幅减少大部分低级类型错误在编码阶段就被拦截了。T3 社区把这种模式叫做“端到端类型安全”也是t3code风格代码最核心的特征。2. 技术栈拆解每一层都是干什么的为什么选它2.1 Next.js 做骨架路由和渲染方式的统一出口很多人第一反应是“前端用 React后端自己搞一套 Express 不就行了为什么要上 Next.js”答案是如果你选择 tRPC 做通信层Next.js 的 API Routes 或者 App Router 的 Route Handlers 能让你把前后端放在同一个工程里托管省掉跨域、本地开发代理、部署环境不统一这一堆破事。在t3code的场景里Next.js 承担的不只是 SSR 或 SSG 那些页面渲染能力它更重要的角色是“所有代码的宿主”。你的 tRPC router 挂在 Next.js 的接口路径下前端页面通过 RPC 链接指向同一个应用部署的时候只需要启动一个 Node.js 服务就行。相比传统前后端分离少维护一套基础设施对个人项目和小团队特别友好。如果你还在 Next.js 的 App Router 和 Pages Router 之间纠结我的建议是新项目直接走 App Router。T3 社区官方模板现在默认提供 App Router 版本tRPC 在 App Router 下的集成方式也更符合未来的方向。Pages Router 不是不能用了但除非你有存量项目要维护否则没必要在新项目里走回头路。2.2 Prisma数据库的 schema 就是类型的唯一来源Prisma 在 T3 里的地位比“一个 ORM”要重要得多。它不仅是操作数据库的工具更是整个类型系统的起点。Prisma Schema 文件定义了数据模型、字段类型、表关系然后通过prisma generate生成类型安全的客户端代码这个客户端会在 tRPC 的路由中被直接调用。我以 SQLite 为例给你一个最基础的模型定义model Todo { id String id default(cuid()) title String completed Boolean default(false) createdAt DateTime default(now()) }执行npx prisma db push之后这个表会出现在数据库里执行npx prisma generate之后你就能在代码里这样写const todos await db.todo.findMany();db.todo的类型是带着字段名和类型自动推断的findMany返回的每一项都有id、title、completed、createdAt的类型信息。这套类型会一直传到 tRPC router 的输出里再传到前端 hooks 的返回值里。你不需要在前端单独定义Todo接口因为 Prisma 已经帮你把类型整个链条打通了。实际项目中我强烈建议把schema.prisma当作数据库设计的唯一入口不要手动去数据库里加字段。改动模型后跑一次migrate或者db push让数据库结构和类型同步更新避免“数据库里有列但类型里没有”的撕裂问题。2.3 tRPC让前端像调用本地函数一样调用后端tRPC 是t3code最特色的部分也是最容易劝退新人的部分。它的核心思路极其激进不生成 OpenAPI schema不定义 REST 资源路径也不搞 GraphQL 那套查询语言而是直接在服务端定义函数把函数类型共享给前端。服务端路由定义长这样import { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { db } from ~/server/db; export const todoRouter createTRPCRouter({ getAll: publicProcedure.query(async () { return db.todo.findMany(); }), add: publicProcedure .input(z.object({ title: z.string().min(1) })) .mutation(async ({ input }) { return db.todo.create({ data: { title: input.title } }); }), });前端调用时不需要手写 URL 拼接不需要手写 fetch 封装更不需要自己声明返回值类型const { data: todos } trpc.todo.getAll.useQuery(); const addTodo trpc.todo.add.useMutation({ onSuccess: () utils.todo.getAll.invalidate(), });注意trpc.todo.getAll这个路径它是从服务端 router 的嵌套结构自动生成的。你服务端怎么定义客户端就怎么调用类型跟着路由结构走。这就是 tRPC 的魔法所在——它不发明网络协议而是把 TypeScript 的类型系统当作 API 契约来用。有人会问tRPC 是不是只能在 Next.js 里用其实不是它可以搭配 Express、Fastify甚至独立部署。但在t3code的场景里它和 Next.js 是一对最佳拍档因为你可以在同一个进程里完成页面渲染和接口调用省掉了协议序列化和反序列化的开销。2.4 Tailwind 和 NextAuth不是必须但用了就回不去Tailwind CSS 在 T3 里承担的是 UI 层“提速器”的角色。你不需要思考类名怎么命名直接在 JSX 里用原子类拼出布局和样式div classNamemx-auto max-w-2xl p-4 h1 classNametext-2xl font-bold text-slate-800我的待办事项/h1 /divTailwind 和 TypeScript 一样属于“写的时候越小心维护的时候越省心”的工具。它的内容扫描机制会只打包你用到的类名生产环境 CSS 体积控制得很好。如果你喜欢组件化开发配合 shadcn/ui 这类基于 Tailwind 的组件库开发速度会非常快。NextAuth 则是认证模块的热门选择尤其是社交登录需求多的时候。它和 Prisma 的集成靠next-auth/prisma-adapter实现你只要在schema.prisma里定义好 Account、Session、User 这些模型NextAuth 就能把用户会话挂到数据库里。但我要提醒一句如果你的项目不需要登录别硬上 NextAuth。T3 社区的意思是“需要的时候再加”而不是“有就用”。认证会引入额外的数据表和会话逻辑纯工具类的内部项目加它纯粹是给自己找麻烦。3. 亲手跑通一个 t3code 项目从初始化到实现 CRUD3.1 用脚手架起步推荐选项这么选官方脚手架create-t3-app是进入 T3 世界最快的路径。一条命令就能生成一个带类型安全、Prisma、tRPC 配置完整的工程npx create-t3-applatest my-t3-project交互式命令行会问你要不要启用 Tailwind、Prisma、tRPC、NextAuth。我的建议是第一次尝试时全勾上这样能完整看到这套组合的全貌等你熟悉了以后再按需裁剪。初始化完成后目录结构大致这样my-t3-project/ src/ app/ # 前端页面App Router 结构 pages/ # 早期模板会有新版默认没有 server/ api/ root.ts # 所有 router 汇总出口 routers/ # 每个业务模块一个 router db.ts # Prisma 客户端实例 trpc/ server.ts # 路由、上下文创建 client.ts # 客户端调用工具 validators/ # 你可以自己加 zod 校验文件 prisma/ schema.prisma # 数据库模型 .env # 环境变量脚手架自带一个“typesafe env”机制就是说.env里的变量在代码中被访问前已经做了运行时校验。这看起来有点“过度设计”但实际踩过“数据库连不上才发现 DATABASE_URL 写错”的坑后你会明白这一步帮你省了多少时间。3.2 定义数据模型并和数据库同步我演示一个简单的“项目任务管理”场景你在prisma/schema.prisma里加一个 Task 模型model Task { id String id default(cuid()) title String done Boolean default(false) createdAt DateTime default(now()) }然后在终端执行npx prisma db push这个命令直接把 schema 同步到数据库不需要生成迁移文件适合开发阶段快速迭代。正式环境的数据库结构变更建议用npx prisma migrate dev它会生成版本化的迁移记录方便团队协作和回滚。跑完db push后再执行npx prisma generate生成最新的 Prisma Client让db.task这个对象具备完整的类型提示。如果你用的是 VS Code这个步骤完成后可能需要重启一下 TS Server按 CtrlShiftP输入“TypeScript: Restart TS server”才能让类型感应立刻刷新。3.3 编写 tRPC 路由zod 是守门员接着在src/server/api/routers下新建task.tsimport { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { db } from ~/server/db; export const taskRouter createTRPCRouter({ list: publicProcedure.query(() { return db.task.findMany({ orderBy: { createdAt: desc } }); }), create: publicProcedure .input(z.object({ title: z.string().min(1).max(100) })) .mutation(async ({ input }) { return db.task.create({ data: { title: input.title } }); }), toggle: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input }) { const task await db.task.findUnique({ where: { id: input.id } }); if (!task) throw new Error(任务不存在); return db.task.update({ where: { id: input.id }, data: { done: !task.done }, }); }), });这里publicProcedure表示无需登录即可访问如果你的项目有认证可以换成protectedProcedureT3 脚手架默认帮你写好了一个会检查 session 的受保护过程。zod的 input 校验非常关键它既拦截非法参数又自动推断出输入的类型前端调用时传错类型会直接编译报错。在root.ts里把taskRouter挂到总路由下export const appRouter createTRPCRouter({ task: taskRouter, });到这里后端部分就算完成了。服务端没有单独写路由前缀也没有写 HTTP handler就完成了对外数据访问能力的定义。3.4 前端通过 hooks 调用后端的流程在页面组件中调用任务接口use client; import { api } from ~/trpc/react; export default function TaskBoard() { const { data: tasks, isLoading } api.task.list.useQuery(); const createTask api.task.create.useMutation({ onSuccess: () { // 刷新列表 utils.task.list.invalidate(); }, }); if (isLoading) return div加载中.../div; return ( ul {tasks?.map((task) ( li key{task.id}{task.title}/li ))} /ul ); }api.task.list.useQuery()这个 hooks 的返回值tasks类型就是Task[]你不需要再引入任何type Task的定义。多写一个字段、少写一个字段、字段类型不匹配IDE 都能看出来。onSuccess里调用的invalidate是 tRPC 内置的缓存刷新机制它会让该查询自动重新拉取最新数据。你可能已经注意到整条链路里没有出现 fetch、axios、接口 URL。这就是t3code的爽点——你用写代码的舒服换掉了调试接口的麻烦。当项目规模变大几十个查询和变更操作都是这种“函数式调用”维护成本相比 REST 风格要低很多。4. 实战中容易踩的坑类型不同步、环境变量和数据库迁移4.1 改了后端路由前端类型却没更新这是 T3 项目里最常见的抱怨之一。你在服务端新增了一个delete过程回到前端发现api.task.delete根本不存在类型报错显示找不到这个属性。原因很可能是 TypeScript 类型信息被缓存了。我先给出最直接的三种排查手段按 CtrlShiftP执行 “TypeScript: Restart TS server”如果有.next目录产生的缓存删掉后重新npm run dev检查tsconfig.json中strict模式tRPC 的类型推断依赖严格模式如果你关了严格模式类型保护会大打折扣大多数情况重启 TS Server 就能解决。如果你改了 Prisma Schema 和 tRPC Router 后前端还是旧类型先执行npx prisma generate再重启 dev server。T3 的类型是从服务端往客户端单向流动的服务端类型没刷新前端看到的永远是旧版。我还遇到过一个更隐蔽的问题VSCode 有多个 TypeScript 版本项目装了新版本 TS但编辑器还在用旧版本。打开 VSCode 右下角的 TypeScript 版本提示确认是否选择了“Use Workspace Version”。这个不匹配会导致类型检查结果飘忽不定折腾很久才发现问题不在代码本身。4.2 环境变量行配置错误导致数据库连不上的坑Prisma 的DATABASE_URL如果配错所有数据库操作都会报错常见报错是Cant reach database server。排查时要依次检查三个地方.env文件是否存在且内容正确.env.example是否把必填变量列全方便同事或未来的你参考数据库地址是否包含正确的端口、用户名、密码如果你用的是 Postgres 或 MySQLSQLite 本地开发时DATABASE_URL长这样DATABASE_URLfile:./db.sqlite文件路径要以prisma目录为参照不要写错层级。我建议新手先用 SQLite 跑通全链路再切换到外部数据库避免一开始就被连库问题分心。如果你是跟着脚手架启用了 NextAuth一定不要遗漏NEXTAUTH_SECRET。这个变量如果缺失或太短NextAuth 会直接拒绝启动或者登录后 session 无法持久化。生成一个随机 secret 用这条命令openssl rand -base64 32把输出结果复制到.env里。4.3 Prisma 迁移冲突和表已存在报错npx prisma db push适合开发初期但它不会记录迁移历史。等到项目上线我建议切换到npx prisma migrate dev来管理数据库变更。如果你的数据库里已经有旧表执行迁移时可能出现 “table already exists” 的报错因为迁移记录里不承认这些表之间的关联关系。一个比较省心的处理流程先备份现有数据库执行npx prisma migrate dev --name init它会尝试创建迁移基线如果报冲突考虑用npx prisma db pull先把现有库结构反向转换为 schema再把需要新增的字段加上去重新执行npx prisma migrate dev这里面的经验是不要在别人已经放满数据的表上轻易执行migrate reset那会把数据清空。开发环境无所谓生产环境一旦手滑恢复会非常痛苦。动手前多花 30 秒看看prisma/migrations目录里有没有历史迁移记录能避免一次没有退路的重置。4.4 NextAuth 和 Prisma 类型不匹配的经典报错如果你启用了 NextAuth又选择了 Prisma 作为 session 存储报错里出现PrismaClientKnownRequestError或Type Account is not assignable这类信息时大概率是schema.prisma里缺少 NextAuth 要求的五个模型Account、Session、User、VerificationToken、Authenticator。一个偷懒但可靠的办法是把官方 adapter 要求的模型直接复制到 schema 里然后执行npx prisma db push。之后在auth.ts里配置 adapter 时就不会再报“找不到表”的错。如果你不需要社交登录只做邮箱密码登录可以考虑暂时不装 NextAuth用 session 手写简化版登录。T3 社区并不强制你走到哪都带 NextAuth项目里少一个依赖就少一类兼容问题。4.5 常见问题速查表现象最可能的原因解决动作前端调用不到新增的 tRPC 过程TS Server 缓存旧类型重启 TS Server必要时删.next目录Prisma 报连接超时DATABASE_URL有误或数据库未启动检查.env用客户端手动连接测试NextAuth 登录后 session 不生效NEXTAUTH_SECRET缺失或过短生成新的 secret 并配置到.env迁移报表已存在已有表结构与迁移记录不一致用db pull反推 schema手工对齐迁移基线Tailwind 样式不生效content 配置里没包含组件文件路径在tailwind.config.ts中检查content数组zod 校验报 undefined前端没传或传了 undefined给 input 加默认值或调整使用处的传参5. 最后一个建议从模仿开始但别把所有工具都搬回家t3code这套风格最大的价值不是它选了什么库而是它把“类型安全”这个概念从开发者的愿望清单变成了默认配置。你在脚手架里写下的第一行 tRPC 代码可能感受不到什么特别但当你维护一个半年没动的项目回来改需求时编译器能直接告诉你改了数据库字段后哪几个地方会挂那种踏实感是传统 REST 栈很难给的。我个人在实际操作中的体会是先完整跑一遍 create-t3-app 的默认模板把每个文件都打开看一遍搞清楚createTRPCContext、createCallerFactory、httpBatchLink这些名字分别做什么。不要急着往上堆业务代码先把架子理解透。之后再尝试把一个你已经写好的小项目迁移过来例如把一个记录收支的网页从“React Axios Express Sequelize”改造成 T3 方案比对前后端类型维护的心智负担。这样迁移一次你比看十篇文章都更能理解为什么社区对它的评价那么高。最后再分享一个我从实战里总结的扩展方向T3 的架构非常适合做独立开发者的 MVP 工具比如内部数据看板、小型 SaaS 后台、自动化脚本的管理界面。因为它降低了全栈类型维护的成本单人开发时不用在接口文档和类型定义之间反复横跳。如果你正打算启动一个快速验证的项目用 T3 做起点大概率不会后悔。