t3code 这个项目名我第一反应就是 T3 Stack 生态里的产物。如果你在 GitHub 或技术社区搜过这个词大概率会落到 create-t3-app 这个脚手架工具上——一个帮你把 TypeScript、Next.js、tRPC、Prisma、Tailwind CSS 全自动装配好的全栈项目模板。很多开发者会把自己基于这套组合建的仓库直接命名为 t3code图的就是一个快、稳、类型安全到底。这篇东西我想聊的不是官方文档的复读而是作为实际用 t3code 建过几个完整项目的人把整个技术选型的底层逻辑、每个核心组件的协作方式、从零搭建到生产部署的完整链路以及我踩过的那些坑一次性讲透。无论你是刚接触全栈的初学者还是想给团队快速搭一套内部工具的后端老手这篇都能帮你少走至少一个月的弯路。1. T3 Stack 生态的整体设计与选型思路1.1 为什么会有 t3code 这类项目我最早知道 create-t3-app是从一位叫 Theo 的开发者开始的。他在直播里反复强调一个观点现在的全栈开发最大的时间黑洞不是写业务逻辑而是写客户端和服务端之间那层重复的类型定义。你想想传统模式下前端调后端接口是不是得先看接口文档然后手写类型定义接口一变前端又得同步改。用 GraphQL 能缓解但引入 schema、codegen、缓存那一堆概念对中小项目来说负担并不轻。t3code 这类项目的意义就是用一个完整的脚手架把端到端类型安全这件事变成默认能力。你写一个后端 query前端的 useQuery 立刻就有对应的类型提示连字段名拼错编译器都能直接报错省掉的完全是重复劳动。这套组合不是随便拼出来的。Next.js 提供服务端渲染和前后端一体的工程结构tRPC 解决类型安全的 API 调用Prisma 管数据库层Tailwind CSS 解决样式迭代速度TypeScript 是贯穿全局的粘合剂。1.2 选型背后的取舍逻辑很多人问我为什么不用 NestJS 加 REST为什么不用 Express 加 Mongoose答案其实很朴实t3code 适合的场景是前后端都由同一个团队甚至同一个人维护的项目。它强调的是开发效率和类型安全而不是大团队里严格的接口契约治理。拿 tRPC 来说它和 GraphQL 的最本质区别就是tRPC 没有自己的 schema 语言所有类型定义都来自 TypeScript。这意味着你不会有两套真相来源不会出现服务端改了字段但前端类型没同步的情况。代价是tRPC 只适合 TypeScript 项目其他语言想接入就没戏了——但这个代价对个人开发者和中小团队来说根本不算代价。Prisma 的选择也是同样的思路。它把 SQL 封装成类型安全的查询语句并且通过prisma generate直接从 schema 生成完整的 client 类型。你定义了一个Post模型代码里prisma.post.findMany()返回的类型就是自动推断的Post[]DBA 看了想骂人但对业务开发者来说这种开发体验真的会上瘾。整套思路说到底就是一句话用类型系统尽可能在编译期消灭运行期才会暴露的问题。这也是 t3code 类项目能持续保持热度的根本原因。2. 核心组件深度拆解每个零件到底怎么干活2.1 Next.js App Router 的新协作模式新版 create-t3-app 默认使用 App Router而不再用 Pages Router。这个变化在 t3code 项目里体现得特别明显服务端组件和客户端组件的边界直接决定了 tRPC 该怎么调用。先说结论尽量在服务端组件里直接调用服务端逻辑只有需要交互式状态更新的时候才用客户端组件调 tRPC query。服务端组件的写法非常直接比如你要拉取文章列表直接在组件里这样写import { api } from ~/trpc/server; export default async function PostsPage() { const posts await api.post.list(); return ( div {posts.map((post) ( div key{post.id} h2{post.title}/h2 p{post.content}/p /div ))} /div ); }这段代码编译成框架层之后tRPC的 server caller 会在服务端直接调用 router 的方法不发 HTTP 请求不需要序列化和反序列化性能上比传统的 BFF 模式好很多。同时因为api.post.list()的返回类型是从 router 里直接推断的所以posts的每一个字段都自带提示写错了立刻标红。这种开发体验在旧模式下完全做不到。但要注意服务端组件不能用任何事件监听函数不能有useState也不存在生命周期。所以一旦你需要在页面上点赞、修改文章、搜索过滤就必须切换到客户端组件。这种能服务端处理就不客户端处理的思路是 App Router 时代 t3code 项目遵循的第一原则。2.2 tRPC让 API 层隐形的关键tRPC 整件事的逻辑可以用一个类比说清楚如果你家楼上楼下同时住着你自己你取东西根本不需要打电话再让对方递下来——直接走过去拿就行了。tRPC 在同一个 TypeScript 项目里就是把客户端和服务端放进同一个类型空间让函数调用取代网络请求。在 t3code 项目里核心的 router 定义通常在src/server/api/root.ts和src/server/api/routers/目录下。每一个 router 由若干个 procedure 组成procedure 就是具体的 API 端点import { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { prisma } from ~/server/db; export const postRouter createTRPCRouter({ list: publicProcedure.query(async () { return prisma.post.findMany({ orderBy: { createdAt: desc } }); }), create: publicProcedure .input(z.object({ title: z.string().min(1), content: z.string().min(1), })) .mutation(async ({ input, ctx }) { return prisma.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session?.user?.id ?? , }, }); }), });我没给你省略任何东西这就是完整的核心链路。客户端调用的时候你不需要手写 fetch、不需要处理 JSON、不需要关心 URLuse client; import { api } from ~/trpc/react; export function PostList() { const [posts] api.post.list.useSuspenseQuery(); const utils api.useUtils(); const addPost api.post.create.useMutation({ async onSuccess() { await utils.post.list.invalidate(); }, }); // ... UI 部分 }useSuspenseQuery会返回类型安全的响应数据useMutation执行完后invalidate让列表端自动重新拉取用户看到的新数据不需要你手动刷新。类型错误、字段名错误、参数错误全在编辑器里直接暴露。API 层这个概念在 t3code 项目里基本就隐形了。2.3 Prisma数据库模型即类型源头Prisma 在 t3code 中的作用是把数据库的表结构和 TypeScript 类型绑定在一起。你的prisma/schema.prisma就是唯一的模型定义源model User { id String id default(cuid()) name String? email String unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Post { id String id default(cuid()) title String content String author User relation(fields: [authorId], references: [id]) authorId String createdAt DateTime default(now()) updatedAt DateTime updatedAt }关键是运行npx prisma migrate dev --name init后Prisma 会同时完成两件事一是推送 SQL 变更到数据库二是重新生成 client 类型。之后你在任意 tRPC procedure 里写prisma.post.findMany()返回类型就和 schema 完全一致绝无偏差。这也是 t3code 项目里最容易出问题的地方后面讲排查的时候我专门说。2.4 Tailwind CSS 与 shadcn/ui 组合的迭代优势Tailwind 在 t3code 里默认被集成到 PostCSS 层你不用写一个 CSS 文件直接在 JSX 里堆 class 就行。这套组合对个人项目的核心价值是放弃上下文切换你不需要去维护一套独立的样式文件组件和样式在同一个文件里对应地方改。当你的页面结构频繁调整、原型迭代飞快的阶段这种模式能省下很多找选择器的时间。如果要做得更快速t3code 默认还会引入组件库 shadcn/ui。它和其他组件库最大的区别是它会直接把组件的源码复制到你的项目里而不是提供一个黑盒依赖。你可以任意改它的样式、逻辑EA 完全在你的控制之下。Tailwind 加 shadcn/ui 的组合基本就是设计感和开发速度的平衡解。3. 从零搭建 t3code 项目的完整实操记录3.1 环境准备与初始化我先说你前后要准备什么Node.js 18.18 以上版本建议直接用最新的 LTS。一个包管理器npm、pnpm、bun 都可以我推荐 pnpm依赖安装快很多。如果你要用 PostgreSQL本机先装好或者用 Docker 起一个如果你暂时不想装数据库也能先用 SQLite 起步代价是生产部署还得再迁。初始化项目的命令非常简单跑这条npx create-t3-applatest my-t3code-app注意了如果你用 pnpm 就全行都不需要npx前缀直接pnpm create t3-applatest my-t3code-app。运行后它会问你两个问题一个是包管理器另一个是是否使用 TypeScript默认是千万别不选然后就是一套功能勾选Next.js默认不可去掉Tailwind CSS建议保留tRPC建议保留Prisma建议保留NextAuth.js可选取决于你要不要用户体系App Router新版默认不可去掉如果你只是先跑通流程那就全选它生成的模板是能直接 start 的完整可运行项目。3.2 环境变量的设置逻辑初始化完成后第一件事是把.env.example复制成.env然后逐个填值。很多人会直接跳过这一步导致后面一堆诡异报错。DATABASE_URLpostgresql://postgres:passwordlocalhost:5432/my-t3code-app?schemapublic NEXTAUTH_SECRET随便找个长字符串建议用 openssl rand -base64 32 生成 NEXTAUTH_URLhttp://localhost:3000NEXTAUTH_SECRET是登录会话加密用的开发环境随便填一个长字符串就能跑生产环境必须改成一个环境变量管理的真实随机值。DATABASE_URL如果用 SQLite 就是file:./db.sqlite。T3 脚手架默认还会在src/env.mjs里用 zod 写一个环境变量校验层。这地方是很多人第一次见到的反向把关——你在启动前它就会告诉你环境变量谁没填、谁格式不对而不是等你跑到某个功能才爆出来。3.3 定义数据模型并生成 API数据库表怎么设计不重要重要的是跑迁移的姿势。我以文章系统为例把模拟数据先精简一下设计User和Post两张表运行两条命令npx prisma migrate dev --name init npx prisma db seed跑完以后数据库表就创建好了client 也重新生成了。此时如果你想加一个公开接口最新文章你不用写额外的 API 路由直接去src/server/api/routers/post.ts里加一个 procedure然后前端就能直接调业务代码不用考虑这个接口要不要放在另一个服务。3.4 前端接入与联调在前端侧T3 脚手架会在src/trpc/react.tsx里帮你初始化好api对象它包含了所有 router 的类型。你在任何客户端组件里import { api } from ~/trpc/react然后api.post.list.useQuery()TypeScript 会像变魔术一样告诉你返回数据的完整类型。我自己走一遍完整流程下来从初始化到页面上成功显示数据库里的文章中间几乎不会卡壳。这就是 t3code 的优势它把最容易出的配置问题全部在脚手架阶段消灭掉了。4. 常见问题与排查技巧实录4.1 Prisma Client 类型没有生成这个是我见过出现频率最高的坑没有之一。症状是Error: query is not defined或者编辑器里prisma.post直接标红。原因基本都是你在改完schema.prisma后没有重新跑npx prisma generate。注意migrate dev会连带生成但如果你直接手动改了 schema 而没跑 migration那 client 类型不会同步。解决办法也很简单npx prisma generate或者干脆npm run postinstall——因为 T3 脚手架默认在 postinstall 里挂了prisma generate你重新npm install它也会重新生成。4.2 环境变量导致的server-only类报错t3code 项目里所有在src/server/下的代码默认是import server-only的意思是你不能在客户端组件里 import 它。如果你非要 import就会看到Server-only之类的错误。很多新手在服务端路由、db、auth 之间互相引用时踩了这个坑。解决办法只有一个严格遵守服务端内容只在服务端引用的目录边界。页面组件里如果需要 session 或数据库数据优先在服务端组件里取好再传 props 给客户端组件。4.3 tRPC query 缓存不更新用useMutation改了数据之后页面上的列表不刷新。这个问题我用大白话解释tRPC 的 query 默认是走 React Query 的缓存mutation 成功之后你必须手动让它失效它才会重新向服务端拉数据。合规的操作我刚才写过了就是utils.post.list.invalidate()。如果不做你只能手动刷新页面这体验就太差了。4.4 NextAuth 登录后 session 拿不到 user id很多 t3code 项目的业务需要当前用户的 ID比如authorId。如果你发现ctx.session?.user?.id是undefined十有八九是 Prisma 的 User 模型里没有给id这个字段加映射或者你在 session callback 里忘了把user.id塞进 session。你需要检查src/server/auth.ts里的sessioncallbacksession({ session, user }) { if (session.user) { session.user.id user.id; } return session; }这样 tRPC context 里的 session 就能拿到 user.id 了。