1. 为什么是 t3code一个全栈项目的选型起点大概两个月前我的个人项目里出现了一个绕不开的岔路口要做一个带用户登录、数据展示、后台管理的小型全栈应用但前后端技术栈到底怎么选让我纠结了很久。纠结的核心不是“做不出来”而是“做完之后还要不要人”我自己维护一个项目最怕的是写完当天很兴奋一周后打开代码库却因为类型割裂、接口文档过期、调试又要开两个终端而彻底失去维护的兴趣。所以当 t3code 这个项目的代号落在纸上时我的目标就很明确了不是再搭一个“能跑的 Demo”而是把一个代码库当成长期产品来设计——前端、后端、数据库、认证全部收拢在一个 TypeScript 体系里让类型从一个数据库表一路流到 React 组件的 props 上中间不掉链子。t3code 的含义其实很直白就是 T3 Stack 实践库。T3 是社区里对 TypeScript、Tailwind CSS、tRPC 这一组工具链的简称再加一个 Next.js 作为框架底座正好组成一个“从数据库到 UI 全链路类型安全”的现代全栈组合。很多开发者在微博和技术群里讨论过这套组合但真正把它完整落地成项目、跑通认证、部署上线的并不多。我这篇文章就想把 t3code 从零到上线的整个过程梳理一遍包括为什么这么选型、目录怎么分、数据模型怎么设计、tRPC 路由怎么写、部署时踩过哪些坑全部用实际代码和真实操作说话。如果你也想做一个自己的全栈项目或者正在 React / Vue 单页应用和 Next.js 全栈方案之间犹豫t3code 的复盘过程应该能给你一个比较完整的参考。我会尽量少讲抽象概念多放能直接抄走的代码和命令同时把每一步背后的理由也讲清楚免得你搭完一个项目却不知道下一个项目该怎么改。2. 目录结构与数据模型先搭骨架再写业务2.1 按模块划分代码库的实验第一次用 create-t3-app 脚手架时生成的默认目录已经帮你分好了src/pages、src/server、src/trpc这些基础区域。但一旦开始写真实业务你会发现默认目录只适合“跑通”不适合“生长”。我在 t3code 里做的第一件事就是按照业务模块重新调整目录。原则只有一条让一个功能的所有代码尽量待在同一个垂直切片里而不是按技术类型把所有 components 堆在一起、所有 api 堆在一起。这里我踩过一个挺典型的坑早期我把所有表格组件挂在src/components下所有查询函数挂在src/server/api/routers下表面上各归各位但当一个表格要修改字段时我得来回跳三个目录才能改完。后来改成按 feature 划分结构变成这样src/ features/ projects/ components/ # 项目的表格、表单、详情卡片 hooks/ # 项目相关的客户端状态与 hooks api.ts # tRPC 查询入口收敛本 feature 的所有 API 调用 members/ components/ hooks/ api.ts server/ auth.ts # NextAuth 配置 db.ts # Prisma Client 单例 api/ routers/ post.ts user.ts root.ts # 汇总所有子路由 shared/ schemas/ # zod 校验 schema服务端和客户端共用 utils/ # 格式化、权限判断等通用函数这样调整之后开发体验提升非常明显。要改 projects 模块时我只需要进src/features/projects不用在整个项目里大海捞针。同时shared/schemas里面的 zod 校验规则既能在服务端入参校验时用又能在前端表单注册时用一套规则两边走把“前后端各写一遍校验”这个经典重复劳动直接消灭了。2.2 Prisma 模型设计的三个心得数据层是 t3code 的骨架我用的是 Prisma SQLite。很多人觉得 SQLite 只适合本地开发其实对个人项目、小团队内部工具来说SQLite 的单文件部署反而省心——不用单独维护数据库服务备份就是拷贝一个文件。线上真到了并发量上来Prisma 的 provider 改成 PostgreSQL 也只需要改一行配置加一次迁移迁移成本完全可以接受。在设计数据模型时我特别注意三件事。第一所有关联字段都要建立索引尤其是一对多关系的另一侧。第二尽量用时间戳字段记录创建和更新时间但默认值必须交给数据库生成而不是在应用代码里用new Date()插入这样才能保证时区一致性。第三业务字段尽量加枚举约束比如项目状态用 PROJECT_STATUS 枚举而不是一个自由文本字段。下面是 t3code 项目里实际用到的简化模型你可以直接参考这个模式model Project { id String id default(cuid()) name String slug String unique status ProjectStatus default(ACTIVE) ownerId String owner User relation(fields: [ownerId], references: [id]) createdAt DateTime default(now()) updatedAt DateTime updatedAt index([ownerId, status]) } enum ProjectStatus { ACTIVE ARCHIVED DELETED }这里特别说明一下index([ownerId, status])这个复合索引。很多 Prisma 新手都忽略索引觉得数据量小不在乎。但项目一旦跑起来列表页最常见的查询就是where ownerId ? and status ?这种过滤没有复合索引的时候全表扫描的代价会随数据量线性增长等到几百上千条数据时就会明显卡顿。我因为这个问题重构过一次表结构所以建议你从一开始就加上。2.3 环境变量的无头坑我是吃了环境变量大亏才意识到要专门讲它。t3code 刚跑起来的时候一切正常部署之后所有登录请求全部报 500查了整整一天最后发现是AUTH_SECRET没设置到生产环境变量里面。T3 栈里把环境变量写进.env只是第一步部署平台Vercel、Docker 容器通常有自己独立的环境变量面板。我的做法是建一个.env.example放在仓库里把所有需要的环境变量列好注释清楚用途部署时照着它逐项填。同时要注意 Next.js 环境变量的前缀规则。只有以NEXT_PUBLIC_开头的变量才会被打包进浏览器端代码服务端变量像DATABASE_URL、AUTH_SECRET必须避免这个前缀否则会有泄露风险。最好再用 zod 对process.env做一次运行时校验启动时缺哪个变量就直接报错而不是等到运行到某个页面才炸。这也是 T3 脚手架默认附带的做法很多文档没解释为什么我理解下来就是让配置错误尽早暴露别把问题留到用户面前。3. 从认证到 APItRPC 路由的设计细节3.1 为什么直接选 NextAuth 而不是自己写登录登录认证是很多全栈项目的第一个分水岭。自己实现一套 session cookie 体系也不是不行但安全性细节非常多——密码哈希算法选型、token 刷新机制、CSRF 防护、会话失效清理每一条都值得单独写一篇博客来覆盖。对 t3code 这种个人项目来说我不觉得从头造轮子是明智的选择所以我直接用了 NextAuth 的 Credentials 和 GitHub OAuth 双方案。GitHub OAuth 的配置非常顺滑只需要在 GitHub OAuth Apps 页面创建应用把Client ID和Client Secret填进环境变量。Credentials 方式邮箱 密码则需要自己做好密码哈希我推荐你使用 bcrypt 或者 argon2。bcrypt 是社区里验证特别多的方案虽然性能不如 argon2 激进但兼容性和文档优势非常明显。注册接口里取到密码后先await bcrypt.hash(password, 10)再存进数据库。登录时用bcrypt.compare验证密码。这里有一个我后来才注意到的细节NextAuth 的 secret 千万不要在代码里 hardcode而且生产环境的 secret 一定不能和开发环境共用。一旦 secret 泄露攻击者可以伪造 session token后果非常严重。3.2 tRPC 路由的模块化思路tRPC 是 t3code 项目里我体验最好的部分。它本质上是一个 RPC 框架客户端直接调用服务端定义的函数整个调用过程像在操作本地函数一样而且类型自动推导不需要像 REST 那样手动维护接口类型定义。我用的是initTRPC创建一个实例然后通过createCallerFactory做成服务端调用或者createTRPCNext接在前端。路由文件按业务模块拆开每个文件导出一个createTRPCRouter对象。比如 projects 路由import { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; import { updateProjectSchema } from ~/shared/schemas/project; export const projectRouter createTRPCRouter({ list: protectedProcedure .query(async ({ ctx }) { return ctx.db.project.findMany({ where: { ownerId: ctx.session.user.id, status: ACTIVE }, orderBy: { createdAt: desc }, }); }), update: protectedProcedure .input(updateProjectSchema) .mutation(async ({ ctx, input }) { const project await ctx.db.project.findUnique({ where: { id: input.id }, }); if (!project || project.ownerId ! ctx.session.user.id) { throw new TRPCError({ code: UNAUTHORIZED, message: 您没有权限修改该项目, }); } return ctx.db.project.update({ where: { id: input.id }, data: { name: input.name, status: input.status }, }); }), });注意protectedProcedure这是我在 tRPC 上下文里加了 session 判断后产生的专用 procedure。未登录用户直接调用这些接口会立刻收到 UNAUTHORIZED 错误不用在每个路由里手动检查一遍 session。这样做的好处是安全逻辑收敛在了一个地方后续新增接口时只要选对 procedure默认就是安全的。3.3 服务端渲染与客户端调用的边界T3 栈里有个容易混淆的点tRPC 既可以服务端调用也可以客户端调用。tRPC 的createCallerFactory能在 Next.js 的 Server Component 里直接取数这样首屏数据是在服务端渲染的对 SEO 友好另一方面React Query 把客户端缓存、重新验证、乐观更新都接好了。我的原则是页面初次加载需要的数据尽量走服务端取数用户交互之后才需要的数据走客户端调用。举个例子项目列表页默认数据直接在服务端用createCallerFactory拿到再传给页面组件而用户搜索、筛选、新增项目等交互操作则走客户端trpc.project.list.useQuery()。这样兼顾了首屏速度和交互及时性也不容易出现服务端和客户端状态不同步的水合问题。tRPC 的类型推导还省掉了大量重复工作。服务端修改了一个查询返回的字段类型前端所有调用的地方立刻会出现类型错误提示开发阶段就能发现接口签名不匹配的问题。相比以前写 REST 接口要单独维护一份 OpenAPI/Swagger 文档、再手动生成客户端类型t3code 的类型完全是从代码里长出来的不需要额外劳动也不会出现文档和实现漂移。4. 部署、监控与常见坑4.1 线上部署的两种路径t3code 这类 Next.js 全栈项目部署非常方便。我个人试过两条路径都完全可行。第一条是 Vercel。把 GitHub 仓库连进去Vercel 会自动识别 Next.js 项目构建命令都不用改。唯一要做的就是在 Vercel 的项目设置里把环境变量填好然后部署即可。SQLite 数据库文件怎么处理是一个麻烦点——Vercel 的函数是无状态的写文件系统不稳定所以线上我直接换成了 PostgreSQL用 Neon 或者 Supabase 这类托管服务给一个连接字符串就行。Prisma 换库只需要改datasource db的 provider 和连接字符串再跑一次prisma migrate deploy。第二条是自建服务器 Docker Compose。t3code 线上版我现在就是跑在一台 Linux 服务器上的用 Docker 打包 Next.js 应用再用 Compose 启动一个 PostgreSQL 容器配合 Nginx 做反向代理和 HTTPS 证书。Dockerfile 我用的是多阶段构建第一阶段安装依赖并next build第二阶段只拷贝构建产物和node_modules里的生产依赖镜像体积从 1.5GB 降到了不到 300MB。4.2 我踩过的五个坑按痛苦程度排序先列一张表把 t3code 开发上线过程中最有代表性的问题整理出来后面再逐个讲排查思路。症状根因解决方式刷新后页面闪现登录状态丢失saveSession 未开启或 token cookie 同站点设置问题检查 NextAuth 配置设置session.strategy: database或确认 cookie secure 属性API 偶尔 500重启恢复SQLite 连接被并发写占用开发环境线程池调小或直接迁移 PostgreSQL字体/样式在首屏闪烁Tailwind CSS 的darkMode配置在服务端和客户端不一致确认darkMode: [class]且根组件同步挂载 class上传图片一直 413Nginx 默认请求体大小只有 1MBclient_max_body_size 10m;项目列表查询越来越慢缺少复合索引按业务筛选条件补复合索引第一个坑在开发阶段还不太明显部署到线上后用户刷新页面时前端拿到一个新的 NextAuth session 请求如果浏览器 cookie 的SameSite属性和部署域名的 HTTPS 配置不对cookie 不会被正确带上已经登录的用户也会被判定为未登录。解决方法是确保NEXTAUTH_URL设置正确且在 NextAuth 配置里把 cookie 的 secure 选项打开。第二个坑是 SQLite 在真实并发场景下的瓶颈。SQLite 适合单进程读写当 Next.js 无服务器函数以多实例并发执行时数据库文件经常被多个进程同时写入就会偶尔报database is locked。t3code 开发版本里我继续保留 SQLite好处是零配置但线上版必须换 PostgreSQL否则很容易在流量稍微上来一点时就出问题。第三个坑很隐蔽。我用 Tailwind CSS 配置暗色模式时用media策略直接跟随系统但这种策略在服务端渲染时拿不到用户偏好导致服务端认为亮色、客户端认为暗色页面挂载时就出现闪烁。换成class策略之后我在根布局组件里读取用户偏好并显式渲染darkclass服务端和客户端就完全一致了。第四个坑在自建服务器上几乎必踩。Nginx 默认的client_max_body_size是 1M用户上传一张稍大的图片就直接 413。排查方法很简单先看 Nginx 错误日志如果看到client intended to send too large body就说明是请求体大小限制。加上一行配置就行。第五个坑前面已经提到加复合索引解决。它能排在最后一个不是说不严重而是绝大多数性能问题只要建好索引查询速度都能立刻恢复算是性价比最高的修复。4.3 为什么坚持严格类型检查T3 栈给你搭好了全链路类型安全的地基但如果不坚持下去这些优势会被一点点蚕食。我在 t3code 里开的是 TypeScriptstrict: true并且把 eslint 的no-explicit-any作为 error 级别规则。刚写的时候会觉得约束太多尤其是一些第三方库类型定义不全的时候想要as any糊弄过去。但两个月跑下来感受非常明显因为不能轻易any每次遇到类型报错我都会回到数据源头去看 schema这反而帮我提前发现了三个字段命名不一致的问题——前端传createdAtPrisma 返回createTime这类 bug 如果放在任意类型的缝隙里可能要等到 UI 上数据全空白才能发现。严格类型检查还有一层好处重构时安全感拉满。我把 projects 模块的数据返回结构调整过一次服务端改了查询返回的字段前端所有用到旧字段的地方全部在编辑器里标红我照着红圈改了十几分钟就完成了整个重构不用手动搜索“哪里可能用到了”。这种体验一旦适应就再也回不到纯 JavaScript 时代了。5. 把 t3code 跑通之后我又优化了什么5.1 查询性能优化分页与缓存项目列表数据量增长后全量查询迟早会卡。我建议所有列表类接口都做游标分页而不是传统的 offset 分页。为什么用游标因为 offset 翻页到深页时数据库还要扫描前面所有被跳过的行性能会逐步下降游标分页用where id cursor的方式取下一页每条记录只被扫描一次数据量再大也相对平稳。t3code 里我封装了一个通用的分页逻辑核心就是用一个cursor参数记录上次返回的最后一条记录的id配合take限制条数。前端 React Query 会自动把游标状态缓存下来按“加载更多”按钮时把上次游标传给后端。具体代码比较长这里给一个简化的核心思路const list await ctx.db.project.findMany({ where: { ownerId: session.user.id }, take: 20, ...(cursor ? { skip: 1, cursor: { id: cursor } } : {}), orderBy: { createdAt: desc }, });注意skip: 1因为游标记录本身也满足查询条件要跳过它。这个细节我一开始漏了结果每页加载都出现第一条数据重复。缓存方面React Query 默认有 staleTime 机制。查询一个不常变的列表时把staleTime调到 30 秒甚至 1 分钟可以显著减少后端请求。t3code 的详情页我把staleTime设为 5 分钟配合服务端取数页面几乎秒开用户体验好了很多。5.2 权限管理从“能登录”到“能分角色”原型阶段只要有人登录就行但真实使用一周后权限不足的问题就浮出来了。我加了简单的角色系统User 模型里加role枚举字段取值为 ADMIN / USER。然后在 tRPC 层做授权分离protectedProcedure只保证已登录adminProcedure在它基础上再判断角色非管理员直接抛出 FORBIDDEN。在 Server Action 和 API 层都复用这套逻辑避免前端按钮隐藏但后端接口裸奔的情况。角色权限设计不用一开始就上 RBAC 那套大的模型对于个人项目或小团队一个枚举字段加几个专用 procedure 就够用了。等真的出现多角色、多资源、细粒度操作权限需求再考虑引入casl/ability之类的权限库也不迟。过早抽象反而会让代码难以理解。服务端数据校验同样重要。t3code 里的所有 mutation 入参都走 zod schema表单数据在后端接口入口再完整校验一遍。不是为了和前端过不去而是防止绕过前端直接调 API 传入脏数据。type-safe 的 API 层让你容易忽略这件事——总觉得类型都对但类型只保证格式不保证业务合法性比如字符串长度、枚举范围、时间先后关系都还要靠 zod 兜底。5.3 测试与监控给项目上保险t3code 早期完全没有测试直到一次改数据库 schema 时不小心把某个字段名拼错了页面静默出错我才意识到测试的重要性。加了 vitest 和 React Testing Library覆盖核心路由和几个关键组件。写测试这事不需要一步到位。我先给最核心的三个数据流写了测试认证逻辑、项目 CRUD 路由、权限校验。这三个部分一旦出问题整个应用都没法用属于“优先级最高”的测试。UI 组件的测试则是后面有空再逐渐补毕竟组件变动频率高写得太细反而会频繁返工。服务器侧我用的是 Sentry 做错误监控前端Sentry.init后端通过 Next.js 的 instrumentation 上报 unhandled error。上线后的第一周就抓到两个线上 bug一个是在某个特定操作下session.user.id为空的空指针另一个是第三方服务超时导致的请求挂起。没有监控的情况下这些错误大概率是用户来骂了才知道现在则是报警先来修复完用户还没察觉。6. 如果重做一次 t3code我会怎么做这篇文章写到这基本把 t3code 从选型到上线、再到优化加固的完整过程都过了一遍。如果要给一个总结性的回答我不会说技术选择有多么先进反而会想说一个更朴素的体会全栈项目最容易失控的地方不是某一项技术你不会而是前后端之间的类型、校验、权限、状态同步这些问题总是在缝隙里捣乱。t3code 用的这一套工具组合本质上就是把这些缝隙用 TypeScript 填平让开发者的精力主要花在真正的业务逻辑上。如果现在从头再做一个类似项目我也会毫不犹豫地再走 T3 这条路但仍然有一些调整。第一数据库会直接上 PostgreSQL而不再从 SQLite 迁移省掉一次换库折腾第二一开始就要建索引和加测试而不是等问题浮出水面再补第三权限模型先用最简单的方式跑通但接口安全校验永远不放低标准。这些经验看起来都很简单但都是我在这段真实项目过程中一点点踩出来的。写这篇复盘最想表达的一件事其实是项目代号叫什么并不重要重要的是它是否能够长期健康地被维护。t3code 这个代码库现在还在持续更新日常记录、数据导入、权限调整都在里面进行。它不庞大不复杂但它足够清晰让我任何时候打开都能快速定位问题修改起来也不怕打碎其他部分。如果你也在纠结全栈项目怎么搭不妨按我上面的流程试一次把类型安全和模块划分做扎实。相信我两个月后你会感谢当时认真铺路的自己。
阅读完成 · 觉得有帮助?