
1. 从“t3code”这个关键词说起它到底指什么第一次看到“t3code”这个词很多人会一头雾水。它不像“Python教程”或者“React入门”那样一眼就能看出领域归属反而带着一种内部代号的味道。我在几个技术社区和代码托管平台翻了一圈发现这个词的指向其实相当集中它通常被用来指代一类基于 T3 技术栈的代码实践、项目模板或脚手架工具。这里的“T3”不是某个单一框架而是指Next.js TypeScript tRPC Tailwind CSS Prisma这一套组合拳最早由 Theo Browne 等人推广开来后来社区里就把遵循这套架构的项目统称为“T3 栈项目”而“t3code”则常被用作这类项目的仓库名、CLI 命令名或者代码片段的标签。换句话说当你看到“t3code”时大概率面对的是一个全栈 TypeScript 项目它强调端到端的类型安全、极简的 API 层和快速的开发体验。它解决的问题很具体传统前后端分离项目里接口类型靠手写或代码生成容易漂移REST 或 GraphQL 的样板代码多状态管理和数据获取逻辑分散。T3 栈通过 tRPC 把后端函数直接暴露给前端调用类型自动推导省掉了中间层。而“t3code”作为关键词往往出现在开发者分享项目结构、初始化命令或者踩坑记录的语境里。这篇文章适合谁看如果你已经写过 React 或 Next.js但对 tRPC、Prisma 的配合还不太熟或者你接手了一个名为“t3code”的仓库却不知道从哪下手那接下来的内容就是为你准备的。我会从项目初始化、目录结构、核心依赖的协作逻辑、常见报错排查到实际部署前的检查一步步拆开讲。所有内容基于社区常见实践和我自己在多个 T3 栈项目里的操作经验不保证是唯一解但能让你少走弯路。提示本文提到的“t3code”泛指 T3 栈项目的代码实践不特指某一个具体仓库。如果你的项目名恰好叫 t3code可以把文中的路径和命令按实际情况替换。2. 初始化一个 t3code 项目命令背后的选择逻辑2.1 为什么推荐用 create-t3-app 而不是手动搭手动从零搭建一个 T3 栈项目不是不行但你会花大量时间在配置文件的互相引用上。create-t3-app这个脚手架的价值在于它把 Next.js、tRPC、Prisma、Tailwind、NextAuth 等模块的初始化顺序和类型声明文件都提前对齐了。我试过手动配 tRPC 的 router 和 client光是处理AppRouter的类型导出就折腾了半小时而脚手架生成的代码里server/api/root.ts和utils/api.ts之间的类型链路是直接打通的。运行命令很简单npm create t3-applatest t3code # 或者 pnpm create t3-applatest t3code执行后 CLI 会问你几个问题要不要用 TypeScript默认 yes、要不要加 tRPC、要不要加 Prisma、要不要加 NextAuth、要不要加 Tailwind。我的建议是全选 yes除非你明确知道某个模块用不上。比如你只是做个纯静态页面那 Tailwind 可以留tRPC 和 Prisma 可以去掉但如果你打算做带数据库的全栈应用全选能省掉后续手动集成的麻烦。这里有个细节CLI 会问你是否使用src目录。我习惯选“是”因为把源码收在src下根目录只放配置文件和静态资源视觉上更干净。但这不是硬性要求团队里如果有人习惯根目录直接放app或pages统一就行。2.2 生成后的目录里哪些文件是“活”的初始化完成后你会看到一个典型的 T3 项目结构。我拿一个刚生成的 t3code 项目举例关键路径如下t3code/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ └── trpc/ │ │ │ └── [trpc]/ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts │ │ │ └── routers/ │ │ │ └── post.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── react.tsx │ │ └── server.ts │ └── utils/ │ └── api.ts ├── .env ├── next.config.js ├── package.json └── tsconfig.json其中src/server/api/root.ts是所有路由的汇总入口你每加一个 router都要在这里注册。src/trpc/react.tsx导出了前端用的api对象它带着完整的类型信息。src/utils/api.ts是给服务端组件或 API 路由用的辅助函数。这三个文件构成了 tRPC 的类型闭环改任何一个都要留意另外两个的引用。.env文件里默认有DATABASE_URL和NEXTAUTH_SECRET等占位符。如果你选了 Prisma第一件事就是把DATABASE_URL改成你本地或远程数据库的连接串。我见过有人直接跑pnpm dev然后报错说找不到数据库其实就是忘了改这个。2.3 依赖安装与首次启动的坑脚手架生成后先别急着pnpm dev。按顺序做这几步pnpm install或 npm/yarn看你用哪个修改.env里的DATABASE_URL运行pnpm db:push把 Prisma schema 推到数据库再pnpm devdb:push这个脚本是脚手架预置的对应prisma db push。它适合开发阶段快速同步表结构不需要生成迁移文件。但如果你要上生产应该用prisma migrate dev来生成版本化的迁移记录。我踩过的坑是在开发环境用db:push改了字段然后直接部署到生产结果生产数据库没有对应的迁移历史后续想回滚都难。所以从项目一开始就养成用 migrate 的习惯哪怕麻烦一点。另一个常见问题是端口占用。Next.js 默认跑 3000如果被占会提示换端口。但 tRPC 的客户端配置里如果写死了http://localhost:3000/api/trpc换端口后就会请求失败。检查src/trpc/react.tsx里的url设置通常它用的是相对路径/api/trpc那就没问题如果是绝对路径记得改成环境变量。3. tRPC 在 t3code 里的实际工作方式类型安全不是玄学3.1 一个完整的请求链路拆解很多人第一次看 T3 项目会被api.post.getAll.useQuery()这种写法迷惑前端怎么知道后端有这个函数答案藏在 TypeScript 的类型推导里。整个链路是这样的后端在src/server/api/routers/post.ts里定义了一个createTRPCRouter里面有个getAll过程。这个 router 被注册到src/server/api/root.ts的appRouter里。appRouter的类型AppRouter被导出。前端在src/trpc/react.tsx里用createTRPCReactAppRouter()创建了api对象。于是api.post.getAll在编辑器里就有了完整的类型提示包括输入参数和返回值。这个过程不需要你手动写接口类型也不需要代码生成步骤。TypeScript 编译器在后台完成了所有工作。我实测下来只要root.ts里注册正确前端调用时参数传错类型会立刻标红比等运行时才报错强太多。3.2 定义 router 时的输入校验Zod 不是可选项T3 栈默认用 Zod 做输入校验。在post.ts里你会看到类似这样的代码import { z } from zod; import { createTRPCRouter, publicProcedure } from ../trpc; export const postRouter createTRPCRouter({ getById: publicProcedure .input(z.object({ id: z.string() })) .query(({ ctx, input }) { return ctx.db.post.findUnique({ where: { id: input.id } }); }), });这里的.input()不是装饰品。如果你省略它input的类型就是unknown你在函数体里访问input.id会报错。而且 Zod 会在运行时校验传入的参数如果前端传了个数字而 schema 要求字符串tRPC 会直接返回 400 错误不会进到你的查询逻辑。我建议每个 procedure 都加 input 校验哪怕是空对象也写z.object({})这样类型和运行时都安全。3.3 服务端调用与客户端调用的区别在 t3code 项目里你有两种方式调用后端逻辑客户端组件用api.post.getAll.useQuery()走 HTTP 请求到/api/trpc。服务端组件或 API 路由用src/trpc/server.ts导出的api直接调用函数不经过 HTTP。服务端调用适合在 RSCReact Server Components里预取数据减少客户端瀑布流。但要注意服务端调用时没有 React Query 的缓存和重试机制每次都是直接执行。我通常会在页面级组件里用服务端调用拿初始数据然后在客户端组件里用useQuery做交互更新。两者共享同一个 router 定义类型完全一致。注意服务端调用api.post.getAll()返回的是 Promise你需要await它。而客户端useQuery返回的是包含data、isLoading等字段的对象别搞混了。4. Prisma 与数据库schema 设计决定后期维护成本4.1 schema.prisma 里的关键配置Prisma 的 schema 文件是 t3code 项目里最需要认真对待的部分。一个典型的schema.prisma开头是这样的generator client { provider prisma-client-js } datasource db { provider postgresql url env(DATABASE_URL) }provider可以是postgresql、mysql、sqlite等。开发阶段用 SQLite 很方便一个文件搞定但生产环境通常用 PostgreSQL。我建议开发和生产用同一种数据库类型避免 SQLite 和 PostgreSQL 在字段类型上的差异导致迁移失败。比如 SQLite 没有原生enumPrisma 会用字符串模拟换到 PostgreSQL 后行为可能不同。4.2 模型定义与关系字段的实操细节假设你要做一个博客的 t3code 项目Post和User的模型可能长这样model Post { id String id default(cuid()) title String content String? published Boolean default(false) authorId String author User relation(fields: [authorId], references: [id]) createdAt DateTime default(now()) updatedAt DateTime updatedAt } model User { id String id default(cuid()) email String unique name String? posts Post[] }这里有几个经验点default(cuid())比自增整数更适合分布式场景也避免暴露业务量。updatedAt让 Prisma 自动维护更新时间不用手写。关系字段author和posts是虚拟的不会在数据库里生成列但 Prisma Client 会据此生成关联查询方法。改完 schema 后运行pnpm db:push或pnpm db:migrate。如果数据库里已有数据加一个非空字段会失败需要给默认值或者先清空数据。我遇到过在已有Post表里加published Boolean没给默认值迁移直接报错。后来改成published Boolean default(false)才通过。4.3 在 tRPC 里使用 Prisma Client 的正确姿势src/server/db.ts里通常这样初始化 Prisma Clientimport { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma: PrismaClient | undefined; }; export const db globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV development ? [query, error, warn] : [error], }); if (process.env.NODE_ENV ! production) globalForPrisma.prisma db;这段代码的目的是避免开发环境下热重载时反复创建 PrismaClient 实例导致数据库连接数暴涨。globalThis缓存是官方推荐的做法。日志配置里开发环境打印 query 能帮你看到实际执行的 SQL排查性能问题很有用生产环境只留 error减少日志量。在 tRPC 的 context 里ctx.db就是这个db实例。你可以在src/server/api/trpc.ts里看到 context 的创建逻辑。如果你需要加用户会话信息也是在这里注入。5. 开发过程中最容易卡住的几个报错与排查路径5.1 “Type AppRouter does not satisfy the constraint” 怎么解这个报错通常出现在你新增了一个 router 但忘记在root.ts里注册或者注册时写错了键名。比如你建了comment.ts导出了commentRouter但在root.ts里写成了comment: commentRouter而实际导入名是commentRouter的别名。TypeScript 会提示AppRouter不满足约束因为它找不到对应的类型。排查步骤打开src/server/api/root.ts确认appRouter里每个键值对都正确。检查新 router 的导出名和导入名是否一致。如果用了路径别名如~/server/api/routers/comment确认tsconfig.json里的paths配置正确。我自己的习惯是每加一个 router先在root.ts里注册再写具体逻辑。这样类型错误会立刻暴露不会等到前端调用时才发现。5.2 Prisma Client 找不到模型或字段改了schema.prisma后如果没重新生成 Prisma Clientctx.db.post可能报“Property post does not exist”。解决方法是运行pnpm prisma generate。db:push和db:migrate通常会自动触发 generate但如果你手动改了 schema 又没跑迁移就需要单独执行。另一个情况是字段名拼写错误。Prisma 的模型字段是大小写敏感的createdAt和createdat是两个不同的东西。编辑器有自动补全时不容易错但手写迁移脚本时要格外小心。5.3 前端调用返回 500 但控制台没细节tRPC 的错误默认在生产环境会隐藏堆栈开发环境则会在终端打印。如果你在浏览器看到 500先去跑pnpm dev的终端窗口看有没有红色报错。常见原因有数据库连接失败、Zod 校验失败但没处理、Prisma 查询抛异常。我建议在src/server/api/trpc.ts里配置errorFormatter把 Zod 的错误信息格式化后返回给前端。这样前端能直接看到哪个字段校验不通过不用猜。具体做法是在initTRPC.context().create()时传入errorFormatter社区里有现成的代码片段可以参考。提示开发阶段可以把TRPCError的cause打印出来但生产环境记得关掉避免泄露敏感信息。6. 从开发到部署t3code 项目上线前的检查清单6.1 环境变量与构建命令的对应关系T3 项目部署时.env文件不会被打包所以所有环境变量必须在部署平台的后台设置。需要检查的变量包括DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL如果用了 NextAuth。NEXTAUTH_URL在生产环境必须是你实际的域名否则登录回调会失败。构建命令通常是pnpm build启动命令是pnpm start。但如果你用了 Prisma构建前需要确保prisma generate已经执行。可以在package.json的build脚本里加上prisma generate next build这样每次构建都会重新生成 Client。6.2 数据库迁移在生产环境的执行时机生产环境不能用db:push要用prisma migrate deploy。这个命令会执行所有未应用的迁移文件不会修改 schema 也不会生成新迁移。你需要在部署流程里加一步在启动应用之前运行pnpm prisma migrate deploy。很多部署平台支持release command或pre-deploy钩子把迁移放在那里最合适。如果迁移失败应用不应该启动。我见过有人把迁移和应用启动并行执行结果应用启动时表还没建好直接报错。顺序必须是先迁移再启动。6.3 类型检查与 lint 在 CI 里的必要性T3 栈的核心卖点是类型安全但如果 CI 里不跑tsc --noEmit类型错误可能被 Next.js 的构建过程忽略Next.js 默认在构建时做类型检查但可以关闭。我建议在 CI 里显式加一步pnpm typecheck脚手架通常已经配好了这个脚本。另外pnpm lint也要跑ESLint 能抓出一些 tRPC 使用上的反模式比如在循环里调用useQuery。部署前的最后检查本地跑一遍pnpm build确认没有类型错误和 lint 错误。这一步能拦下大部分低级问题比上线后回滚强。7. 一些让 t3code 项目更顺手的个人习惯我在多个 T3 栈项目里反复调整后固定了几个小习惯。第一个是把 router 按业务域拆分而不是按 CRUD 拆分。比如post.ts里既有getAll也有create而不是建一个queries.ts和一个mutations.ts。这样改一个功能时只动一个文件心智负担小。第二个是在src/server/api/routers/下建一个_app.ts或者index.ts来统一导出但root.ts仍然手动注册。这样导入路径短一些但注册关系依然显式。我不推荐用动态导入自动注册 router因为类型推导会变弱而且出错了不好定位。第三个是给每个 procedure 加 JSDoc 注释说明输入输出和副作用。tRPC 的类型提示虽然强但不会告诉你这个 mutation 会发邮件还是会扣库存。注释是给未来的自己和同事看的。第四个是定期运行pnpm outdated检查依赖更新。T3 栈的各个包版本耦合较紧比如 tRPC 和 React Query 的版本要匹配Prisma 和prisma/client要一致。升级时最好一次只升一个跑完测试再升下一个。我试过一次性升级所有依赖结果 tRPC 的 API 变了改了半天才恢复。最后分享一个排查类型问题的技巧当AppRouter类型报错但看不出哪里不对时在root.ts里把appRouter的类型显式写出来或者用satisfies操作符约束一下TypeScript 会给出更具体的错误位置。这个办法帮我省过不少时间。