☰
t3code 全栈模板实战:T3 技术栈与 AI 编码代理的避坑指南
2026/10/26 8:44:25 网站建设 项目流程

1. t3code 到底是什么:不是又一个脚手架,而是一套约定

开发库里如果出现一个叫 t3code 的项目,第一反应要么是 T3 技术栈,要么是和 AI 编码相关的东西。我这个 t3code 恰好把两个答案都占了:一套跑在 Next.js、tRPC、Prisma、Tailwind 之上的 TypeScript 全栈模板;但真正落地的时候,我又刻意做了一步很多人不会做的工作——在仓库里给 AI 编码代理留了一条能看懂、也不会瞎改的入口。这篇文章把 t3code 从初始化到部署、再到接入 AI 编码工作流的全过程翻出来讲,包括那些只在生产环境里才会冒出来的坑。

如果你也想搭一套能长期维护、能交给同事接手、能塞进 AI 编码工作流的前端加后端一体化项目,这篇文章能省掉你几个晚上查文档的时间。不会讲太高深的设计模式,重点就三个:可复现、能跑通、上线不出幺蛾子。

1.1 T3 到底指哪三个 T

T3 不是某个发行版的全称,它更像一个约定,把四样东西拼在一起:Next.js 负责应用壳和路由,tRPC 负责类型安全的接口通信,Prisma 负责数据库访问,Tailwind 负责样式。官方 T3 技术栈的“T”多数时候指向 TypeScript、Tailwind、tRPC,而 Next.js 是最底层的运行容器。这个组合最大的价值不是“新”,而是把类型安全从数据库一路推到了前端组件。

打个比方:它像一张交通图,Next.js 是主干道,tRPC 是每条斑马线都画得清清楚楚的过街天桥,Prisma 是地下车库的闸机,Tailwind 是路牌。你不需要记住每条路长什么样,只需要知道从哪条路上去、到哪个出口下来,全流程都是同一张图。t3code 这个项目名,本质上就是围绕这张图定的一套目录、规范和上下文。

1.2 t3code 要解决的三个具体问题

第一是约定大于配置。项目里每个人都有自己的建目录习惯,有人把接口全放pages/api,有人把所有查询写在组件里,三个月之后就没人敢动了。t3code 先把目录、文件命名、数据访问方式定死,谁进来都能顺着结构找代码。

第二是整条链路的类型安全。前端调用 tRPC 的时候,参数和返回值是从后端类型推导出来的,改数据库字段时,类型错误会在编译期直接炸出来,而不是上线之后被用户骂出来的。

第三是给 AI 编码代理一个可控的边界。现在让 AI 写代码很容易,但 AI 改代码往往不守规矩。我在 t3code 里加了一份AGENTS.md,告诉它哪些目录能碰、哪些操作必须跑 typecheck,效果比只靠一句“你小心一点”好太多。

2. 初始化选型:create-t3-app 的选项勾选和几个版本新坑

用官方脚手架初始化是最省事的路。自己手搭 Next.js 也不是不行,但 create-t3-app 已经把 App Router、Tailwind、tRPC、Prisma 之间的联动调好了,尤其是 tRPC 客户端的 provider、server 端的 context 初始化这些样板代码,手写一遍很容易漏。

初始化命令很简单:

npx create-t3-app@latest t3code cd t3code pnpm install pnpm dev

2.1 第一次建项目,勾选框怎么选

create-t3-app 启动后会问一堆“要不要装”的问题,我按常规 SaaS 项目的标准给你一张表:

选项建议理由
Tailwind CSS勾选样式方案,默认集成度高,不勾后续想加也费劲
Prisma勾选数据访问层,后期加字段比手写 SQL 更可控
tRPC勾选全栈类型安全的灵魂,T3 的核心就是这个
Authentication按需勾选有登录就勾 NextAuth/Auth.js,纯内容站可以先不勾
Drizzle ORM暂不勾选如果和 Prisma 同时勾容易分散注意力,先一条路走通
ESLint勾选代码规范底线,AI 生成的代码也需要被它约束
Prettier勾选格式化统一,避免团队里每个人 format 配置都不一样

如果你是做企业后台或者重交互应用,Prisma、tRPC、Auth 基本是必选;如果只是做一个简单静态展示站,那连 tRPC 都可以先不勾,前端直接请求接口就够了。t3code 的定位是“能生长的项目模板”,所以我默认按全栈标准来。

2.2 Tailwind 4 会让一堆老教程集体失灵

这是初始化阶段最容易翻车的地方。create-t3-app 新版本默认带的是 Tailwind 4,它和 Tailwind 3 的配置方式差别很大。

Taillwind 3 时代,入口 CSS 长这样:

@tailwind base; @tailwind components; @tailwind utilities;

Tailwind 4 只需要一行:

@import "tailwindcss";

如果你照着老教程写了一堆@tailwind指令,页面会出现样式全部失效或者大部分失效的情况。更隐蔽的是自定义主题的配置方式也变了。Tailwind 3 要在tailwind.config.ts里写colors、spacing,Tailwind 4 默认推荐直接在 CSS 里用@theme定义。我见过有人升级后tailwind.config.ts还在,但改了半天颜色不生效,最后才发现新版本根本不读那份配置了。

建议初始化之后第一件事就是打开src/styles/globals.css,确认里面是@import "tailwindcss";。如果你的项目必须沿用旧版tailwind.config.ts,可以保留文件并在 CSS 里加一行@config "../../tailwind.config.ts";让它重新生效,但这属于过渡方案,不要长期依赖。

2.3 初始化命令和第一遍跑通

初始化完成后,先别急着加业务代码,按这个顺序验证一遍:

pnpm dev

浏览器打开localhost:3000,能看到默认首页就算基础通了。然后再跑一次pnpm build,确保 SSR 构建没报错。这两个命令过了,再开始改页面和加模型。

3. 把 tRPC 用稳:路由拆分、Zod 边界和 RSC 的分工

tRPC 是 t3code 里最值得花时间理解的部分。很多人把 tRPC 当成一个“不用写 REST 路由的 RPC 框架”,其实它的核心价值不是省路由,而是让前端调用后端接口时,参数和返回值的类型都能被编译器自动推导。这个能力用好之后,重构成本会低很多。

3.1 路由拆分:一张路由表别装下全部业务

create-t3-app 默认会把路由集中在src/server/api/root.ts,然后在routers目录里按模块拆。我的习惯是每个业务域一个文件,比如帖子、标签、评论,而不是把所有query和mutation全堆在一个文件里。

src/server/api/ root.ts routers/ post.ts tag.ts comment.ts trpc.ts

这样做的好处是,AI 在帮你加接口时,能根据文件名定位到具体模块,而不是在几百行的路由文件里乱找。root.ts 保持干净,只负责把各模块的路由合并:

import { createTRPCRouter } from "@/server/api/trpc"; import { postRouter } from "@/server/api/routers/post"; import { tagRouter } from "@/server/api/routers/tag"; export const appRouter = createTRPCRouter({ post: postRouter, tag: tagRouter, });

3.2 用 Zod 把输入边界锁死

tRPC 通常配 Zod 做输入校验。Zod 的意义不只是防脏数据,它还能让前端useQuery和useMutation的时候,自动推导出参数类型。如果后端的输入结构改了,前端编译直接报错,这比“后端返回了一个诡异错误”要友好得多。

下面是一个带游标分页和登录保护的帖子路由例子:

import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "@/server/api/trpc"; export const postRouter = createTRPCRouter({ list: publicProcedure .input( z.object({ cursor: z.string().optional(), limit: z.number().min(1).max(50).default(10), }), ) .query(async ({ ctx, input }) => { const posts = await ctx.db.post.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: "desc" }, }); let nextCursor: string | undefined; if (posts.length > input.limit) { const nextItem = posts.pop(); nextCursor = nextItem?.id; } return { items: posts, nextCursor, }; }), create: protectedProcedure .input( z.object({ title: z.string().min(1).max(120), body: z.string().max(10000).optional(), }), ) .mutation(async ({ ctx, input }) => { return ctx.db.post.create({ data: { title: input.title, body: input.body, authorId: ctx.user.id, }, }); }), });

注意分页这里用了take: input.limit + 1来判断是否还有下一页,这种写法比count全量统计在数据量大时性能好很多。保护登录用的是protectedProcedure,下一节会展开。

3.3 在 RSC 场景里别硬套 tRPC

Next.js 的 React Server Components 出来之后,很多人开始犹豫:既然服务端组件能直接查数据库,那还要 tRPC 干什么?

我的经验是:查询如果发生在服务端组件内部,并且不需要跨前后端共享类型,直接用 Prisma 查询没问题;但只要是客户端需要交互的组件,比如按钮触发、表单提交、分页加载,都应该走 tRPC。t3code 里的原则是:查询尽量放在服务端组件里,变更统一走 tRPC mutation,这样既享受了 RSC 的渲染性能,又保住了接口的类型安全和可审计性。

但要注意,服务端组件一旦直接查数据,它就会变成异步组件,不能直接给子组件传函数当属性。如果你发现自己把一堆回调函数传来传去,就是该拆成客户端组件的信号了。

4. Prisma 与鉴权的细节:少踩坑就是省时间

数据库层最容易踩坑的地方不在写 SQL,而在模型变更和敏感字段的读取。t3code 里我坚持两条规矩:所有数据库字段变更都走 migration,所有敏感字段都不出现在默认查询结果里。这两条规矩看起来简单,执行到位能省掉一大半线上事故。

4.1 敏感字段拿出来,比 keep vacio 更好

老项目里经常能看到手写select: { name: true, email: true, image: true }的白名单写法。问题是每次模型加了字段,你都要记得去每个查询里补。漏一次,密码哈希或者手机号就可能跟着查出来了。

Prisma 的新版本提供了omit,可以从查询结果中排除字段,比维护 select 白名单省心得多:

const user = await ctx.db.user.findUnique({ where: { id }, omit: { hashedPassword: true, }, });

如果你的 Prisma 版本还不支持这个 API,那就把敏感字段的读取收敛到一个公共函数里,不要到处散落。比如封一个getPublicUserById,所有对外接口都调它,至少能保证改一处就全局生效。还有一点,hashedPassword这种字段建议在schema.prisma里明确命名,不要叫password,因为password太容易在写业务代码时被顺手select出来。

4.2 模型变更的正确姿势

开发阶段我统一用migrate dev:

npx prisma migrate dev --name add_post_publish_state

这条命令会自动生成 migration SQL、执行到本地数据库、再重新生成 Prisma Client。生产环境绝对不要用db push,要用部署迁移:

npx prisma migrate deploy

migrate deploy的执行是有序的,每条 migration 只跑一次,符合上线阶段对可控性的要求。db push更像原型阶段的快速同步工具,它不维护历史版本,一旦多人协作很容易出现“本地和我库里字段不一样”的混乱。我自己早期用db push进生产环境,结果就是模型和 migration 历史对不上,最后只能手动修库,那种痛苦一次就够。

4.3 protectedProcedure 拦住未登录

t3code 的鉴权中间件用protectedProcedure实现,它的逻辑很直白:从 ctx 里拿 session,没登录直接抛 UNAUTHORIZED,登录了就把user塞进新的 ctx 里,后面的 handler 直接用。

export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.session || !ctx.session.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx: { session: ctx.session, user: ctx.session.user, }, }); });

这里最关键的是中间件返回时把ctx重新传下去,后续所有query和mutation里都能拿到ctx.user.id,类型还是推导出来的,不会出现user可能是 null 的手动判断。配合 NextAuth.js,你只需要在创建 tRPC context 时把getServerAuthSession()的结果放进 context,登录状态就干净地串起来了。

5. 上线回放:t3code 最容易翻车的 5 个现场和完整排查过程

部署阶段的坑,大多数不是技术难,而是环境差异。t3code 从本地跑到生产环境,我踩过一批很典型的坑,直接给你一张排查表,再挑两个详细复盘。

5.1 问题清单

现象根因处理方式
样式全部失效Tailwind 4 配置方式变了用@import "tailwindcss";替换旧/前@tailwind` 指令
日期类型变成字符串tRPC 的 transformer 没在客户端配置客户端链接也传transformer: superjson
Prisma 报 P1001serverless 环境连不上数据库配置连接池地址,迁移用directUrl
登录态在线上反复失效AUTH_SECRET/环境变量没配在托管平台配置环境变量,本地和线上保持一致
构建时 Prisma Client 缺失镜像里没执行prisma generatebuild 前先跑npx prisma generate

5.2 完整排查:tRPC 的 Date 到了客户端变成字符串

某个周六晚上,线上反馈“创建时间显示 Invalid Date”。本地开发从来没出过这个问题,第一批排查路径是这样的:

先打开浏览器 Network,看 tRPC 接口返回的 JSON,发现createdAt的值像是普通字符串,但仔细看又能看到 superjson 元数据的痕迹。接着对比本地和线上的同一次请求,本地正常,线上异常。这个现象很典型:服务端配置了transformer: superjson,但客户端发起请求时的httpBatchLink没有传同一个 transformer,类型推导没问题,但因为序列化方式不一致,Date 在客户端没有被还原成真正的Date对象。

修复就是在客户端初始化 tRPC 的地方,同样传transformer: superjson:

export const api = createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: "/api/trpc", transformer: superjson, }), ], });

这个坑的隐蔽点在于,TypeScript 类型层面完全正常,只有运行时才会暴露。而且它只在某些版本差异或配置不对称时出现,所以特别容易漏。

5.3 Prisma 在线上连不上数据库的定位过程

另一次线上报错是 Prisma P1001,连接池连不上数据库。排查时我先确认本地pnpm dev正常,说明代码没问题,问题出在部署环境到数据库的网络链路。

具体定位分了三步:

第一步,看部署平台的日志,确认DATABASE_URL是否注入成功。很多托管平台的环境变量不会自动带下来,名字少一个字符都可能导致连接串无效。

第二步,看数据库提供商给的连接串。有些云数据库的地址分“连接池地址”和“直连地址”两种。如果你用 serverless 服务,运行时必须用连接池地址,否则大量短期连接会把数据库资源打满;而执行 migration 时,连接池反而不适合,它会干扰迁移事务。Prisma 在schema.prisma里可以用directUrl单独指定迁移用的直连地址:

datasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_URL") }

第三步,检查构建镜像。如果你用 Docker 部署,prisma generate必须在 build 阶段执行,否则容器里只有源码没有生成的 Client。很多 CI 构建失败都死在这一步,典型表现是本地跑pnpm build正常,镜像一构建就是找不到@prisma/client。

6. AI 编码代理进场:t3code 的护栏怎么设置

现在让 AI 写代码已经是常态了。问题是 AI 生成的代码经常“看着能用”,但风格和项目规范对不上。t3code 后期最重要的改造,就是给 AI 编码代理建立护栏。

6.1 给 Agent 一份 AGENTS.md 而不是一句提示词

AI 编码代理进仓库之后,第一件事不是让它写功能,而是让它读项目规则。我在 t3code 根目录放了一份AGENTS.md,内容不复杂,但把几条红线写得明明白白:

# t3code 开发指南 - TypeScript strict 模式已经开启,不要用 any 绕过类型错误。 - 所有服务端数据访问统一走 tRPC,除非调用方是服务端组件且不需要交互。 - 修改 prisma/schema.prisma 后必须创建一个新的 migration,不允许直接 db push。 - 提交前必须运行 pnpm typecheck 和 pnpm lint。 - 新增路由时,把对应 handler 放到 src/server/api/routers 下的业务模块中。

这份文件的重点不是告诉 AI“你是个厉害的工程师”,而是把项目里的硬约定写清楚。实测下来,加了AGENTS.md之后,AI 生成的代码基本能直接进pnpm typecheck,不会再随手丢一些// @ts-ignore或者凭空发明目录结构。

6.2 类型检查和测试当成护栏

规则文件只能约束习惯,真正拦住问题的还是自动化检查。我在 package.json 里把检查命令固化好,提交前必跑:

{ "scripts": { "typecheck": "tsc --noEmit", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "test": "vitest run" } }

CI 里再加一个最简单的 workflow,每次 push 都跑一遍这三条。AI 生成代码之后,它自己也会看到红叉,会主动修正。没有 CI 的情况下,AI 经常“自信地把错误提交上去”,有了红色失败信号之后,它反而更老实。

6.3 我的实际使用体会

踩过一圈坑之后,我给 AI 分配任务的姿势已经固定了:让它先在AGENTS.md里找到对应的接入方式,再让它读一个同类型路由作为模板,最后明确告诉它“改完要跑 typecheck”。这三步看起来多,实际上 AI 几秒钟就完成了,但生成的代码质量明显上了一个台阶。

t3code 能做到这一步,本质上不是因为工具多先进,而是因为项目里每一步都留下了可读、可约束、可自动校验的结构。数据库模型变了,有 migration 历史;接口变复杂了,有 tRPC 的类型推导;AI 进场了,有AGENTS.md和 CI 兜底。如果你也想搭一个能长期维护的全栈项目,我建议你先从目录拆分和这些自动化规则开始,不要指望一步到位。项目都是在真实的需求推动下,慢慢长成最顺手的样子。

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

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

立即咨询