
做前端的这几年我前后试过不少脚手架方案从最早的 CRA 到后来的 Vite再到各种带模板的全家桶总感觉差了点什么。要么生成出来是一个只会跑 demo 的空壳要么类型定义散落一地前后端全靠手写接口文档硬撑。直到我遇到 t3code 这个方案体验完全不一样——它是基于 T3 Stack 理念做的一套全栈项目脚手架把 TypeScript、Next.js、Tailwind、tRPC、Prisma、NextAuth 这些工具按照“端到端类型安全”的思路整合在一起。换句话说你写完数据库模型类型能一路跑到浏览器组件里中途不用手动维护任何接口定义改一个字段整个链路上引用它的地方都会跟着变。这个体验对于想快速搭 MVP、或者团队想统一技术规范的人来说吸引力非常大。这篇文章我会从 t3code 的核心理念讲起拆解它为什么选这套技术组合然后带着你把一个项目从初始化跑到数据库迁移、写接口、调页面最后把常见的坑和排查思路一并整理出来。不管你是刚从前端转全栈的新手还是已经在团队里负责脚手架选型的老兵这篇文章给你的东西都应该用得上。1. 先说清楚 t3code 到底在解决什么问题1.1 T3 Stack 是什么为什么它值得专门做个脚手架T3 Stack 不是某个单一框架它是一组工具的组合思路。核心就四个字类型安全。TypeScript 在前后端之间不再是一层“装饰”而是真正贯穿全链路的契约。你定义在 Prisma Schema 里的模型生成的类型既服务于后端查询也能被前端通过 tRPC 直接引用你给某个接口定的入参校验规则比如标题必填、最小长度前端调用时同样能在编辑器里看到对应的类型提示。这种体验不是普通的 RESTful API 能给的——REST 需要手动写接口文档、维护 Swagger、生成 client SDK一旦字段变了文档和代码很容易同步漂移。t3code 就是围绕这套组合做的一个初始化工具。它把 T3 Stack 里最常用、也最难手工配置的几块拼在了一起Next.js 负责应用框架和 API 路由tRPC 负责前后端类型安全的函数调用Prisma 负责数据库建模和迁移NextAuth 负责认证Tailwind 负责样式。你不需要花一下午去读五份配置文档再纠结版本兼容性。跑一条命令选几个选项项目结构就出来了而且每个文件都能讲清楚它是干嘛的。1.2 传统脚手架方案的三个让人头疼的地方之前用 CRA 或 Vite 建项目最直观的感受是“骨架是有了但路还长”。首先目录结构是纯前端的没有任何 server 端的影子数据请求全靠你封装 fetch然后手动写interface ResponseData后端加一个字段前端要跟着改两三处。其次认证模块永远是空白从 session 存储到登录回调每做一个项目都要重新实现一遍。第三数据库连接、ORM 迁移、环境变量管理这些“看不见的基础设施”默认模板里完全没有新手走到这一步经常卡住。t3code 的出现相当于把这些默认欠账一次性补齐了。你初始化完的项目里已经预置好了一个最小但完整的闭环数据库连接、认证回调、受保护的 API 路由、带真实数据的页面示例。你在此基础上改业务代码而不是从零开始搭地基。我自己的感觉是同等规模的全栈 demo用手动配置的方式至少要一个下午用 t3code 二十分钟就能跑通全链路。1.3 什么场景适合用它什么场景要谨慎结合我实际用的体会t3code 最适合这几种情况一是个人想快速验证产品想法MVP 阶段最需要的就是快速迭代二是团队内部统一新项目模板让每个新项目的目录结构、鉴权方式、命名规则保持一致减少“每个项目长得都不一样”的维护成本三是教学或学习想理解“前后端类型如何贯通”的人拆开 t3code 生成的代码读一遍比看十篇博客都有用。但也不是所有项目都适合。如果项目需要开放 API 给第三方使用那就得认真考虑 REST 的兼容性因为 tRPC 毕竟是 RPC 风格外部客户端接入门槛稍高。再比如你的团队因为特殊要求必须用 Java 或者 Go 做后端那这套方案也不合适。t3code 的选择是聚焦 TypeScript 全栈用它最舒服的场景而不是做成一个万能工具。2. 技术栈选型解析为什么偏偏是这几个凑在一起2.1 用 Next.js 当底座因为它把“前后端能放在同一个代码库”这件事做得最顺选 Next.js 而不是 Express、Fastify或者单纯的 Vite最关键的原因是它天生就支持 API Routes。你在app/api目录下写一个文件就是一个后端接口浏览器或者前端页面可以直接请求。这个特性让 tRPC 的接入变得非常自然——tRPC 的 HTTP 适配器只需要挂到 Next.js 的一个 API 路由上整个项目就有了一个完整的后端入口不需要像 Express 项目那样额外部署独立的 Node 服务。另一个实际好处是部署友好。Next.js 项目可以直接部署到 Vercel 这类 Serverless 平台数据库在本地或者云上都没关系反正通过连接字符串访问。Next.js 自己还有路由缓存、图片优化、SEO 相关的支持作为脚手架底座覆盖面够广。2.2 tRPC 就是“把后端函数变成你前端调用的函数”类型从这边到那边不落地tRPC 是我认为 t3code 这套组合里最亮眼的角色。用传统 REST 的方式写接口你得定义 URL、HTTP 方法、请求体和响应体的类型前后端各维护一份。tRPC 的思路是后端写一个函数前端直接像调用本地函数一样调用它过程里类型定义是共享的。我举个简单的例子。你在服务端定义了一个查询接口list: publicProcedure.query(({ ctx }) { return ctx.db.post.findMany({ select: { id: true, title: true, createdAt: true }, }); })前端就可以这样调用const { data: posts } api.post.list.useQuery();这里data的类型会自动推断成{ id: string; title: string; createdAt: Date }[]。如果服务端把title改成name前端编辑器里立刻标红。无需 codegen无需手动导入 OpenAPI 生成的类型定义。对于一支全 TypeScript 团队这种“本来就应该这样”的效率提升用过就回不去了。2.3 Prisma用 schema 定义数据库然后让类型从 schema 里长出来Prisma 在这些工具里承担的是“数据模型 访问”的角色。它的使用逻辑和 Mongoose、TypeORM 都不太一样你先写一个schema.prisma文件定义模型和关系然后运行prisma migrate dev生成迁移 SQL再运行prisma generate生成客户端类型。之后你在代码里调用prisma.post.findMany()返回的对象类型就是从 schema 自动推导出来的。这种“单一事实来源”的设计跟 T3 的理念天然契合。数据库字段、后端类型、前端类型都跟 schema 挂钩改一处全链路跟着变。对比传统方式——数据库字段是数据库的事后端类型是 TypeScript interface前端类型是另一个 interface——你就能理解为什么很多人一旦跨过这个门槛就再也不想回去了。2.4 Tailwind 的选择不是随大流是为了“少写样式文件多写业务逻辑”Tailwind 进入这套组合在很多人眼里是“UI 偏好”但站在脚手架设计者的角度它更重要的价值是减少了文件的碎片化。写 CSS Modules 或者 styled-components每个组件可能都要配一个样式文件用 Tailwind样式写在 class 里组件和视觉是在一个地方里看全的。对于快速迭代和移动端适配它的响应式前缀用起来确实方便像md:flex、sm:w-1/2都是直接在类名上解决问题。当然这纯粹是个人喜好问题t3code 把 Tailwind 作为默认选项但不强制初始化时你可以选择不装它。不过我的建议是第一次用就保留因为 t3code 自带的示例页面和组件样式都基于 Tailwind去掉之后示例代码的可读性会打折扣。2.5 NextAuth 解决认证闭环登录状态不只是前端的大爷也是后端中间件的硬条件全栈项目绕不开登录认证。NextAuth 的优势在于它跟 Next.js 的集成度非常高配置好了 session 回调之后你可以在服务端组件里直接读取当前用户信息也能在 tRPC 中间件里判断“这个人有没有登录”。t3code 预置了 NextAuth 的路由、数据库 session 存储的表结构以及默认的 Google/GitHub 等 provider 示例。你只需要在自己的开发者后台申请好密钥填进.env就能跑通整个登录流程。很多新手会问“那我用 JWT 存 cookie或者干脆前端存 localStorage行不行”行但不是这个架构的首选。t3code 走的是 session JWT 混合模式安全性和扩展性平衡得比较好以后要接微信扫码、LDAP 或者自定义手机号登录都是在 NextAuth 的 provider 层面扩展不会把业务代码搅乱。3. 从零起步用 t3code 初始化一个全栈项目的完整过程3.1 环境准备先把 Node.js 和包管理器理清楚要顺利跑 t3code你的机器上需要先装好 Node.js建议版本在 18.17 以上太老的版本在 Next.js 13/14 系列下会有兼容性问题。包管理器方面npm、pnpm、yarn 都可以但我个人推荐 pnpm。理由很简单t3code 生成的项目的依赖树比较深pnpm 的硬链接机制能省不少磁盘空间而且安装速度真的快。初始化命令如下npx create-t3-applatest my-t3-app如果网速条件一般可以加--registry指定 npm 镜像源或者直接用pnpm dlx create-t3-applatest my-t3-app。还要提一个细节项目名不要用中文不要带大写字母。t3code 生成的包名会直接复用这个目录名后面 npm 发布或者部署到某些平台时大写字母会引起不必要的麻烦。3.2 交互式选项逐个拆解该勾选的都选上不该勾选的也别手痒执行完初始化命令t3code 会让你选择要集成的模块具体交互大概是这样的选项建议说明TypeScript必选这套方案的核心失去了 TS 就失去了所有意义Tailwind CSS建议选示例页面依赖它后续想替换成其他方案可以后期改tRPC建议选你不选 tRPCt3code 的核心就少了一半但纯前端项目可以跳过Prisma建议选凡是需要持久化数据的项目都选NextAuth.js建议选哪怕你现在没有第三方登录需求先留着后续加配置也方便ESLint / Prettier建议选团队项目一定要选格式化统一比什么都重要初始化时还会问“import alias 用不用默认的~还是”如果你不打算后期魔改路径无脑就行。其实这就是 t3code 的“渐进式”设计——你没用到 Prisma那生成的项目里就不会有多余的schema.prisma你没用到 tRPC那就不会有一堆 server 目录的样板代码。这样每个项目都尽量干净不会给你塞一堆用不上的东西。3.3 初始化完之后的目录结构每个关键文件是干嘛的跑完初始化你会看到类似这样的结构my-t3-app/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── pages/ │ │ ├── api/ │ │ │ ├── auth/[...nextauth].ts │ │ │ └── trpc/[trpc].ts │ │ └── index.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ └── root.ts │ │ └── db.ts │ └── styles/ │ └── globals.css ├── .env └── package.json这里最值得留意的是server/api/root.ts和server/api/trpc.ts。前者是“路由聚合入口”你新写的业务 router 都要在这里合并后者是 tRPC 的初始化上下文文件所有中间件和数据库实例都在这里注册。很多新手会犯一个错新写了一个postRouter但忘了在root.ts里挂载结果前端怎么调用都是 404。这不是 t3code 的问题是没理解集合入口的概念。prisma/schema.prisma就是数据库模型的唯一事实来源模型改动后要记得重新 migrate。3.4 数据库连接与首次迁移从DATABASE_URL到表结构落地把项目跑起来之前数据库一定要先连通。t3code 默认生成的是 SQLite 配置.env里长这样DATABASE_URLfile:./db.sqliteSQLite 对本地开发最友好不依赖外部服务直接就是一个文件。我建议刚接触这套方案的人先用它等真正理解了 schema 和迁移的流程再切 PostgreSQL 或者 MySQL 也不迟。切生产环境的数据库时修改.env里的DATABASE_URL然后执行npx prisma migrate dev --name init这条命令会根据你的schema.prisma生成 SQL 迁移文件并自动执行到数据库里。执行之后你的数据库表结构就跟 schema 同步了。我踩过的一个坑是改了 schema 之后忘了跑 migrate前端页面还在跑一旦查询到新增字段就报 “column does not exist”。所以schema 改了之后migrate 和 generate 这两步应该形成肌肉记忆。4. 核心链路实操从数据模型到前端页面的完整闭环4.1 先定义数据模型用 Prisma Schema 作为这根链条的起点现在开始写业务代码。假设我们做一个简单的“公告板”应用用户可以登录后发布公告。第一步是定义数据模型。打开prisma/schema.prisma加上 Post 和 User 两个模型的关联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 DateTime default(now()) title String content String? published Boolean default(false) author User relation(fields: [authorId], references: [id]) authorId String createdAt DateTime default(now()) updatedAt DateTime updatedAt }这里User模型是 NextAuth 已经帮你建好的我们只需要补上posts Post[]关系。Post模型里的id用了default(cuid())避免自增主键在分布式场景下的碰撞问题。定义完保存然后运行npx prisma migrate dev --name add_post npx prisma generatemigrate会把表建到数据库里generate会重新生成 Prisma Client 的类型声明。如果你用的是prisma/clientV5 或以上版本generate后编辑器不一定马上刷新类型偶尔需要重启一下 TypeScript 服务。4.2 写第一个 tRPC 路由公开查询和受保护写入的分工创建src/server/api/routers/post.tsimport { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ list: publicProcedure.query(({ ctx }) { return ctx.db.post.findMany({ orderBy: { createdAt: desc }, include: { author: { select: { name: true } } }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1, 标题不能为空), content: z.string().optional(), })) .mutation(({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });这里面的门道有不少。公开查询list不需要登录就能看用的是publicProcedure。写入操作create需要用户登录用的是protectedProcedure。protectedProcedure的作用是检查 session没有登录的用户调用接口会直接收到UNAUTHORIZED错误。输入校验交给 zodtitle 的最小长度、content 的类型都在这里声明前端调用时也会自动获得对应的类型不需要手动写一个CratePostInputinterface。然后别忘了把postRouter挂载到聚合入口src/server/api/root.ts里import { createTRPCRouter } from ~/server/api/trpc; import { postRouter } from ~/server/api/routers/post; export const appRouter createTRPCRouter({ post: postRouter, }); export type AppRouter typeof appRouter;挂载这步完成后前端才能通过api.post.create访问到刚才写的 mutation。这个聚合入口之于 tRPC就好比 REST 项目里的路由注册表漏了它什么都白搭。4.3 前端页面里调用接口类型从服务端一路推过来页面部分用 Next.js 的 React 组件来实现。在src/pages/index.tsx里你可以这样写import { api } from ~/utils/api; export default function Home() { const { data: posts, isLoading } api.post.list.useQuery(); const createPost api.post.create.useMutation({ onSuccess: () { // 让列表重新拉取 void utils.post.list.invalidate(); }, }); if (isLoading) return div加载中.../div; return ( main classNamep-8 h1 classNametext-2xl font-bold公告板/h1 ul {posts?.map((post) ( li key{post.id} strong{post.title}/strong by {post.author.name} /li ))} /ul /main ); }你没有在代码里定义Post的 interface也没有写fetch的响应类型但posts数组里的每一项都已经知道有id、title、content、author.name这些字段。这是 tRPC 把服务端类型“投射”到客户端 hook 上的结果。使用时有一个细节api.post.list.useQuery()这个 useQuery 不是 React Query 原生 hook而是 tRPC 包了一层它已经帮你把 queryKey 和类型绑好了。所以不要试图直接调useQuery({ queryKey: [post.list] })那样你就是在绕开 tRPC 的类型安全路径。4.4 用中间件做权限控制而不仅仅在函数体里写 if 判断上面的protectedProcedure是 t3code 自带的。如果你想实现更细的权限比如“只有管理员能删除公告”可以自定义一个中间件。t3code 里src/server/api/trpc.ts长这样import { initTRPC, TRPCError } from trpc/server; import { type CreateNextContextOptions } from trpc/server/adapters/next; export const createTRPCContext (opts: CreateNextContextOptions) { const { session } getServerAuthSession(opts); return { db, session, }; }; const t initTRPC.contexttypeof createTRPCContext().create(); const enforceUserIsAdmin t.procedure.use(({ ctx, next }) { if (ctx.session?.user.role ! ADMIN) { throw new TRPCError({ code: FORBIDDEN }); } return next({ ctx }); }); export const adminProcedure t.procedure.use(enforceUserIsAdmin);这个中间件会在函数体之外拦截请求权限不通过直接抛异常。相比在函数体里重复写if (!isAdmin) throw ...这种方案在 route 多的时候更节省代码量。这里session.user.role字段需要你在 schema 里加上并处理好数据库映射。这个例子说明 t3code 的权限设计不是死板的你的业务需要什么级别的控制顺着中间件的思路扩展就行。5. 实际开发中踩过的坑问题排查与避坑手册5.1 Prisma 报错P1001、引擎找不到、修改模型后类型不更新Prisma 相关的报错是新手阶段最密集的问题。P1001 是数据库连不上绝大多数情况是你的DATABASE_URL写错了或者数据库服务没启动。本地用 SQLite 的记得确认文件路径是file:./prisma/dev.db这种相对路径别写到绝对路径去。另外如果你刚改完schema.prisma在代码里却一直看不到新字段十有八九是忘了重新跑prisma generate。还有一个相对冷门的坑在某些 Linux 服务器环境下Prisma Engine 二进制下载失败或者权限不够会报 “Could not find the query engine”。这种情况要先清理node_modules/.prisma然后重新执行prisma generate或者设置环境变量跳过引擎下载PRISMA_ENGINES_MIRROR但这是网络环境相关的临时方案生产环境更建议直接用 Docker 镜像预热。我见过不少人在这一步反复折腾最后发现是公司内部网络对 GitHub 限制严格换源后三秒钟解决。5.2 tRPC 类型不对齐服务端改了前端还是旧类型t3code 最依赖的就是类型但类型出问题也是最让人抓狂的。场景是服务端 router 里新增了一个 query前端api.post.newQuery却一直报类型不存在。这个问题的常见原因是 TypeScript 服务没有重新加载。重新编译或者按编辑器的 “Restart TypeScript Server” 基本都能解决。如果重启了还不行检查你是不是在两个项目里分别建了 tRPC client。t3code 的标准做法是前端的utils/api里的createTRPCNext或createTRPCReact只有一个实例确保router的类型引用了AppRouter类型。如果你自己在某个组件里又手写了一个createTRPCReact哪怕代码一样类型系统会认为它是另一个“实例”两端对不上是必然的。还有一个场景是 tRPC 的query返回Date类型JSON 序列化后变成字符串前端拿到的类型里却是Date。t3code 默认加了superjson序列化器这个需求已经被处理掉了。如果你自定义序列化器时把它替换掉了那么记得 Date 类型不会自动穿越网络边界。5.3 NextAuth 登录跳转异常CSRF 错误、403、回调地址不对试过 t3code 的人基本都会在登录步骤卡一会儿。最常见的就是本地启动后登录跳转到 provider 后回调回来 403。80% 的情况是.env里的NEXTAUTH_URL没有配置成http://localhost:3000或者NEXTAUTH_SECRET不是合法值。这两个变量是 NextAuth 会话签名和回调校验的基础缺哪个都会出问题。生成随机 secret 可以用openssl rand -base64 32本地开发还可以遇到 CSRF 不匹配的问题原因是浏览器同时开着多个代理端口或者NEXTAUTH_URL和实际访问地址不一致。简单粗暴的解法固定使用http://localhost:3000不要一开始就用局域网 IP 访问。5.4 Tailwind 样式不生效类名明明写了页面就是丑陋的黑白这个问题有三个高频原因。第一tailwind.config的content字段没有把你新创建的目录包含进去。t3code 默认配置是./src/**/*.{js,ts,jsx,tsx}如果你在项目根目录之外又建了shared或packages目录里面的类名不会生成。第二你可能在全局 CSS 里覆盖了 Tailwind 的 base 层但顺序放错了。先引入 Tailwind 的三个指令再写自定义样式这是百试不爽的顺序tailwind base; tailwind components; tailwind utilities;第三用了动态字符串拼接类名比如className{font-${weight}}Tailwind 在编译时无法扫描到这种动态值样式自然不出现。要么用完整的类名字符串要么用 CSS 变量方案处理。5.5 生产构建报错内存溢出、NextAuth 配置缺失、数据库外网访问受限next build阶段如果出现JavaScript heap out of memory通常是因为项目里某个页面做了大量同步阻塞计算或者依赖了过大的静态数据。临时缓解可以加 Node 内存参数NODE_OPTIONS--max_old_space_size4096 next build但根本上还是需要优化代码比如把重量级逻辑放到 Server Actions 或 API route 里避免在构建时执行。生产环境部署时还会遇到一个典型问题DATABASE_URL指向的数据库只允许内网访问Serverless 平台连不上。这种情况要么配置数据库的白名单和安全组要么通过平台侧的内网代理访问数据库。具体要看你部署在什么平台但总原则是先把部署环境的出网 IP 加到数据库访问白名单里。5.6 我不止一次踩过的坑忘记挂载 router、包管理器混用这是我用 t3code 的这几个月里最常犯的一类错误。新建了postRouter却忘记在root.ts里挂载An API page forpost.list直接 404。这个问题的排查相对快因为后端日志会直接告诉你 “No router matched” 或者前端控制台会报TRPCError: Not found但新手可能会盯着路由看半天才意识到是集合入口漏了。我的建议是新建 router 的 checklist 里把“挂载 root.ts”作为最后一项强制勾选。另一个是包管理器混用的坑。有人用 npm 初始化了项目之后又用 pnpm 安装依赖结果node_modules结构变了Next.js 的缓存和 Prisma 的引擎二进制各自为政跑起来各种过时。统一包管理器是 t3code 项目里的隐性规则之一不是性能洁癖是真的能避免很多玄学问题。6. 从另一个维度看 t3code把它当成学习材料和团队基建很多人以为 t3code 的价值只是“省去配置时间”但其实它更大的价值是当教材用。T3 Stack 里每一个决策点都有它的道理而 t3code 把设计者的这些思考直接固化成了项目结构。你读它的源码能看到一个标准的 tRPC 路由是怎么组织入参校验、权限控制和数据库访问的一个 NextAuth 的 session 是怎么在前后端之间传递的一个 Prisma Schema 如何在 migrations 和 client 生成之间形成闭环。对于还没有建立全栈工程概念的开发者来说一边读一边改比任何教程都直接。团队层面t3code 还有一个好处它生成了一套规范。不同成员写的新路由都会自然放进routers/目录公共类型放在server/api层级数据库逻辑走 Prisma 而不是到处写 SQL。这种结构上的“约束感”反而是长期项目维护最需要的。我见过太多项目初期自由度很高三个月后就变成谁都动不了的毛线团。如果让我给一个使用建议我会说第一次用 t3code 时开启所有默认选项先把它当作“官方食谱”完整跑一遍理解每个模块的作用。第二、第三次再按需裁剪。不要从一开始就为了“精简”而删掉 tRPC 或 Prisma因为那会让你错过这套方案最核心的类型安全体验。等到你对整个链路熟悉了自然知道哪些模块在你的业务里是冗余的。我在实际使用中还发现一个很适合扩展的方向把 t3code 生成的 tRPC router 和 Prisma schema 当作一个“领域服务层”在其上再挂一层业务逻辑模块就很容易演进成更大型应用的雏形。换句话说t3code 的定位不是终点而是一个起点。但它这个起点站得比大多数脚手架都高因为它帮你把最麻烦的“前后端类型同步”问题直接解决了让你能更专注在实际业务上。希望这篇文章能给准备上车 T3 Stack 的人一个比较完整的图景。如果你想找一个既能快速出活、又能让你学到全栈工程最佳实践的框架t3code 绝对值得花一个下午试试。