
如果你混前端圈最近这两年肯定绕不开一个名字——t3code。更准确地说它是社区对 T3 Stack 的昵称项目全称叫 create-t3-app在 GitHub 上热度极高。我第一次在推上刷到这个标签时还以为是某个新框架点进去才发现它其实是一套“帮你把全栈项目一秒钟搭好”的脚手架模板核心组合是 Next.js TypeScript Tailwind CSS tRPC Prisma。我最初对这玩意儿有点不屑觉得不就是一个模板嘛我自己手动搭也是一样。但真正用它在两个项目里从头跑到部署之后我只想说一句真香。这篇文章我就把从零初始化、核心链路开发、环境配置到部署上线的完整过程捋一遍包括我踩过的坑、摸索出的细节和排查套路给想上手 t3code 或者正在用它但卡壳的朋友一份能直接抄作业的参考。1. t3code到底是什么TypeScript全栈的“一键全家桶”1.1 T3 Stack的五件套拆解t3code 背后是一套技术选型组合也就是 T3 Stack。这里先把每件套到底是干嘛的讲清楚不然你初始化完还是会一头雾水。Next.jsReact 的元框架提供文件路由、服务端渲染SSR、静态生成SSG、API 路由等能力。t3code 默认用的是 App Router 模式整个应用既是前端页面又能承担后端 API 的角色省掉单独部署一个 Express 服务的开销。TypeScript给 JavaScript 加上静态类型。t3code 最核心的卖点就是“端到端类型安全”没有 TS 这个前提后面 tRPC 的类型联动根本玩不起来。Tailwind CSS原子化 CSS 框架。它最大的意义是让你不离开 HTML 就能完成全部样式不需要起类名、不需要写独立的样式文件。如果你不熟 CSS上手成本极低如果你熟开发速度也能快一截。tRPC这就是整个技术栈的“灵魂”了。它让你在前后端之间调用函数时类型直接从后端“流”到前端不需要手写 API 文档、不需要生成 OpenAPI 类型、不需要在 fetch 请求那里再定义一次请求体类型。后端的 query 和 mutation 怎么定义前端调用时就有对应的智能提示和编译期检查。Prisma数据库 ORM。用 schema 文件定义数据模型然后通过命令行生成类型安全的数据库客户端。配合迁移工具数据库表结构变动都能用版本化迁移文件管理起来。除这五件之外t3code 还把 NextAuth.js认证、Turborepo单仓库等也是可选项。你在初始化时如果想保留默认全家桶那它们也会被整合进去。1.2 为什么这个组合能流行要说 t3code 为什么能火痛点就很明确了。以前做全栈项目基本流程是后端用 Node 搭一个服务路由和控制器写一大堆前端单独建项目接口调用全靠手写 fetch 和类型断言中间还要维护一份接口文档改一个字段就要前后端各改一遍。沟通成本和维护成本都很高。t3code 的思路简单粗暴把类型安全从数据库一路打通到 UI 层。Prisma 根据 schema 生成类型安全的数据库客户端tRPC 把服务端函数的参数和返回值类型自动暴露给前端Next.js 负责把两者粘在同一个项目里。你在前端写一个调用传的参数类型不对编辑器直接标红后端改了返回值结构前端引用位置立刻报错。这种“改一处、全链路漂红”的能力在大项目里的价值极高。还有一点很关键它把“选择困难症”治好了。很多初学者卡在“该用什么框架”这个问题上几天都前进不了t3code 直接给了你一套被社区反复验证过的稳妥组合让你把精力放在业务逻辑而不是技术选型上。这就是它能快速流行起来的最主要原因。1.3 谁适合用t3code根据我的实际使用体验这几类人用 t3code 非常划算独立开发者想快速做 MVP、验证产品想法不需要花大量时间搭基础设施。t3code 跑起来一个全栈项目只要几分钟比手搭省太多时间。中小型团队前后端都要写但人手不多t3code 减少联调和文档成本类型安全在协作中的价值巨大。想系统学习现代全栈的初学者一个项目里把 Next.js、Prisma、tRPC、Tailwind 全部用起来学到的都是当前社区主流方案。喜欢类型安全的人如果你写 TS 写惯了那就更不用犹豫了。相对不适合的场景也有比如超大规模单体应用或者对前端体积极度敏感的纯展示型站点。还有如果你的团队已经有成熟的微服务架构那 t3code 更适合作为其中一个服务而不是整个架构的唯一选择。2. 初始化项目5分钟跑起来一个全栈骨架2.1 前置环境准备动手之前先把环境准备好。我的建议版本如下Node.js18.17 或 20.x LTS不要用太旧的版本Next.js 14 对 Node 版本有要求。包管理器npm、pnpm 或 yarn 任选我个人推荐 pnpm安装速度快、磁盘占用小最重要的是一致性好。数据库开发阶段先用 SQLite 最省事零配置但生产环境强烈建议换 PostgreSQL。如果你本地装了 Docker用 docker 跑一个 postgres 容器也很方便。Git最好提前全局配置好 user.name 和 user.email因为 create-t3-app 初始化后会自动帮你创建 git 仓库。这些准备好之后就可以进入正题了。2.2 使用create-t3-app脚手架t3code 官方网站上提供了一条命令本地执行以下命令即可开始npm create t3-applatest或者用 pnpmpnpm create t3-applatest执行后命令行会问你这个项目叫什么名字输入进去它还会问你要使用哪些可选功能。这里我把默认选项逐个说明一下以免你全点完不知道每一项是干嘛的。NextAuth.js用户认证方案。如果你的应用需要登录、注册、OAuth 第三方登录选上如果只是展示站可以不选。Prisma数据库 ORM几乎建议必选不然这个模板的核心体验就没了。Tailwind CSS样式方案建议选。tRPCAPI 方案选上才有端到端类型安全。如果只是纯展示项目可选可不选。App Router如果版本支持默认推荐 App Router这是 Next.js 的未来方向。Import Alias路径别名默认~或/*都行主要是让 import 路径更干净。我通常保持默认全选然后一路回车。等依赖安装完它会提示cd 你的项目名 npm run dev。跑起来之后浏览器打开 localhost:3000看到欢迎页就说明骨架已经通了。2.3 目录结构与核心文件解读初始化完成后的目录结构是非常有讲究的我截几个关键目录给你看. ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ └── auth/ │ │ │ └── [...nextauth]/ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ │ └── example.ts │ │ │ ├── root.ts │ │ │ └── trpc.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── server.ts │ │ └── react.tsx │ └── styles/ │ └── globals.css ├── .env └── package.json这里几个关键文件的作用你得先搞清楚prisma/schema.prisma所有数据模型定义的地方。src/server/db.tsPrismaClient 单例导出避免开发模式下数据库连接数爆炸。src/server/api/root.ts所有子路由汇聚成 appRouter 的地方。src/server/api/trpc.tstRPC 的 router 创建、公共 procedure 和受保护 procedure 的定义处。src/trpc/react.tsx给前端用的 tRPC React Query 封装。src/app/api/auth/[...nextauth]/route.tsNextAuth 在 App Router 下的 API 入口所有认证请求都打到这个路由。只要把这些文件的功能串起来整个项目的请求链路就清晰了前端组件调用src/trpc/react.tsx导出的 hooks它们把请求发给 Next.js 的 API 路由tRPC 在服务端根据 router 配置找到对应 procedure 执行过程中如果用到数据库就走 Prisma如果涉及用户信息就走 NextAuth 的 session。3. 核心开发流程从数据库到页面的完整链路3.1 数据库建模与Prisma迁移开发一个 t3code 项目第一步永远是定义数据模型。用我最常用的一个“笔记应用”例子来说明。打开prisma/schema.prisma它原本长这样generator client { provider prisma-client-js } datasource db { provider sqlite url env(DATABASE_URL) }现在我要加一个笔记表和一个用户表用户表其实 NextAuth 默认模板里没有需要自己建修改后model User { id String id default(cuid()) name String? email String? unique emailVerified DateTime? image String? accounts Account[] sessions Session[] notes Note[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Note { 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 }然后把原来的 SQLite 连接字符串改成 PostgreSQL如果本机用 Docker 跑连接串长这样DATABASE_URLpostgresql://postgres:postgreslocalhost:5432/t3notes?schemapublic改完之后执行迁移命令npx prisma migrate dev --name init这个命令会做两件事对比 schema 里的模型和数据库当前状态生成一个迁移 SQL 文件然后执行它同时把最新的 Prisma Client 生成到node_modules/.prisma/client让src/server/db.ts导出的客户端拿到最新类型。如果你用的是 SQLite那连接串只需要改成DATABASE_URLfile:./db.sqlite不需要起任何服务迁移和查询都会自动生成对应文件很适合前期快速开发。3.2 tRPC路由类型安全的API层数据模型有了下一步就是把数据库操作暴露成 API。打开src/server/api/routers/example.ts这是模板自带的示例 router教你写简单的 query。我把它改造成笔记的 CRUDimport { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from /server/api/trpc; export const noteRouter createTRPCRouter({ getAll: protectedProcedure .query(async ({ ctx }) { return ctx.db.note.findMany({ where: { authorId: ctx.session.user.id }, orderBy: { createdAt: desc }, }); }), getById: protectedProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) { return ctx.db.note.findFirst({ where: { id: input.id, authorId: ctx.session.user.id }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), }), ) .mutation(async ({ ctx, input }) { return ctx.db.note.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { await ctx.db.note.deleteMany({ where: { id: input.id, authorId: ctx.session.user.id }, }); return { success: true }; }), });注意到几个细节protectedProcedure表示这个接口必须登录才能调对应的实现是在trpc.ts里用getServerSession判断用户身份。input用 zod 校验参数。t3code 集成 zod 是标配前端调用时也能拿到完整的类型提示。ctx.db.note.findMany里的ctx.db是 tRPC context 中注入的 PrismaClient 实例请求进来时会自动创建用完自动释放。删除操作用的是deleteMany而不是delete并且带上了authorId条件这是防止越权删除别人数据的一个很简单的做法。定义好 router 之后需要把它挂载到src/server/api/root.ts里去import { noteRouter } from /server/api/routers/note; import { createTRPCRouter } from /server/api/trpc; export const appRouter createTRPCRouter({ note: noteRouter, });这样前端调用路径就是api.note.getAll。3.3 前端页面与组件调用后端接口定义完成前端调用超级简单。在任意客户端组件记得加use client里直接这样写use client; import { api } from /trpc/react; export function NoteList() { const { data: notes, isLoading, refetch } api.note.getAll.useQuery(); if (isLoading) return div加载中.../div; return ( ul {notes?.map((note) ( li key{note.id} h2{note.title}/h2 p{note.content}/p button onClick{() { api.note.delete.useMutation().mutate({ id: note.id }); }} 删除 /button /li ))} /ul ); }create 方法的使用也类似但有一点需要注意useMutation 返回的 mutation 对象的mutateAsync和mutate方法调用时机不同错误处理策略也不一样。我建议在需要拿到返回值做后续操作时用mutateAsync在只需要触发操作不需要关心结果时用mutate。const createNote api.note.create.useMutation({ onSuccess: () { // 让查询自动失效重新拉取最新列表 refetch(); }, }); createNote.mutate({ title: 第一篇笔记, content: 内容 });这里refetch是useQuery返回的或者更优雅的做法是用 tRPC 的useUtils().note.getAll.invalidate()让列表自动失效重取。t3code 集成了 TanStack Query很多缓存和失效策略都能直接用这是它比手写 fetch 强太多的地方。3.4 NextAuth认证集成几乎所有真实项目都需要用户系统t3code 模板已经帮你把 NextAuth 接进来了。默认配置在src/server/auth.tsimport { getServerSession } from next-auth; import { authOptions } from /server/auth; export const getServerAuthSession () getServerSession(authOptions);而authOptions里可以配置各种 Provider。我最常用的是 GitHub 登录import GitHub from next-auth/providers/github; export const authOptions { providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID ?? , clientSecret: process.env.GITHUB_CLIENT_SECRET ?? , }), ], // ...其他配置 };然后在.env文件里填上GITHUB_CLIENT_IDyour_client_id GITHUB_CLIENT_SECRETyour_client_secret NEXTAUTH_SECRETyour_secret NEXTAUTH_URLhttp://localhost:3000这时打开页面点登录按钮跳到 GitHub 授权页授权后回跳session 就建立起来了。在受保护的页面里直接在服务端组件里拿 sessionimport { getServerAuthSession } from /server/auth; export default async function DashboardPage() { const session await getServerAuthSession(); if (!session) { return div请先登录/div; } return div欢迎回来{session.user.name}/div; }在客户端组件里则用useSessionuse client; import { useSession } from next-auth/react; export function UserBadge() { const { data: session } useSession(); return span{session?.user?.name ?? 未登录}/span; }整个认证流程在 t3code 里非常顺滑不需要自己画登录页、写 session 存储、做安全 cookie这些都是模板默认处理好的。4. 开发提效与避坑我实际踩过的几个雷4.1 环境变量与常见配置坑t3code 有一个.env.example文件但它不会把真实环境变量自动填进去。最常见的问题就是项目跑不起来第一反应应该去看.env文件。cp .env.example .env注意几个变量DATABASE_URL如果填了 SQLite就写file:./db.sqlite放在项目内如果填 PostgreSQL要确保数据库已启动、用户名密码正确。NEXTAUTH_SECRET可以用一个随机字符串也可以运行下面命令生成openssl rand -base64 32NEXTAUTH_URL在开发环境可以填http://localhost:3000但生产环境必须换成线上域名否则登录跳转会出现奇怪的 404。还有个小细节Next.js 的NEXTAUTH_URL如果忘了设置在生产环境部署时会默认从请求头里推断容易跟实际域名不一致。我建议不管在哪个环境都明确写上。4.2 Prisma与tRPC类型不刷新的问题这是新手最容易卡住的点。你在schema.prisma加了字段执行了prisma migrate dev但编辑器里 tRPC 返回的类型还是旧的。原因很简单Prisma Client 的类型是生成在node_modules里的编辑器需要重新加载 TS 语言服务。一旦改了 schema 并迁移完手动重启一下 TS 服务端VS Code 里 Command Palette 搜 “TypeScript: Restart TS Server”或者重启编辑器类型就会刷新。另外注意一点ctx.db.note.findMany()返回的类型包含所有字段如果你在前端只想用部分字段直接解构就行了不用重新做类型声明。它的类型是从数据库一路传到前端的中间没有任何“数据管道变质”的风险。4.3 NextAuth与tRPC的适配细节t3code 模板里protectedProcedure的实现大概是export const protectedProcedure createTRPCProcedure() .use(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user } } }); });它从服务器 session 直接拿当前登录用户注入到 tRPC 的 context 里。这样在后端 procedure 里写ctx.session.user.id才不会炸。但如果你在layout.tsx里调用了SessionProvider又在服务端组件里用了getServerAuthSession需要留意session 的更新同步。比如用户改名后客户端 session 可能还是旧的。解决办法是在需要的地方调用useSession().update()手动更新。我的建议是尽量在服务端组件里拿 session客户端只在交互逻辑需要用户信息时才用useSession减少 session 过期和同步问题。5. 部署上线从本机到生产环境5.1 Vercel部署全流程t3code 官方最推荐的部署平台是 Vercel因为它和 Next.js 是同一家公司对 App Router 的适配几乎零摩擦。部署步骤也很简单将项目推到 GitHub然后在 Vercel 导入仓库构建设置里框架选择 Next.js然后配置环境变量。必填的环境变量还是那几个DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL以及你的 OAuth Provider 的回调地址里要加上 Vercel 分配的线上域名。以 GitHub OAuth 为例在 GitHub 开发者设置里把http://localhost:3000和https://你的域名都加到允许回调的列表里。数据库这里有一个关键注意点Vercel 的无服务器函数是无状态的每个请求可能连到不同的实例所以像 Prisma 这种必须保持连接池的客户端数据库连接串要配置好连接池模式。如果你的数据库托管服务提供对应的连接串比如 Supabase 的 pooler 连接串直接在 DATABASE_URL 里使用即可。如果用 SQLite会发现部署后数据根本存不住——这就是为什么前面反复强调生产环境要换 PostgreSQL 的原因。5.2 常见部署问题排查我在部署 t3code 项目时遇到过几个高频问题直接列出来供你对照问题现象可能原因解决办法部署后访问 404Vercel 构建设置里没有识别到 Next.js检查框架预设选择 Next.js 并确认 package.json 正确数据库操作报错连不上数据库DATABASE_URL 里地址是 localhost改为远程数据库连接串不要用 localhost登录跳转到 localhostNEXTAUTH_URL 没设置或设置的是旧地址改成线上域名迁移没执行找不到表Prisma migrate 没有在生产环境执行在部署构建命令里加prisma migrate deploy prisma generateAPI 请求全部超时数据库连接池不够或者查询太重检查数据库连接限制用连接池模式这些坑我都踩过一遍尤其是本地 SQLite 数据在 Vercel 上完全消失这个问题浪费了我整整一个下午。在这里再次强调如果不是纯粹的学习项目一开始就直接上 PostgreSQL别偷懒。6. 常见问题排查速查表做一个速查表把日常开发里最常碰到的几个问题一次性整理出来你遇到时可以快速对照。症状可能的原因排查顺序修复方法pnpm dev启动后浏览器白屏/报 500环境变量缺失先看控制台输出的报错检查.env是否完整从.env.example复制一份.env填好所有变量prisma migrate dev报 P1001数据库连不上检查DATABASE_URL连接串、数据库服务是否启动确认连接串里主机名、端口、用户名密码正确前端拿到undefined没有登录/tRPC 路由没挂载看浏览器 Network 请求状态检查 root.ts确认是否登录确认 router 已挂载到appRouter登录后跳回首页但 session 为空回调地址或 cookie 域名不对查看 NextAuth 日志和 GitHub 授权配置检查NEXTAUTH_URL和 Provider 回调地址自己在本地调试时遇到 prisma client 不是最新TS Server 缓存改完 schema 后重开 TS Server重启 TS Server 或重启编辑器部署到 Vercel 后 API 返回 500数据库连接串不对或缺少迁移查看 Vercel 日志修改DATABASE_URL在构建命令里加上 migrate删除操作改了别人的数据没加用户条件检查 procedure 里是否使用了deleteMany并带 authorId给所有涉及用户数据的查询都加authorId条件这张表我建议收藏一下至少能帮你省掉 50% 的搜索时间。7. 写在最后的几条经验t3code 用到现在我有一个很深的体会它最大的价值不在“帮你省了几分钟初始化时间”而在于它的默认选择强制你走了一条正确的路。端到端类型安全不是玄学它是实打实的效率提升。以前我写全栈最怕的就是接口文档过期、参数命名不一致、类型对不上现在这些问题的发生频率直线下降因为编译器早就帮你挡住了大多数低级错误。还有一点小建议如果你决定用 t3code 做正式项目数据库就直接选 PostgreSQL 起步哪怕前期用的是免费托管服务也行。别用 SQLite 做生产数据存储这不是 t3code 的问题而是所有无服务器部署环境都会遇到的物理限制。最后再分享一个个人习惯每次初始化 t3code 项目后我会顺手把example路由和页面删掉替换成自己的第一张业务表。这种从“模板体验”到“真实业务”的切换会让你更快地理解这个技术栈的工作方式。希望这篇内容能帮你少踩一些坑把时间花在真正有意思的业务逻辑上。