t3code搭建指南:基于T3 Stack的全栈类型安全脚手架实践
前段时间我把一个小项目起了个代号叫t3code,起因特别简单:我要给团队的内部小工具做个全栈应用,不想再像以前那样前后端分流、接口文档还要单独维护。折腾了两三天之后,我发现 T3 Stack 这套组合正好能把整个研发链路压缩到非常舒服的状态,于是干脆把整套脚手架、规范、目录结构以及部署配置统一收拢成 t3code 这个项目代号,后面所有内部小应用都从它这里开新的分支。这篇文章就把我搭 t3code 的完整过程做个梳理,把为什么选这套栈、每一步怎么落地、哪些地方容易踩坑全部摊开讲一遍,适合刚接触全栈开发、想用一个更紧凑的方式组织前后端代码的同学参考。
1. 先说清楚背景:t3code 这个代号是怎么来的
1.1 为什么我不用 create-next-app 直接开项目
很多人问我,既然核心是 Next.js,为什么不直接create-next-app一把梭。create-next-app 本身很好,但它只会给你一个 Next.js 的空壳:一个 App Router 目录、一套样式入口、一些基础配置。真正写业务的时候,你会发现还要自行决定很多东西:数据库 ORM 用 Prisma、Drizzle 还是直接 SQL,接口层用 REST、GraphQL 还是别的,表单校验用什么库,登录认证怎么接。这些决策单独看都不难,但落在实际项目里,就是一个又一个的上下文切换成本。
t3code 的意义在于把这些决策先固化下来。它不是一个发明新框架的项目,而是把当前生态里配合最舒服的几个工具,按照统一规则绑到一起。你拿到的不是空目录,而是已经打通了"数据库 → 服务端 API → 前端调用 → 页面渲染 → 部署配置"这整条链路的基础工程。这样团队里新来的人也好,自己临时写个原型也好,都不需要从零思考架构问题,直接在业务上堆代码就行。
1.2 这个脚手架想解决什么痛点
我早期写全栈应用,最常见的问题是前后端类型不同步。后端定义了一个接口返回用户信息,前端手写一个类型,字段名一个用createdAt,一个拼成create_time,联调的时候才发现对不上。t3code 在选型阶段就瞄准了这个痛点,核心思路是让 TypeScript 的类型系统贯通整个应用。后端路由里定义好的输入输出类型,前端调用时能直接推导出来,字段名改一处,编译器立刻告诉你哪里需要跟着改。这种体验一旦习惯了,就很难退回手写接口文档的方式。
t3code 还解决了另一个问题:重复劳动。每个项目都要做的东西,包括数据库连接、session 管理、错误处理、统一响应格式,全部在模板层做掉。我的目标很明确:拿到 t3code 之后,一个小时后就要能开始写业务表结构,而不是花一整天配环境。
2. 技术选型拆解:T3 Stack 每一层都在干什么
2.1 四件套的真实分工
T3 Stack 的核心成员一般是 Next.js、TypeScript、Tailwind CSS、tRPC,再加上 Prisma、NextAuth、zod 这些外围伴侣。很多人第一次看会觉得工具太多,其实它们的分工非常清晰:
Next.js 负责页面框架和渲染层。页面既可以写服务端组件也可以写客户端组件,API 路由也挂在同一个进程里,省掉单独起服务端的动作。TypeScript 负责全链路类型约束;Tailwind 负责样式,不做过多设计系统的约定,写起来快。tRPC 是连接前后端的关键,它让服务端的 router 定义能被前端直接引用,从而获得像本地函数调用一样的类型安全体验。
数据库和校验部分我选了 Prisma 和 zod。Prisma 把表结构定义在 schema 文件里,然后自动生成客户端代码;zod 则负责在进数据库之前做一层数据校验,保证不该进库的数据进不去。NextAuth 拿来处理登录认证,覆盖了常见的 OAuth 和邮箱登录场景。
2.2 端到端类型安全是怎么串起来的
很多朋友觉得"类型安全"是一个比较虚的概念,我直接用 t3code 里的实际流程解释一遍。服务端的某个 router 接收一个参数{ title: string, content: string },这个输入校验规则用 zod 定义。前端需要调用这个接口时,tRPC 会自动生成对应的 hooks,你在页面上写api.post.create.useMutation(),参数类型就是从服务端推导过来的。
这意味着什么?如果服务端把content改成body,前端编辑器里所有用到content的地方都会跳出红色波浪线,你不可能等到运行时报错才发现。同样,后端返回的数据结构变了,前端读取字段的位置立刻会类型检查失败。对我这种经常同时改前后端代码的人来说,这种约束极大减少了联调环节的摩擦。
为了让大家更直观理解,我做了一张小对比表:
| 对比维度 | 传统 REST + 手写类型 | tRPC + TypeScript |
|---|---|---|
| 接口定义方式 | 写路由+文档 | 定义 router + zod |
| 前端类型来源 | 手写或代码生成 | 自动推导,无需额外生成步骤 |
| 字段重命名影响 | 容易漏改、报错滞后 | 编译期直接暴露 |
| 接口请求调试 | 需要 Postman 或浏览器控制台 | 方法调用即请求,自带类型提示 |
| 学习成本 | 低 | 中等,但收益明显 |
2.3 跟传统 REST 方案摆在一起看差距
不是说 REST 不好,而是对于"前后端一起维护的小团队"来说,tRPC 的抽象层次更顺手。REST 的精髓是资源语义,如果做开放 API 或第三方对接,REST 永远是稳妥的选择。但 t3code 定位在内部工具、小而美的应用、MVP 原型这类场景,这类项目里更看重的是改代码的速度和端到端类型联动。
GraphQL 也很强,但 GraphQL 需要引入 schema 语言、resolver、客户端缓存策略,对一个小工具来说有点杀鸡用牛刀。tRPC 没有复杂的查询语言,本质就是一个远程函数调用,配合 React Query 管理缓存和 loading 状态,体验非常清爽。我把这个对比讲给团队里同事听,大家很快就达成了一致:做内部系统,选 tRPC,做开放平台,再考虑 REST。
3. 实操落地:从空目录到跑通第一个 tRPC 接口
3.1 初始化与目录规划
t3code 不是让你手写一套,官方提供了一个脚手架create-t3-app,一条命令就能把基础工程拉起来。我建议在初始化时就选清楚需要的模块:
pnpm create t3-app@latest t3code交互式选项里,我选了 Next.js、TypeScript、Tailwind、tRPC、Prisma、NextAuth、zod,数据库暂时选了 SQLite,为的是本地开发零配置。初始化完成后的目录结构非常清晰,我保留了以下几个骨干位置:
t3code/ src/ pages/ # 页面路由 server/ api/ # tRPC router 的聚合位置 auth.ts # NextAuth 配置 db.ts # Prisma 客户端单例 styles/ # 全局样式 prisma/ schema.prisma # 数据模型定义 .env # 环境变量它默认已经用 App Router 或者 Pages Router 取决于版本,t3code 迭代得比较快,新版本初期会问你要哪种,我直接用默认项。目录一旦固化,后面新增模块就是"照着旧模块复制一份再改业务"的流程。
3.2 数据模型定义与数据库准备
我选 SQLite 起步,主要是为了本地好跑。prisma/schema.prisma里的内容就两块:数据源和模型。举个例子,我做一个简单的代码片段管理功能时,建立了一张Post表,它其实用来存"代码片段":
model Post { id Int @id @default(autoincrement()) title String content String language String @default("text") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }写完 schema 之后,接下来两步是生成迁移和更新客户端:
pnpm prisma migrate dev --name init pnpm prisma generate第一次接触 Prisma 的同学要注意,migrate dev只适合开发环境,它会直接在你本地库上执行迁移脚本。团队协作时,建议把这个命令生成的migrations目录提交到仓库,其他人拉下来后跑prisma migrate deploy即可。
3.3 服务端路由:定义第一个 query 与 mutation
t3code 的服务端入口集中在src/server/api/root.ts,每个业务模块一个 router。比如我想给片段管理增加创建和查询功能,就建立一个postRouter:
import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; export const postRouter = createTRPCRouter({ create: publicProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), language: z.string().default("text"), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.post.create({ data: { title: input.title, content: input.content, language: input.language, }, }); }), list: publicProcedure.query(async ({ ctx }) => { return ctx.db.post.findMany({ orderBy: { createdAt: "desc" }, }); }), });这里有一个很关键的细节:publicProcedure表示未登录也能调用,如果业务需要登录态,就换成protectedProcedure。t3code 模板里默认帮你写好了 protectedProcedure 和 session 关联逻辑,实际业务里 90% 的增删改查都应该是受保护的。
这种写法的好处是,一个路由里既有参数校验又有业务逻辑,从输入到输出都是类型明确的。你没写错参数名,编译器不会放过你。
3.4 前端页面接入 tRPC 调用链路
服务端定义好之后,前端调用非常简单。以列表页面为例:
import { api } from "~/utils/api"; export default function HomePage() { const { data, isLoading } = api.post.list.useQuery(); if (isLoading) return <p>加载中...</p>; return ( <ul> {data?.map((post) => ( <li key={post.id}>{post.title} / {post.language}</li> ))} </ul> ); }创建一个新片段的时候,用useMutation加缓存失效:
const utils = api.useUtils(); const createPost = api.post.create.useMutation({ onSuccess: async () => { await utils.post.list.invalidate(); }, });invalidate是关键操作,它让 React Query 知道列表数据已经过期,下次渲染或用户回到页面时自动重新拉取。很多新人卡在这一步,以为调用了 mutation 页面就会刷新,实际必须处理缓存失效。理解了这一层,t3code 的前后端联动就算真正入门了。
4. 必须说清楚的收尾细节:认证、部署、优化
4.1 登录态是怎么接上的
t3code 里认证走的是 NextAuth,默认配置在src/server/auth.ts里。我以 GitHub OAuth 为例,先在.env里配好密钥:
GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=xxx NEXTAUTH_SECRET=随便一个足够长的随机串 NEXTAUTH_URL=http://localhost:3000前端登录按钮接signIn("github"),页面里用useSession判断当前用户。接 session 之后,前面说的 router 可以直接从 ctx 里拿到session.user,比如把 Post 表和创建人关联起来,这样每个人只能看到自己的片段。NextAuth 这套在 t3code 里整合得很深,只要按模板配置好,你不需要手写 JWT 的签发与解析逻辑。
提示:NEXTAUTH_SECRET 一定不能用弱密码,我习惯用
openssl rand -base64 32生成。上线后这个值要换掉开发环境的默认值,否则部署环境里的会话无法验证。
4.2 部署到服务器时容易踩的坑
t3code 部署我首选 Vercel,因为 Next.js 是它的原生公民。项目设置里填好环境变量,选择迁移命令,推仓库就能自动构建。但如果你要把应用部署到自己的 Linux 服务器上,注意几个点。
第一,prisma generate必须出现在构建流程里。很多服务器构建失败的原因就是装完依赖之后没有生成最新的 Prisma Client。在构建命令里显式加上:
prisma generate && next build第二,环境变量在构建时就得齐全。Next.js 在构建阶段会读取NEXTAUTH_URL、DATABASE_URL这些值,很多页面首屏渲染又依赖 session,如果变量缺失,构建出来的是残缺页面。第三,数据库如果是远程的,确认服务器能访问对应端口,本地调试时能用 localhost,部署后没改地址就特别容易出现连接超时。
4.3 构建产物与首屏体验优化
t3code 默认配置已经比较克制,但实际业务做大了之后还是要做几件事。一个是我会定期检查页面是否引入了过多的客户端组件。能放到服务端组件里做的查询,就不要在客户端组件里做,这样首屏 HTML 里直接就带有数据,不会出现白屏闪烁。tRPC 的serverClient可以在服务端预取数据,这个模板里给了相关示例,用起来很方便。
第二个是静态化与动态渲染的取舍。列表页如果对实时性要求不高,可以用generateStaticParams做成静态路径,配合增量再验证,效果非常好。内部管理系统这类实时性要求高的页面,则保持动态渲染。t3code 让你以页面为单位控制这个粒度,不需要全站统一。
第三个是图片和静态资源的处理。Next.js 自带 Image 组件,但内部工具场景里如果只是普通 logo 和小图标,直接用 svg 内联可能更省事。我在 t3code 的通用组件里放了一套图标集合,避免每个页面都单独请求一堆小图片,减少构建体积也减少请求数。
5. 问题排查速查表与实际工作体会
5.1 我遇到过的几个典型报错
t3code 最常报怪异的错误往往是类型链路上的小问题。我这里挑五个最有代表性的:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
前端调用api.xxx.yyy.useQuery()时类型不存在 | 服务端 router 没有注册到 root router | 在root.ts里追加对应 router |
Prisma 报Schema Parsing Error | schema 里模型或字段名和迁移记录不一致 | 检查字段名,删除重复迁移后重新migrate dev |
| NextAuth 登录后一直回跳登录页 | NEXTAUTH_URL 或回调地址配置不对 | 核对.env中的 URL,确认没有带多余斜杠 |
| 构建时内存不够 | Prisma 和 Next 同时编译占内存 | 降低并发,或换大内存实例构建 |
| tRPC 返回 500 INTERNAL_SERVER_ERROR | 服务端逻辑抛异常,比如数据库写入失败 | 看服务端日志,通常直接输出具体错误原因 |
排查 tRPC 的问题有个小技巧:把前端页面里的Network面板打开,直接看请求路径和响应体。tRPC 的路径命名基本和 router 嵌套保持一致,看到post.create这类路径,心里就能大概定位是哪一层出问题。
5.2 t3code 适合什么场景,不适合什么场景
我接下来想聊点项目外的判断。t3code 这套模板适合的场景非常明显:内部工具、管理后台、个人作品集、MVP 验证、或者团队内部需要快速迭代的业务系统。这些场景看重开发效率,类型安全能帮你少出低级错误,而且维护期也不长,不需要非常复杂的拆分。
如果你要做的是大型的开放平台、提供公开 API 给第三方、有复杂的权限体系、需要绝大多数平台都适用的标准接口,那 tRPC 并不合适,REST 或 GraphQL 会更有生态兼容性。t3code 不是万能药,它是一把非常顺手的厨刀,你在厨房切菜很好用,但不能指望它代替挖掘机。
另外还要诚实地说,如果团队完全没接触过 TypeScript,上来就用 t3code 会有学习曲线。我的建议是先确保每个人掌握基础的 TypeScript 类型语法和 zod 的基本用法,这些学起来并不难,但确实是整个体系的地基。
5.3 给后来者的三条实操建议
第一条,一定要熟读模板里的trpc.ts和root.ts,理解 procedure 的创建过程和 router 注册机制。这两处是人最容易改坏的地方,也是整个类型联动的核心。
第二条,别急着加太多自定义封装。t3code 已经给你处理了大量边界问题,新人在不熟悉的情况下再包一层自己的 helper,反而容易掩盖错误。先按模板的原生方式写业务,等踩过几个真实的坑之后,再考虑抽象公共逻辑。
第三条,把环境变量和密钥的规范早早在团队里定下来。我见过不少项目因为.env文件被乱传或者密钥提交到仓库,导致线上服务被扫到。t3code 模板里自带.env.example,建议要求团队所有新成员都先复制.env.example为自己本地的.env,不允许直接把本地配置提交上去。
我个人在实际折腾 t3code 后的体会是,真正省时间的不是某一个具体库,而是整套工具之间那种"不需要我反复切换心智模型"的顺畅感。写业务时,我只需要关心数据模型、路由逻辑和页面布局,剩下的验证、缓存、类型推导都在背后帮我处理。这种状态才是我理想中的全栈开发体验。
最后再分享一个我一直在用的小习惯:每次基于 t3code 新开业务项目时,先花十分钟写一页 README,把项目用途、核心模型、部署地址和常用命令写清楚。很多小项目做到后面烂尾,不是代码难写,而是接手的人不知道从哪开始读。t3code 给了你一个整洁的启动姿势,剩下的长期维护,还得靠这些不起眼的习惯撑着。