t3code搭建指南:基于T3 Stack的全栈类型安全脚手架实践

发布时间:2026/10/26 23:29:00
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 Errorschema 里模型或字段名和迁移记录不一致检查字段名,删除重复迁移后重新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 给了你一个整洁的启动姿势,剩下的长期维护,还得靠这些不起眼的习惯撑着。