搜索“t3code”的人,大概率不是想找一个具体软件,而是在摸一张技术选型地图。我第一次看到这个词是在create-t3-app的仓库讨论区里,背后其实是t3-oss这套围绕全栈TypeScript的开源方案——核心思路是让前后端共享类型、少写胶水代码、快速得到一个结构清晰的应用骨架。如果你最近正纠结Next.js项目怎么组织、tRPC到底解决了什么问题、Prisma和数据库怎么接,这篇就是我用真实项目完整跑过一轮之后的笔记,从概念到初始化,到改造成业务系统,再到我踩过的几个坑,一次说透。
t3code对我而言更像一个入口词。最初我以为它是某个新框架的名字,翻了一圈才发现这是一条技术路线的代称:TypeScript、Tailwind、tRPC、Next.js这些工具被t3-oss组织整合之后,统一通过create-t3-app脚手架对外分发。真正让我决定上手,是因为团队里一个内部小工具反复在写API客户端类型、手动处理跨域和请求封装,实在烦了。后来用这套方案重写,前后端类型直接共享,数据库建模和接口暴露放在同一个仓库里管,开发体验和之前完全是两个量级。这篇文章写给两类人:刚接触全栈TypeScript、想找一个靠谱起手式的同学,以及已经在用Next.js但没搞明白tRPC和Prisma怎么整合的开发者。下文所有内容基于我一次完整的项目实操,包含初始化过程的每一步、关键代码、部署配置,以及修改过程中遇到的真实报错和排查思路。
1. “t3code”到底是什么:一条技术路线,不是一个软件名
1.1 从社区热词到t3-oss生态
如果你直接搜t3code,看到的往往是零散的源码片段、GitHub链接和技术讨论帖,几乎没有官网首页,也没有产品下载页。我第一次接触时非常不适应,翻了好久才确认:这是一个开源组织下的技术栈统称,而不是某个可执行程序。
t3这个前缀来自开发者Theo Browne的直播间,他在技术交流中反复强调“类型安全、可扩展、可维护”这几条准则,社区讨论多了之后,“t3栈”就成了约定俗成的说法。t3-oss名下最出名的仓库就是create-t3-app和create-t3-turbo,前者面向单个全栈应用,后者面向多应用monorepo。很多刚入坑的人在这里会被绕晕:明明看到的是t3code这个词,进到仓库却要面对一大堆英文选项,不知道从哪下手。实际上你只需要记住,t3code不要求你先学什么新语言,它就是把已有的好工具按一套逻辑组合起来,再用脚手架把组合过程自动化。
1.2 create-t3-app与create-t3-turbo的定位差异
create-t3-app解决的问题非常明确:打开终端执行一条命令,交互式回答几个问题,得到的是一个已经配置好TypeScript、ESLint、Tailwind、tRPC、Prisma、Auth.js的Next.js项目,直接pnpm dev就能跑起来。它不是普通模板仓库,而是一个代码生成器,每次初始化都会按你的选择重新生成项目结构和配置文件,所以拿到的项目基本没有“仓库作者个人偏好”残留。
create-t3-turbo则面向另一个场景:monorepo多端架构。如果你需要多个应用共享同一个数据库和API,比如一个面向内部管理后台、一个面向普通用户,甚至还有一个移动App,turbo版本会把Next.js应用和Expo应用拆成独立工作区,用Turborepo管理任务缓存,用packages/shared统一共享类型和工具函数。对单应用内部工具来说,create-t3-app已经足够;如果你的项目从规划起就要做多端,直接看create-t3-turbo更合适。
1.3 什么场景适合t3,什么场景别硬上
技术选型最忌讳“因为别人说好就用”。我在决定用t3之前先做了个简单判断。
适合用t3的场景:内部工具和中后台项目、需要快速做原型验证的业务、前后端都由同一个小团队维护的场景。这类需求共同的痛点是“不想为API层维护两份代码”,而t3的类型闭环正好把这份重复劳动去掉了。
不适合硬上的场景也有几个:纯前端展示型页面,完全没有后端交互,用t3反而多了一层Next.js服务器抽象;项目已经有独立API服务,并且需要对外暴露标准REST接口,那你大概率得考虑把tRPC网关化或者干脆不引入;团队完全没有人熟悉TypeScript,并且短期没有学习意愿,这时候强推t3等于同时引入接口管理和类型系统两座大山。
| 判断维度 | 适合t3 | 不建议t3 |
|---|---|---|
| 业务类型 | 内部工具、管理后台、全栈业务应用 | 纯静态展示、已有独立API的服务 |
| 团队情况 | 小团队前后端一体,愿意写TypeScript | 团队抗拒类型系统,或前后端严格分离 |
| 交互复杂度 | 表单增删改查为主 | 复杂实时通信、大量第三方API聚合 |
| 上线周期 | 需要快速从DB到页面 | 已有成熟代码生成体系,迁移成本高 |
2. 为什么是“TypeScript + Next.js + tRPC + Tailwind”这四件套
2.1 TypeScript作为全栈契约,把“文档同步”变成“编译器检查”
传统REST架构里,前后端之间靠接口文档沟通。后端改了接口字段,前端要手动修改类型定义和联调逻辑,字段不匹配的问题经常要等到运行时才暴露。t3这套方案的核心转变是:让同一份类型定义同时服务服务端和客户端。
用tRPC之后,router的参数和返回值类型会自动推断到客户端。前端写api.post.create调用时,IDE直接给出完整类型提示,接口字段变了,保存代码的瞬间编译器就开始报错,根本轮不到运行时。这背后真正的价值不是技术多新奇,而是减少了大量心智负担。我自己的项目里不再需要维护一份“后端返回的数据长什么样”的手写类型,类型来自服务端代码推导,永远不会漂移。
2.2 Next.js的地基作用:全栈页面和路由的承载者
Next.js在t3方案里承担的是应用框架角色。App Router推出之后,页面、API Route、Server Component、Server Action都统一在同一个路由模型之下,这对全栈开发非常友好。你可以把页面当作数据库查询结果的渲染出口,把tRPC挂载到/api/trpc这个Next.js路由上,服务器端逻辑和客户端交互混排在同一个项目里。
我个人的体会是,Next.js在t3里的角色更像“地基”,决定你已经有什么房间,经典款还是新装修款。新项目默认用App Router,如果遇到老教程里大量pages目录写法,不用慌,多数t3相关代码已经演进到src/app结构了。App Router的Server Component在某些场景下可以替代部分tRPC查询,这也就引出了我后面要说的一个坑:在页面里既用服务器端取数又用客户端useQuery,容易产生数据重复或缓存错位。
2.3 tRPC的价值:API层被“函数调用”体验降维
对比一下传统REST调用和tRPC调用的心智差异。普通REST要求你定义:method、url、query参数、body、请求头、错误处理。客户端写fetch,设置Content-Type,处理JSON序列化和统一错误。整个过程里有大量纯体力活,而且每写一个接口都要重新来一遍。
tRPC把这一切变成了一个函数调用。服务端定义一个router,export出去,客户端直接使用api.task.create。这个函数自带类型,入参出参都能被IDE提示。对开发者来说,API调用被降维成“导入一个函数”,网络细节全部封装掉。
有人会问:这不就是RPC吗?确实,tRPC本质就是RPC风格,但它选择TypeScript编译器作为“接口定义语言”,利用inferred types实现端到端类型安全。不需要OpenAPI规范文件,不需要代码生成步骤,不需要维护生成的客户端SDK。这也是它和GraphQL最大的区别:GraphQL有一层schema语言,tRPC直接用TypeScript类型当过墙,少一层抽象就少一份维护成本。
2.4 Tailwind的实用主义选择
Tailwind在t3里不是必须品,但create-t3-app默认推荐它。理由很朴素:全栈脚手架的目标是让业务逻辑快速落地,样式方案如果还要团队反复讨论,就违背了初衷。工具类写法适合快速迭代,配合shadcn/ui这类组件库,复制粘贴就能得到一套可以改主题的组件,不用从零设计。
我早期对Tailwind有偏见,觉得HTML里塞一堆类名很乱。实际用了几个项目后,我的结论是:它最大的优点是消除了“取类名”这个隐形负担。全栈项目最忌讳在业务还没跑通时,先花半天想一个BEM命名规范。Tailwind让你先跑起来再说,真的进入需要组件抽象的规模时再抽成组件也不迟。
3. 初始化实操:从命令到能跑起来的项目
3.1 环境准备与包管理器选择
动手之前先把环境配好。Node.js建议18以上版本,我这边用的是20 LTS。包管理器强烈建议用pnpm,而不是npm。原因有三个:安装速度快,依赖磁盘占用低,更重要的是create-t3-app生成的工程对pnpm的现代依赖处理更顺畅,npm在Prisma的postinstall脚本和依赖提升策略上偶尔会踩到权限和版本错位的坑。
# 检查node版本 node -v # 全局安装pnpm(如果还没有) npm install -g pnpm # 开始初始化 pnpm create t3-app@latest3.2 交互式选项到底怎么选
执行上面命令后,脚手架会先问项目名称,然后进入技术选项确认。我用自己的组合逐项说明。
| 选项 | 我的选择 | 原因 |
|---|---|---|
| TypeScript | yes | t3的根就是类型安全,选否等于自废武功 |
| ESLint | yes | 多人协作时统一代码风格,老项目补上很痛苦,新项目一定要开 |
| Tailwind CSS | yes | 内部工具要快速做界面,有它不用来回切样式文件 |
| React | yes | App Router默认,这就是Next.js当前的开发方式 |
| tRPC | yes | 前后端类型共享的核心,不选它就不用叫t3栈了 |
| Prisma | yes | 项目要存数据,这是最顺手的ORM选择 |
| Auth.js | 暂时no | 第一步先跑通数据和接口,登录后面再补 |
| Docker | no | 本地开发不需要容器,部署走Vercel |
| Import alias | yes | 默认@/别名,路径简洁,后期重构方便 |
这里多说一句Auth.js(也就是NextAuth v5)。如果你是给公司内部工具加登录,建议第一次先不选Auth.js,因为一旦勾选,脚手架会额外生成auth相关目录和配置文件,并且环境变量没配好时整个应用可能启动失败。先把没有鉴权的完整链路跑通,再手动加登录,排查范围会小很多。
3.3 初始化完成后检查的关键文件
项目生成后,我会先看几个位置,确认结构符合预期。
src/ app/ api/trpc/[trpc]/route.ts # tRPC的HTTP入口,挂在Next.js路由上 layout.tsx # 全局布局 page.tsx # 首页 server/ api/ root.ts # 子路由统一注册到这里 routers/ # 业务路由目录 trpc.ts # tRPC实例和鉴权上下文 db.ts # Prisma客户端 trpc/ server.ts # 服务端调用tRPC用 react.tsx # 客户端Provider和hooks封装 shared.ts # 前端调用的创建函数 prisma/ schema.prisma # 数据模型 .env # 环境变量,注意不要提交到git然后执行启动命令:
pnpm dev看到终端出现ready提示后,打开localhost:3000,t3默认首页会出现。到这个节点,脚手架本身已经验证通过。
4. 从样板到业务:数据库建模、登录保护和部署上线
4.1 用Prisma建第一张业务表
我拿一个实际场景来演示:团队任务速记板。需求很简单,所有登录成员可以创建任务,标记完成,按时间倒序查看。先改prisma/schema.prisma:
generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Task { id String @id @default(cuid()) title String content String? done Boolean @default(false) createdAt DateTime @default(now()) }把.env里的DATABASE_URL暂时指向本地SQLite文件:
DATABASE_URL="file:./dev.db"执行迁移:
pnpm prisma migrate dev --name init这条命令会创建数据库文件、生成Prisma Client,并且自动执行db push。如果你改过schema但不想生成迁移记录,可以用prisma db push,但正式项目我习惯走migrate,方便追溯表结构变化。
4.2 写tRPC路由并注册
在src/server/api/routers/task.ts里写业务路由:
import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure, } from "~/server/api/trpc"; export const taskRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.task.findMany({ orderBy: { createdAt: "desc" }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.task.create({ data: { title: input.title, content: input.content, }, }); }), toggle: protectedProcedure .input( z.object({ id: z.string(), done: z.boolean(), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.task.update({ where: { id: input.id }, data: { done: input.done }, }); }), });写完路由后,必须回到src/server/api/root.ts做注册,否则客户端根本找不到这个接口。
import { taskRouter } from "~/server/api/routers/task"; import { createTRPCRouter } from "~/server/api/trpc"; export const appRouter = createTRPCRouter({ task: taskRouter, });客户端调用时非常直接:
import { api } from "~/trpc/react"; function TaskList() { const { data, refetch } = api.task.list.useQuery(); const createTask = api.task.create.useMutation({ onSuccess: () => refetch(), }); return ( <ul> {data?.map((task) => ( <li key={task.id}>{task.title}</li> ))} </ul> ); }4.3 给任务板加上登录保护
任务板说是内部工具,不能裸奔。我后来手动补了Auth.js(NextAuth v5)。先装依赖,再配置环境变量:
AUTH_SECRET="一个足够长的随机字符串" AUTH_URL="http://localhost:3000"加上GitHub Provider环境变量后,在trpc.ts里把鉴权信息挂到上下文:
export const createTRPCContext = async (opts: CreateTRPCContextOptions) => { const session = await getServerAuthSession(); return { db, session, ...opts, }; };protectedProcedure的写法就体现在这里:程序会先检查ctx.session里有没有用户,没有就直接抛UNAUTHORIZED错误。前端用useSession判断登录态,未登录跳到登录页。这一步其实比想象中简单,真正容易出问题的是环境变量没配置齐全,导致启动时Auth.js初始化失败。
4.4 部署到生产环境的正确姿势
本地跑通之后,部署我选Vercel。首先要把数据库从SQLite换成Postgres,因为Vercel的无服务器环境文件系统是临时的,SQLite文件在函数实例销毁后会丢失。推荐Neon或者Supabase的免费Postgres实例。
调整.env:
DATABASE_URL="postgresql://user:password@host/db"还需要在package.json里加上postinstall脚本:
{ "scripts": { "postinstall": "prisma generate" } }这个脚本非常重要。Vercel构建时会执行安装依赖,但不会自动触发Prisma Client生成,不加这个脚本,部署后的应用会报“PrismaClient is not defined”一类的错误。
构建配置全部走默认即可,框架会识别Next.js项目。部署完成后,记得把AUTH_URL改成正式域名,AUTH_SECRET重新生成一个生产用的强随机值。我见过太多人把开发环境的secret直接带到线上,出于安全考虑,这个习惯一定要改。
5. 实测踩坑:几个真实报错的完整排查链路
5.1 启动崩溃:Node版本和包管理器不一致
第一次在另一台机器上clone项目时,我执行pnpm install后直接pnpm dev,终端立刻报错,提示Prisma引擎二进制无法加载。我第一个反应是重新生成Prisma Client,执行pnpm prisma generate,结果问题依旧。随后我检查node版本,才发现那台机器上的Node是16,而create-t3-app生成的工程在package.json里要求18以上。
排查链路其实是这样的:报错信息属于Prisma引擎层,但根因在Node版本不满足。pnpm install虽然成功,可引擎二进制在低版本Node下编译过程崩溃,部分文件缺失,导致运行时不完整。解决办法简单粗暴:升级Node到20,删除node_modules和pnpm-lock.yaml重新安装,所有引擎二进制重新下载,启动恢复正常。
5.2 tRPC类型不匹配:客户端报错却不知道去哪看
调试tRPC最难受的一点是,很多错误信息在服务端,而前端React Query只给一个笼统的“请求失败”。我遇到过一次查询报错,刷新页面偶尔能看到数据,过一会儿又消失,毫无规律。当时我盯着前端的useQuery看了半天,完全没头绪。
后来我静下来梳理排查路径。第一步,打开浏览器Network面板,看/api/trpc/task.list的实际HTTP响应,发现返回500。第二步,看Vercel或本地终端里的服务端日志,定位到Prisma查询报错,提示表不存在。第三步,想起来我只改了schema,并没有执行迁移。执行pnpm prisma migrate dev后,表建出来,接口立即恢复。这个坑提醒我:tRPC的前后端分层让错误边界很清楚,前端拿不到具体堆栈,不代表没有堆栈,去服务端日志里找永远比瞎猜快。
5.3 环境变量前缀的严重风险
还有一个坑属于安全意识问题。有个同事把Postgres连接串写成了NEXT_PUBLIC_DATABASE_URL,理由是“怕服务端读不到”。这个做法极其危险:Next.js会把所有NEXT_PUBLIC_开头的环境变量在构建时内联进客户端JavaScript包,也就是说数据库地址、用户名、密码全部暴露在浏览器里,谁打开页面按F12都能看得一清二楚。
修复方式是区分两类变量。客户端能安全读取的,只有那些明确打算公开展示的配置,比如NEXT_PUBLIC_APP_NAME;涉及数据库连接串、密钥、token的变量,一律不带前缀,只留在服务端环境里。改完之后,我在打包产物里搜索了一圈数据库地址,确认没有泄漏才敢上线。给所有用Next.js的人一个建议:默认情况下,所有环境变量都不要加NEXT_PUBLIC_前缀,只有当你确定某个值可以公开时再加。
5.4 Server Component和tRPC的数据同步问题
App Router时代有个特殊坑:页面既能作为Server Component直接读数据库,也能用tRPC的useQuery在客户端取数。结果就是同一个列表,服务端渲染了一份,React Query又拉了一份,两者状态不同步,用户操作后界面闪烁甚至短暂显示过期数据。
我在任务板页面里改了一版,改为彻底让客户端tRPC负责列表加载,Server Component只负责静态的页面外壳和元信息。如果确实需要服务端取数,就用React Query的hydration机制做数据透传,而不是简单地在Server Component里裸调Prisma查询后又被客户端重复请求。这一点很多教程不会讲,等到数据结构复杂了才发现,会很痛苦。
6. 关于t3这套方案的个人建议与后续扩展
6.1 第一套t3项目的推荐配置组合
如果你想照着这篇文章做自己的第一个t3项目,我建议在初始化时选择:TypeScript、ESLint、Tailwind CSS、React、tRPC、Prisma,暂时不开Auth.js。这组配置能用最少的外部依赖把“数据库到接口到页面”的全链路跑通,让你先感受类型共享带来的开发体验。
等这个基础项目已经稳定,再去手动添加Auth.js。顺序很重要:鉴权一旦加进来,就会涉及session、上下文、受保护路由多个环节同时工作,如果数据和接口链路还没跑顺,排查问题的复杂度会成倍上升。我见过太多初学者一上来全选了,结果启动报错后根本分不清是数据库问题还是Auth配置问题。
6.2 可以继续扩展的几个方向
项目做完之后,有几个平滑的演进路径值得考虑。第一,把SQLite换成Postgres,数据可靠性会好很多,改动量主要在连接串和本地环境变量,schema基本不用动。第二,给项目加上shadcn/ui,表格、弹窗、表单组件直接复制进来改主题,比手写Tailwind类名效率更高。第三,如果后续要做移动端,把代码迁到create-t3-turbo的monorepo结构,共享API层,避免为App单独写一套后端。
还有一个我最近在琢磨的方向:什么时候该把tRPC的一部分逻辑换成Server Actions。App Router的Server Action在表单提交场景下写起来更直观,但类型共享和统一缓存策略不如tRPC成熟。我的判断是,内部工具这类重表单、重列表的项目,tRPC还是最稳的选择;如果项目形态变成以内容展示为主、交互很少,Server Actions会轻便很多。
最后分享一个实操习惯:每次改完schema,我都会第一时间执行prisma migrate dev --name <有意义的命名>,而不是拖到最后。小步提交迁移记录,出问题时可以直接回退到某一个历史版本,这种容错能力在多人协作时价值巨大。t3这套组合本身没有太多高深概念,真正决定项目体验的,往往是这些不起眼的操作习惯。