提到全栈开发,第一次听到t3code这个名字时,我一度以为它只是又一个模板仓库。直到我真正把它跑起来,才意识到它其实是把 T3 Stack 的工程约定固化下来的一套实践型脚手架。T3 Stack 指的是 Next.js + tRPC + Tailwind CSS + TypeScript 这套组合,而t3code在这个基础上把 Prisma、NextAuth、Zod 这些周边能力全部编排成可以直接落地的项目骨架。对于想写类型安全全栈应用、又不想从零折腾配置的人来说,t3code能帮你把环境准备、目录约定、核心链路一次性定下来,装上依赖就能直接写业务代码。这篇文章我会从设计思路讲起,再带你走一遍实际搭建过程,最后把我在踩坑中积累的排查经验一并交底,希望对你上手这套技术栈有实际帮助。
1. 项目定位与整体设计思路
1.1 t3code 到底解决什么问题
很多人刚接触 T3 Stack 时,最大的困扰不是技术本身难,而是“组件太多,不知道它们怎么协同”。Next.js 管页面和路由,tRPC 管接口,Prisma 管数据库,NextAuth 管登录,Zod 管校验——每一块单独看都不复杂,但放在同一个项目里,目录应该怎么分、类型应该怎么流动、哪里必须写 server 代码哪里必须写 client 代码,文档里不会告诉你最终答案。t3code做的最核心的一件事,就是把这种“约定”变成代码:它假定你走 T3 全家桶,然后给你一个已经跑通的、可扩展的启动项目,你只需要往里填业务逻辑。
拿我自己来说,以前撸一个新全栈项目,光是初始化 Next.js、配置 tRPC 服务端、挂 Prisma Client、调 NextAuth 回调,加在一起至少要半天。即便照着官方文档一步步来,中间还会遇到 Node 版本不兼容、Prisma 生成失败、tRPC 类型没有自动导出这类小问题。t3code相当于把这类重复劳动全部提前过了一遍,你拿到手的是已经验过逻辑的骨架,剩下的是业务建模和页面开发,时间成本能省下一大半。
1.2 设计原则:约定大于配置,但保留灵活度
t3code延续了 T3 Stack 的核心观点:浏览器端和服务器端都要尽可能类型安全。类型安全不是指代码不会出 bug,而是指数据库表结构、接口输入输出、页面 props 这三层之间能共享同一个类型源头。改一处,编译期就能把所有关联点标出来,避免运行时才发现字段对不上。
为了达到这个目标,t3code有几个很明显的工程取向:
第一,API 层默认走 tRPC,而不是手写 REST 路由。理由很简单,tRPC 能在前后端之间直接暴露类型定义,不用各自维护一份接口文档。第二,数据库访问层通过 Prisma 统一管理,表结构变更走 migration,而不是在业务代码里散落一堆 SQL。第三,校验逻辑集中在输入边界,也就是在 tRPC 的 procedure 入口用 Zod 拦截掉异常参数,下游代码就可以默认参数是合法的。
这套设计不是t3code独创的,但它把这些都固化成了默认行为。你甚至可以理解为,t3code是一个“不用动脑子的 T3 Stack 启动器”——方向已经给你定好了,除非你有特殊需求,否则照着默认走就是最舒服的路径。
2. 核心技术栈拆解
2.1 Next.js App Router:路由与服务端的融合
t3code选择 Next.js App Router 作为基座,是因为它把服务端组件和客户端组件的边界拉得非常清晰。一个页面里,读取数据库、调用接口这些操作可以放在 Server Component 里执行,只有需要交互、需要状态的部分才标记use client。这种模式下,很多原本要在前端发请求的场景可以直接在服务端读取数据,页面渲染速度明显提升,代码量也更少。
但随之而来的坑也有:App Router 下,凡是使用 hooks 的组件都必须在文件顶部写"use client"。很多新人经常在同一个文件里既想用服务端拿数据,又想用 useState,结果页面直接报错。t3code的目录结构有意把“服务端数据获取组件”和“客户端交互组件”分开,你只需要遵守这个约定,就不会踩到组件边界问题。
2.2 tRPC:端到端类型安全的关键
tRPC 可以说是整套 T3 Stack 里“最爽”的一环,也是t3code最具特点的地方。它不需要你定义 REST 路由、不需要手写请求函数、不需要单独维护 SDK。你在 server 端定义一个 procedure,client 端会自动生成对应的调用函数,并且入参出参的形状在编辑器里是实时可见的。
举个例子,你在服务端写好getUserList这个 query,前端调用时写trpc.user.list.fetch(),如果后端把返回字段改了,前端编辑器会立刻标红。这种体验在 REST 风格下几乎不可能实现。t3code默认把 tRPC 的 router 拆分成几个子模块,比如post、user、todo,每个子模块再挂各种 procedure,业务增长之后依然能保持清晰的命名空间。
需要注意,tRPC 的数据流方向是“客户端 → 服务端函数调用”,所有 procedure 默认在服务端执行,所以你不必担心数据库操作或者敏感逻辑被暴露到浏览器。你只需要明确知道自己的查询什么时候应该用query()、什么时候应该用useMutation()。
2.3 Prisma 与数据库层:模型先行
数据库这层,t3code默认用的是 Prisma ORM,正常情况下接的是 PostgreSQL 或 SQLite。Prisma 的好处是,它让你先定义一个 schema 文件,再根据 schema 自动生成类型安全的数据库客户端。你在schema.prisma里写模型,然后执行prisma migrate dev,它就能同步生成 migration 文件并更新数据库结构。
我在实际使用中最满意的一点是,Prisma 生成的 TypeScript 类型可以无缝流入 tRPC 的返回类型。也就是说,数据库表结构直接决定接口返回类型,中间不需要再做 DTO 转换层。这在小型和中型项目里极大减少了很多无效劳动。等你的业务真正复杂到需要隐藏字段、聚合字段时,再在 tRPC 层做映射也不迟,但一开始完全没必要叠一堆抽象。
选择 SQLite 还是 PostgreSQL 取决于你要部署到哪里。t3code脚手架里通常默认支持两者,只需在环境变量里切换数据库连接串即可。开发阶段用 SQLite 最省事,生产环境再换 PostgreSQL,迁移成本基本只在于重新执行一次 migration。
2.4 认证与校验:NextAuth + Zod 的常见组合
认证部分,T3 Stack 生态里最常见的是 NextAuth.js。t3code一般会默认接入 Credentials 或 GitHub OAuth 中的一种,并把登录状态通过useSession暴露给前端。这里有一个经常被忽略的点:你不仅要保存“用户是谁”,还要在 tRPC 的 procedure 里控制“谁能访问”。
t3code的实践是在每个受保护 procedure 上挂一个 middleware,通过getServerSession读取会话状态,判断未登录时直接抛错。这个操作看起来简单,但很多项目最初没做,等上线后才发现接口可以被任何人直接调用。另一个关键组件是 Zod,它负责校验表单或接口入参。你可以在一个 schema 里定义name: z.string().min(2).max(50),tRPC 会自动在请求进入 handler 之前做校验,校验失败直接返回友好错误信息,不需要你手写一堆 if 判断。
3. 环境准备与落地实操
3.1 环境与依赖安装
动手之前,先把本地环境准备好。我的建议是:Node.js 使用 18 LTS 或更高版本,包管理器选 pnpm 或 npm 都可以,但一旦定了就尽量在一个项目里统一。t3code的核心依赖包括next、react、trpc、@trpc/next、@trpc/server、zod、prisma、next-auth这些,脚手架里通常会自带 lock 文件,你只需要安装即可。
如果你的网络环境同步官方源比较慢,可以临时设置 npm 镜像,但注意不要把它提交进项目配置文件里,免得团队成员克隆后环境不一致。安装完成后,第一件事不是改代码,而是先跑一次npm run dev,看看默认首页能否正常渲染。这一步能帮你快速判断本地环境是否满足要求,省得改了一堆代码之后才发现基础起不来。
3.2 初始化数据库与环境变量
t3code里数据库的初始化一般分为三步:复制环境变量模板、修改连接串、执行迁移。
第一步,在项目根目录找到.env.example,复制一份改名为.env。第二步,把DATABASE_URL改成你本地数据库的连接串,例如 SQLite 场景下可以写成file:./dev.db。第三步,执行npx prisma migrate dev,这个命令会根据schema.prisma里的模型自动创建数据表并生成 Prisma Client。
我遇到过不少朋友在这一步卡住,原因是他们直接修改 schema 之后忘了跑 migrate,导致代码里读到的表结构和数据库不一致。记住一个原则:只要改动schema.prisma,就得重新跑migrate或者至少跑prisma generate。否则 Prisma Client 的类型不会更新,数据库里的表也没有同步。
3.3 目录结构与核心文件
t3code的目录结构通常长这样,熟悉它的分层方式后,你能很快定位到每个业务模块对应的代码位置:
src/ app/ # Next.js App Router 页面与布局 api/ trpc/ # tRPC 的 HTTP 服务入口 auth/ # NextAuth 的请求处理 layout.tsx # 全局布局 page.tsx # 首页 pages/api/ # 旧版 API 路由(如果不使用 App Router) server/ api/ root.ts # tRPC 根 router routers/ # 业务子路由(如 todo.ts、user.ts) trpc.ts # tRPC 实例与 middleware db.ts # Prisma Client 实例 auth.ts # NextAuth 配置 trpc/ client.ts # 客户端 tRPC 调用封装 server.ts # 服务端 tRPC 渲染帮助函数 styles/ globals.css # 全局样式 prisma/ schema.prisma # 数据库模型定义这个结构的核心思路是:把“服务端逻辑”集中在server目录,把“客户端连接”集中在trpc目录,页面层只负责组合调用。你写新功能时,大概率流程是:先在prisma/schema.prisma加表,再在server/api/routers加 procedure,最后在app里的页面组件中调用。
4. 核心链路实战:从零实现一个完整功能
4.1 以 Todo 功能为例:定义数据和接口
为了让你直观感受完整链路,我用一个最简单的 Todo 功能演示。先从数据库模型开始,在prisma/schema.prisma里加上:
model Todo { id String @id @default(cuid()) title String done Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }保存之后执行npx prisma migrate dev --name add_todo。这条命令会创建一张Todo表,同时更新 Prisma Client 的类型。等它输出成功信息后,你就能在代码里使用prisma.todo.findMany()这类类型安全的方法。
接下来是定义 tRPC 的接口路由。在server/api/routers/todo.ts中写:
import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../trpc"; export const todoRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.todo.findMany({ orderBy: { createdAt: "desc" }, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1).max(100) })) .mutation(async ({ ctx, input }) => { return ctx.db.todo.create({ data: { title: input.title }, }); }), toggle: publicProcedure .input(z.object({ id: z.string(), done: z.boolean() })) .mutation(async ({ ctx, input }) => { return ctx.db.todo.update({ where: { id: input.id }, data: { done: input.done }, }); }), });这段代码看起来简短,但它其实已经完成了“接口定义 + 参数校验 + 数据库操作”全部三件事。publicProcedure表示当前接口无需登录即可调用,如果你希望只有登录用户能操作,把它改成protectedProcedure即可,ctx里还会带上当前用户信息。
4.2 用 tRPC + Zod 打通前后端数据链路
定义完 router 后,不要忘记把它挂载到根 router 上。打开server/api/root.ts:
import { todoRouter } from "./routers/todo"; import { createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ todo: todoRouter, }); export type AppRouter = typeof appRouter;然后把AppRouter类型导出,客户端那边利用@trpc/react-query提供的 hooks 就能拿到类型提示。接下来在 React 组件里写首页逻辑,典型代码是这样:
"use client"; import { api } from "~/trpc/client"; export default function TodoPage() { const utils = api.useUtils(); const { data: todos, isLoading } = api.todo.list.useQuery(); const createMutation = api.todo.create.useMutation({ onSuccess: () => utils.todo.list.invalidate(), }); const toggleMutation = api.todo.toggle.useMutation({ onSuccess: () => utils.todo.list.invalidate(), }); if (isLoading) return <div>加载中...</div>; return ( <div> <form onSubmit={(e) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const title = String(formData.get("title") ?? ""); createMutation.mutate({ title }); e.currentTarget.reset(); }} > <input name="title" placeholder="新增待办" /> <button type="submit">添加</button> </form> <ul> {todos?.map((todo) => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.done} onChange={() => toggleMutation.mutate({ id: todo.id, done: !todo.done })} /> <span style={{ textDecoration: todo.done ? "line-through" : undefined }}> {todo.title} </span> </label> </li> ))} </ul> </div> ); }注意,这里的api.todo.list.useQuery()直接就是类型安全的。如果你在 service 里把Todo模型的title改成了name,前端的todo.title会立刻飘红,你根本不会等到页面运行时才发现字段没了。
4.3 接入页面并进行联调
把上面的组件放进src/app/page.tsx后,启动开发服务,打开浏览器应该就能看到 Todo 列表和新增表单。整个链路从数据库到前端 UI 是完整的:前端调用 tRPC hook → 请求打到/api/trpc→ tRPC 服务端校验入参 → handler 里操作数据库 → 返回类型化数据 → React Query 缓存数据并渲染。
这个流程里我最想强调的一点是 React Query 的invalidate机制。每次 mutation 成功后,它会把相关查询标记为过期并重新获取,这样你不需要手动刷新页面或维护本地 store。如果你把工程做得更大,还能用onMutate做乐观更新,让 UI 在请求发出前先改变,体验更好。t3code默认把 React Query 接入 tRPC 客户端,你不需要额外配什么。
5. 常见问题与排查心得
5.1 开发阶段最容易踩的坑
我在多次使用t3code和 T3 Stack 的过程中,积累了一些高频问题,这里直接用表格整理出来,方便你对照排查。
| 现象 | 常见原因 | 解决思路 |
|---|---|---|
| 页面正常但数据请求一直 500 | Prisma Client 版本与 schema 不匹配 | 重新执行prisma generate |
| tRPC 类型在编辑器里报错 | 修改 router 后没有重启 Next.js | 重启npm run dev,让类型重新生成 |
| NextAuth 登录回调失败 | AUTH_SECRET未设置 | 在.env里配置足够随机的 secret |
生产环境构建报TypeError | 组件里把 async 函数直接传给客户端组件 | 数据获取放在 Server Component,通过 props 传入 |
| 表单校验没生效 | Zod schema 只校验了类型,但没有 trim | 在 schema 里使用.trim()或.transform() |
| 数据库迁移时字段冲突 | 多端操作同一 schema 文件 | 统一 migration 顺序,提交前先 pull 最新代码 |
很多坑的根本原因,其实是你跳过了脚手架自带的约束。t3code为了省心,把目录和边界都固定了,最好在熟练之前不要随意“改造流程”。
5.2 我的避坑清单与运维建议
第一,开发时尽量保持一条命令启动:npm run dev。如果你的项目里有多个服务(比如独立的数据库、缓存、文件存储),建议用concurrently或 tmux 把这些启动过程合并起来,否则每次开环境都要手动敲一堆命令,人会特别容易烦躁。
第二,环境变量这层一定要规范。.env不要提交到 Git,线上配置和本地配置分开放。t3code里通常已经给你写好.env.example,你只需要照着复制。注意,NextAuth 的NEXTAUTH_SECRET值要足够随机,建议用openssl rand -base64 32生成,不要用短字符串。
第三,不要忽视中间件。受保护接口一定要通过protectedProcedure过滤,不要因为开发期图快就直接把接口设成publicProcedure。等到上线被刷流量时才想起来补,那个成本会成倍增加。
第四,每次更新 Prisma schema 后,先跑npx prisma migrate dev,再跑npx prisma generate。前者同步数据库,后者同步类型。实际使用中很多人只跑了前者,编辑器老报错,原因就是类型没有重新生成。
最后再分享一个小技巧:t3code这类仓库里的脚手架配置其实是可以按需拆掉的。比如你的项目不需要 NextAuth,那就大胆删掉相关的 router 和 middleware;不需要 Tailwind,也可以换成纯 CSS。关键是保留整体的分层方式和 TypeScript 流转链路,那才是它真正的价值所在。根据我个人过往的经验,把 T3 Stack 的“类型安全流动”这个核心理解到位了,后面无论你换成什么框架,写全栈应用的思路都会清晰很多。