T3 Stack全栈实战:构建个人代码片段管理工具t3code
我做了一个叫 t3code 的小项目名字听起来像个工具代号实际上它是我给自己写的“代码片段收纳盒”。起因特别朴素日常开发里有太多零碎代码——一段自己验证过的正则、一个踩了两小时坑才配好的 CI 流程、一句总是记不住的高级 CSS 写法——它们散落在微信收藏、本地记事本、浏览器书签和语雀文档里真正想找的时候永远翻不到。与其痛恨自己记忆力不如动手做点东西。我用前端圈里口碑很好的 T3 StackTailwindCSS TypeScript tRPC Next.js搭了一个全栈应用让它能把代码片段存下来、打上标签、全文搜到、一键复制还能登录后同步云端。这篇文章就围绕 t3code 的完整落地过程展开包括我为什么这么设计技术方案、数据模型怎么建模、每一步怎么操作、以及我在实际开发中踩过的一堆坑。适合正在学 Next.js 全栈、好奇 tRPC 到底怎么用、或者想过给自己做个高效率内部工具的朋友参考。1. 项目初衷先从“为什么做”开始1.1 这个项目到底解决什么问题做工具之前先想清楚痛点否则很容易做出一个自我感动型产品。我的痛点非常具体开发者的知识碎片太重、太散、太长尾。正经的知识库大家都会写但日常里真正高频复用的是小块内容一段 fetch 封装、一个 transform 函数、一行 docker-compose 命令、某个 API 的关键参数。这些内容专门写一篇文章太浪费不写又觉得可惜。放在收藏夹里没过多久就变成管不管都没区别的数字垃圾。t3code 的定位就是解决这类碎片内容的管理问题。它不是一个面向大众的代码托管平台而是一个“收藏即用”的个人工具看到好用的代码粘贴进来打两个标签之后搜关键词就能秒找到。你还可以给每条 snippet 标记隐私状态公开的内容相当于自己的微型开源仓库私有的则完全是自留地。这个定位决定了后续所有设计都不会跑偏。我不要做复杂的协作编辑器不要做评论系统不要做社交关系链。主流程就只有四个字存、搜、取、走。想明白这个边界开发效率高很多。1.2 名字里藏着技术选型的线索t3code 里的 t3 不是“第三版”的意思它来自社区里流传的 T3 Stack。这套技术组合虽然名字带 T3但本质就是一句话用 TypeScript 把前端、后端、数据库、接口定义全部串起来让类型安全覆盖整个应用。具体到 t3code我用的是这套组合里的核心成员Next.js 负责页面和 API 路由tRPC 负责前端函数调后端Prisma 负责数据库操作TailwindCSS 负责样式NextAuth 负责登录。选这套技术的原因很现实项目只有我一个开发者规模不大但要长期维护最怕的就是前后端接口约定对不上、字段改名导致到处报错、建表之后发现模型设计不合理。T3 Stack 恰好把这些最容易出错的环节用“类型强制”的方式在编译期就堵住了。名字定下来技术方向也就定了。后面所有设计都要围绕这套组合展开不再摇摆。2. 整体设计与技术路线拆解2.1 用 TypeScript 把前后端“焊死”全栈项目最容易翻车的地方不是功能复杂度而是前后端对接口的理解不一致。前端写了一个fetch(/api/snippet/create)后端也写得挺开心但传参叫tagIds前端传的是tagList运行时一点报错都没有数据却悄悄丢了。这种问题在传统前后端分离的项目里排查起来异常痛苦。t3code 的做法是让 TypeScript 覆盖整条链路。在 tRPC 路由中定义的输入输出类型前端调用时会自动获得类型推断。我在编辑器里改一个字段名所有引用位置立刻标红就好像前后端之间焊了一条钢筋谁也别想脱离型别独自漂移。实际操作下来这种体验带来的效率提升非常明显。开发过程不再需要反复切到浏览器控制台看undefined报错写代码时内心是踏实的。付出的代价是要写一些类型定义、学习泛型用法但对于一个以“长期维护”为前提的项目来说这成本值。2.2 tRPC 替代 REST少写 30% 的胶水代码刚开始规划接口时我习惯性地想要不要定义 REST 路由GET /snippets、POST /snippets、DELETE /snippets/:id……这一套自然能跑但会多出一堆重复的事文档、入参校验、客户端封装、类型手工同步。后来我改成 tRPC等于把服务端方法直接暴露给前端。前端调用看起来就像调用本地函数const snippet trpc.snippet.create.useMutation(); snippet.mutate({ title: debounce 函数, content: ..., tags: [js] });后端定义对应的路由方法前端会自动拿到参数类型和返回值类型不匹配直接编译器报错。这就是 tRPC 最核心的价值不需要写接口文档类型定义就是接口契约。tRPC 也有短板它的生态比 REST 小社区资料少某些高级场景需要自己封装。但 t3code 的核心功能就是 CRUD没有流式传输、文件上传、第三方调用等复杂需求tRPC 的短板完全不影响使用。对一个自用工具来说开发效率才是第一位的。2.3 数据模型从 snippet 到 tag 的关联关系数据库设计是项目能不能长期演化的关键。t3code 的业务模型比较简单但我还是没偷懒把关联关系理清楚之后才动手。核心表有四张表名字段要点作用Userid, name, email, image用户信息与 NextAuth 联动Snippetid, title, content, description, isPublic, language, createdAt, updatedAt, authorId代码片段实体Tagid, name, slug标签实体SnippetTagsnippetId, tagIdsnippet 与 tag 的多对多关联表Snippet 和 Tag 是多对多关系MPV 阶段就留好中间表比以后加字段容易得多。isPublic这个字段也很重要它让每一条代码都可以处在“公开分享”和“私有保存”之间自由切换而不是把功能割裂成两个模块。Prisma 的 schema 定义长这样model User { id String id default(cuid()) name String? email String? unique image String? snippets Snippet[] snippetTags SnippetTag[] } model Snippet { id String id default(cuid()) title String description String? content String language String default(text) isPublic Boolean default(false) author User relation(fields: [authorId], references: [id]) authorId String tags SnippetTag[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Tag { id String id default(cuid()) name String unique slug String unique snippets SnippetTag[] } model SnippetTag { snippet Snippet relation(fields: [snippetId], references: [id], onDelete: Cascade) tag Tag relation(fields: [tagId], references: [id], onDelete: Cascade) snippetId String tagId String id([snippetId, tagId]) }有几个细节值得展开。cuid()作为默认 ID 生成器能避免自增 ID 暴露数据量在分布式部署时也不容易冲突。onDelete: Cascade意味着删除 snippet 时自动删除它关联的标签关系避免留下孤儿数据。Tag 表上的slug字段用于 URL 友好展示比如/tag/javascript比/tag/编号直观得多。2.4 交互设计不是所有项目都需要复杂 UI很多人容易陷入一个误区做前端项目就一定要把所有动画、组件、UI 库都用上。t3code 没有走这条路。玩法很直接顶部是一个搜索框输入关键词立刻过滤列表看到想要的 snippet 点击就能复制。整个界面用 TailwindCSS 写暗色主题信息密度高看起来有点像开源社区的 snippet 管理工具。为什么不做下拉框、画布布局、拖拽排序原因还是回到项目定位这是一个给开发者用的效率工具而不是展示型产品。开发者的痛点永远是“找东西快不快”而不是“页面好不好看”。在实际开发中我用 Tailwind 写 class 的效率远高于手写 CSS可以马上调出一个干净、可用的界面。状态管理也是零依赖搜索结果用 React 的useState就够了没有引入 Redux 这类重库。交互层面的核心设计是键盘优先。在搜索框输入时按回车直接复制当前选中项按/可以聚焦搜索框。这对高频使用者非常友好真正的“拿走即走”。3. 从零搭到跑起来t3code 实操全流程3.1 脚手架初始化与环境准备T3 Stack 官方提供了脚手架工具create-t3-app一条命令就能生成包含全套配置的项目骨架。我在本地跑的就是这个npx create-t3-applatest t3code命令执行后会有一系列交互选项。我的选择是TypeScript 开启Next.js 开启tRPC 开启Prisma 开启NextAuth 开启TailwindCSS 开启不开启的内容包括 ESLint 之外的一些扩展插件尽量保持骨架精简。脚手架的版本会自动拉到当前最新的稳定版所以不同时间生成的项目结构可能略有差异不必太担心核心目录src/server/api/routers、src/server/db、src/trpc等逻辑是完全一致的。环境层面有两个前置要求Node.js 版本建议 18 以上包管理器我用的是 npm也可以换成 pnpm命令上略有差异而已。另外本地必须准备好数据库环境我用的是 SQLite 的数据库文件会在下一步通过环境变量指定路径。3.2 数据库建模与迁移脚手架会把 Prisma 默认连接到 SQLite但生产环境我更建议换 PostgreSQL。本地开发阶段先让 SQLite 把项目跑通既快又没心智负担。改造步骤如下。首先在.env里设置数据库连接DATABASE_URLfile:./db.sqlite然后按前面给出的 schema 在prisma/schema.prisma里完成建模。写完执行迁移命令npx prisma migrate dev --name init这个命令会做三件事生成新的迁移文件、在数据库里创建对应的表、生成最新的客户端PrismaClient。迁移完成后还需要一个种子脚本方便我随时用假数据测试npx prisma db seed种子脚本我用了一个最朴素的方式在prisma/seed.ts里直接创建几个用户、几条 snippet、几个 tag。每次 reset 数据库后执行一遍开发时就能立刻看到效果。这一步看似繁琐但在早期迭代里帮我省了大量手工造数据的时间。3.3 认证接入GitHub OAuth 配置由于 t3code 要考虑多设备同步登录验证就不能省。T3 Stack 默认集成了 NextAuth我直接接的是 GitHub OAuth因为作为开发者大家基本都有 GitHub 账号不需要再额外注册。配置流程并不复杂分三步走。第一步去 GitHub 的 Settings - Developer settings - OAuth Apps 里新建一个应用回调地址填http://localhost:3000/api/auth/callback/github。第二步把获得的GITHUB_ID和GITHUB_SECRET写入.env同时生成一个 NextAuth 专用的密钥openssl rand -base64 32将输出粘贴到NEXTAUTH_SECRET一栏。生产环境还需要把NEXTAUTH_URL设为线上域名。第三步在 NextAuth 配置里加上 GitHub Providerimport GitHubProvider from next-auth/providers/github; export const authOptions: NextAuthOptions { providers: [ GitHubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ], callbacks: { async session({ session, user }) { if (session.user) { session.user.id user.id; } return session; }, }, };这里我特意加了一段自定义 callback因为 NextAuth 默认的Session类型不包含用户 ID但后端筛选 snippet 时必须知道“当前登录的人是谁”。不扩展类型的话tRPC 路由里就只能拿到 email拿不到数据库主键后续很可能出现权限判断失败或者关联错误。3.4 实现核心的 CRUD 流程基础搭好之后开始写核心业务逻辑。t3code 的接口都放在src/server/api/routers/snippet.ts里路由结构非常清晰。创建 snippet 的 tRPC 路由长这样import { z } from zod; import { createTRPCRouter, protectedProcedure } from ../trpc; export const snippetRouter createTRPCRouter({ create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), description: z.string().optional(), language: z.string().default(text), isPublic: z.boolean().default(false), tags: z.array(z.string()).default([]), }) ) .mutation(async ({ ctx, input }) { const snippet await ctx.db.snippet.create({ data: { title: input.title, content: input.content, description: input.description, language: input.language, isPublic: input.isPublic, authorId: ctx.session.user.id, }, }); for (const tagName of input.tags) { const slug tagName.toLowerCase().replace(/\s/g, -); const tag await ctx.db.tag.upsert({ where: { slug }, update: {}, create: { name: tagName, slug }, }); await ctx.db.snippetTag.create({ data: { snippetId: snippet.id, tagId: tag.id }, }); } return snippet; }), });细节为关注点。protectedProcedure保证了未登录用户无法创建数据这是 tRPC 里内置权限机制的应用。入参校验交给 zod前端传什么、后端接什么都被约束得死死的。前端调用同样很简单const createSnippet trpc.snippet.create.useMutation({ onSuccess: () { // 刷新列表或收起弹窗 }, }); createSnippet.mutate({ title: 防抖函数, content: function debounce(fn, delay) { ... }, tags: [javascript, utils], });列表查询因为带了标签筛选稍微复杂一点但思路一致。核心是利用 Prisma 的where加上AND和some实现多标签交集查询。删除操作更简单一个delete加权限判断就完事中间表关联因为设置了onDelete: Cascade会自动清理。CRUD 写完之后整个应用主流程已经通了登录的开发者可以新增代码片段打标签搜索复制删除。功能不多但每个环节都稳。3.5 部署从本地到 Vercel 的收尾本地跑通不等于项目完成部署到线上才能真正多用起来。t3code 部署选 Vercel因为它和 Next.js 是天然一对零配置就能处理构建和路由。部署前需要做几件准备。第一把 SQLite 换成 PostgreSQL。Vercel 的无服务器函数不提供可写文件系统SQLite 文件在线上无法持久化。我直接用了商用的 PostgreSQL 云数据库创建实例后把连接字符串填入环境变量。DATABASE_URLpostgresql://用户名:密码主机:端口/数据库名第二执行线上数据库迁移。Vercel 的构建环境执行的是生产构建不会自动跑到 migrate dev因此在本地先执行npx prisma migrate deploy指向的是DATABASE_URL对应的线上实例。这样表结构才会在云数据库里创建好。第三把GITHUB_ID、GITHUB_SECRET、NEXTAUTH_SECRET、NEXTAUTH_URL全部填到 Vercel 项目的环境变量面板中。GitHub OAuth 应用的回调地址也要改成线上域名。这些都做完后推送代码到仓库在 Vercel 里导入项目点 Deploy就完事了。构建时长大概两三分钟部署完成后打开线上域名一个可用的 t3code 就在云端跑起来了。4. 踩坑记录常见问题与排查思路实录4.1 tRPC 类型不匹配导致的“隐形错误”t3code 开发中最让我头疼的一类问题是 tRPC 类型有时会变得不“新鲜”。改了后端路由的入参结构后前端依然能通过编译运行时却报校验失败。原因是 Next.js 开发服务器会缓存已经打包过的类型声明特别是热更新叠加 tRPC 泛型类型时容易出现延迟。排查思路很简单强制重新生成 tRPC 类型。在tsconfig.json的paths配置中tRPC 引用的是通过命令行生成的类型文件重启开发服务器或者执行一次 Next.js 的 clean 操作就能解决。rm -rf .next npm run dev我后来养成的习惯是大改 tRPC 路由结构之后不靠热更新直接清一次缓存再启动避免类型状态不一致的困惑。另一个相关问题是 zod 校验报错定位困难。tRPC 会把 zod 的校验错误包装成统一格式前端onError里拿到的不是原始字符串而是数组格式的错误信息。调试时需要多打印一层try { const result await res.json(); } catch (e) { console.log(e.message); }不要把错误信息直接丢进页面项目里所有 tRPC 错误都统一输出到页面顶部提示条看着直观排查速度也快。4.2 NextAuth 与 Prisma 的 session 坑NextAuth 默认的session回调里能拿到用户但用户对象里只有email、name、image没有数据库里的id。我在 t3code 里写删除 snippet 接口时一开始用ctx.session.user.email去查作者 ID本地测试一切正常部署到线上后发现部分历史数据查不到用户问题出在 user 表主键是id而不是email。这种问题最隐蔽运行时不报错但数据就是“安静地错”。解决方案就是 3.3 里写的自定义sessioncallback把user.id显式注入到session.user。如果发现类型仍扩展不到位还需要检查types/next-auth的模块声明直接在类型声明文件里重写Session接口declare module next-auth { interface Session { user: { id: string; name?: string | null; email?: string | null; image?: string | null; }; } }扩展类型这一步不能省略否则在 tRPC 路由里拿ctx.session.user.id时TypeScript 会直接报“属性不存在”。4.3 开发环境热更新失效项目越写越大后热更新开始变得不稳定。有时候改一个组件页面很久不刷新有时候改了 Prisma schema数据库操作还是旧结构。我排查了一圈主要原因是改了 Prisma schema 后只执行了migrate dev但前后端引用的PrismaClient实例没有重启。解决办法是在.env中设置 Prisma 的日志输出并在 Prisma 客户端实例上启用重试逻辑import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma?: PrismaClient }; export const db globalForPrisma.prisma ?? new PrismaClient({ log: [query, error, warn], }); if (process.env.NODE_ENV ! production) globalForPrisma.prisma db;这个全局单例模式能避免开发环境下 PrismaClient 因为热更新被反复实例化从而连接不释放、查询报“Too many clients already”的错误。改完这段之后热更新恢复稳定数据库操作也没再出现过时结构的问题。4.4 部署后 tRPC 请求 404部署到 Vercel 之后页面能打开但前端一调 tRPC就要等一会儿才报 404。排查了很久才找到原因Vercel 的无服务器函数路由 path 和 tRPC 的挂载路径不匹配。T3 Stack 默认会把 tRPC 挂载到/api/trpc/{trpcPath}如果pages/api/trpc/[trpc].ts这个文件路径写错了或者文件名大小写不对Vercel 在构建时找不到匹配路由。另外一个常见原因是本地开发时没有NEXTAUTH_URL导致回调地址变成localhost部署后 GitHub 登录直接跳 404。我在排查时先看浏览器 Network发现 tRPC 请求本身返回 200只是后续 NextAuth 回调失败才定位到是回调地址问题。遇到部署后 404 的情况按下面顺序查确认NEXTAUTH_URL与线上域名完全一致确认 Vercel 环境变量全部重新部署过改环境变量后需要重新部署确认自定义域名没有在 GitHub OAuth 回调中拼错协议http 还是 https4.5 PostgreSQL 连接被拒部署构建失败这个问题我踩得更直观。上线前做prisma migrate deploy后报“Connection refused”原因是 PostgreSQL 云数据库默认只允许特定 IP 访问而本地和 Vercel 的 IP 都没加白名单。解决方式是在数据库控制台把 Vercel 构建环境所在的网络段加入白名单或者干脆把数据库设置为允许公网访问但限定连接账号的强密码。小规模项目这样操作足够安全因为真实的访问面很小再加上数据库账号独立、密码强度高受到暴力破解的概率很低。部署失败还有一种隐蔽原因Vercel 构建时执行prisma generate需要访问数据库吗其实不需要generate只读 schema 文件。所以构建失败优先怀疑环境变量缺失而不是数据库状态。5. 这个项目还能怎么玩t3code 做到 MVP 已经能日常使用但它的扩展空间其实很大。我自己列了几个后续方向排在最前面的是 AI 能力接入。代码片段经常只有标题没有描述用大模型自动生成描述和摘要能让搜索的准确率高很多。这一点在 t3code 上特别好实现因为 snippet 是结构化数据传给模型只需要一个 POST 请求。另一个方向是浏览器插件。我已经收藏找过很多次代码最自然的诉求是把浏览器里选中的代码直接一键保存到 t3code而不需要手动复制、打开后台、新建、粘贴、打标签这一套流程。做一个轻量扩展比预期要简单核心就是调用 tRPC 的 create mutation关键是处理好标签自动生成需要从页面 URL 和选中文本上下文里提取。最后一个我现在已经计划中的是 GitHub 同步。t3code 里的公开 snippet 可以按照语言分类自动推送到一个 GitHub 仓库这样内容就多了一层备份也更方便开源分享。为了实现它只需要在 snippet 的isPublic为 true 时触发一次 GitHub API 的提交大概几百行代码就够了。对我个人而言做 t3code 最大的收获不是把代码写得多漂亮而是重新理解了“小项目也要认真设计”这句话。数据表结构、类型约束、权限体系这些早动手后面就越轻松。项目再小只要想长期用就不能拿演示工程的心态去写。最后分享一个实操中的小技巧给 snippet 打标签时不要随心所欲把语言javascript、typescript、css和用途utils、config、bug分开打。刚开始我觉得这有点麻烦但数据量稍微多起来之后这个习惯让搜索效率翻了不止一倍。t3code 不是谁的通用产品但至少它把我的碎片化代码重新整理成了可控的资产这个价值已经比写代码本身更值得了。