
t3code 是我最近业余时间从零做的一个代码片段管理工具技术栈直接选了 T3 全家桶Next.js TypeScript Tailwind CSS tRPC Prisma。起因特别朴素写过的代码越来越多本地笔记、浏览器收藏夹、微信文件传输助手里的临时片段散落得到处都是真到要用的时候经常翻半天找不到。这个项目本身不大但恰好覆盖了一套现代全栈 Web 应用从设计、编码到部署上线的完整链路中间踩了不少坑也把 T3 Stack 各成员之间的配合逻辑摸透了。如果你正在纠结要不要上 T3 这套方案或者想找一个完整的、可以直接照抄落地的全栈练手项目这篇应该能帮到你。1. 项目定位T3Code 到底要解决什么问题1.1 我的真实痛点写代码这些年最烦的不是不会写而是“明明写过却找不到”。查过一次的加密算法实现、调好的 Excel 导出函数、某个正则表达式散落在本地 Markdown、浏览器收藏夹、微信文件传输助手里等真要用了翻半天最后灰溜溜重新 Google 一遍。T3Code 就是奔着这个痛点去的一个专门给开发者用的代码片段管理工具支持快速新增、打标签、全文搜索、语法高亮所有数据都存在自己的数据库里。市面上的片段管理工具不是没有比如一些商业软件和笔记应用但我有几个硬需求它们满足不了。第一必须能部署在自己掌控的服务器上数据不经过任何第三方服务第二交互要足够轻打开页面直接搜两秒内出结果第三我想在代码层面完全掌控这个工具方便后续折腾命令行入口或者浏览器插件。既然要自己掌控那就干脆自己动手写一套。1.2 功能范围与 MVP 边界我给这个项目定的 MVP 只有四个核心功能新增片段、打标签、全文搜索、编辑删除。没有做团队协作没有做分享链接没有做数据导出这些全部砍掉。原因很简单个人工具最怕功能膨胀每多一个功能就多一堆边界条件和潜在 Bug核心链路没跑顺之前其他功能都是自嗨。功能边界这么切开发量大概在一个人的一周业余时间能完成的范围。后来事实证明这个判断是对的正因为我砍掉了大多数花活才有精力把搜索速度、编辑体验和类型安全这几个真正影响日常使用的地方打磨到位。这个取舍思路也推荐给你尤其是做个人项目先问自己“哪个功能删了我会立刻后悔”而不是“哪个功能加了显得很酷”。2. 技术选型拆解为什么是 T3 全家桶2.1 T3 Stack 到底是什么T3 Stack 是一套全 TypeScript 的 Web 应用技术组合核心成员包括 Next.js、TypeScript、Tailwind CSS、tRPC通常还会带上 Prisma 和 NextAuth。最早由社区开发者推出核心理念是“用一套语言写前后端让类型从数据库一路流动到浏览器按钮”。这里的名称容易望文生义T3 并不是某个版本的代号只是这套技术组合约定俗成的叫法。所以 t3code 这个名字既点明了它是代码片段管理工具也暗含它构建在 T3 生态之上这层意思。对从没接触过 T3 的读者你可以把它理解成一套“全是 TypeScript 的现代全栈开发全家桶”目标是用最少的心智负担做出前后端类型完全打通的应用。2.2 每个关键选型的理由Next.js 承担页面与路由层Next.js 提供 App Router、服务端组件、API 路由和一键部署到 Vercel 的能力。对单体应用来说这个框架最大的价值是“一个仓库里同时搞定页面和服务端逻辑”不用再单独维护一个 Node 后端项目省掉大量跨项目沟通成本。服务端组件还能直接连数据库首屏数据在服务端渲染好再交给浏览器体验上接近传统服务端应用但开发体验却是组件化的。tRPC 解决前后端通信传统前后端通信要先定义 REST 接口写路由、写控制器、写 DTO然后用 OpenAPI/Swagger 维护契约文档改一个字段要同步改三四处。tRPC 的思路完全不一样RPC 直连函数。前端调用后端就像调用本地函数类型自动推导编译期就能发现接口参数和返回值不匹配的问题。用生活化一点的类比说明REST 像寄快递需要写地址、打包、等驿站收发每一步都有单号tRPC 更像把商品直接递到你手里省去打包拆包的环节拿到就能用。代价是前后端必须共享同一个代码仓库而这恰好是单体项目的自然形态所以我几乎没犹豫就选了它。Prisma 管理数据库层Prisma 的优势在于数据库建模和迁移管理。Schema 文件用声明式语法定义表结构prisma migrate dev自动生成迁移 SQL客户端类型自动生成。写ctx.db.snippet.findMany()的时候编译器就知道返回的是哪些字段不存在字段名拼错这种低级事故。对于没有专职 DBA 的个人项目这个开发体验非常友好把 SQL 的坑封装在框架层出错时错误信息也足够直白。Tailwind CSS 负责样式Tailwind 用原子类直接写在组件里不用切到 CSS 文件反复改选择器。对个人项目来说这个特性的价值不止是效率更多是心理上的“低切换成本”改一个按钮的圆角和间距直接在 class 里加几个类就行不会打断写逻辑的思路。配合深色模式做起来也顺手主题切换类名一套就好。2.3 我刻意不用的方案选型阶段我也认真考虑过几套替代方案最后都放弃了理由写在这里供你参考候选方案放弃理由Spring Boot Vue 前后端分离两套语言、两套工程API 契约需要手工维护单人项目纯负担纯 Next.js REST API没有 tRPC 的类型推导参数校验要写两遍改动接口字段容易漏改直接用商业 SaaS 服务数据不在自己手里而且几乎无法自定义搜索和展示逻辑这套取舍的结论就是T3 Stack 最适合“一个人想用最少的心智负担做一套全栈应用”的场景。它要求你接受 TypeScript 作为唯一语言、接受单仓库模型换来的是全程类型安全和极高的开发效率。如果你本来就是全栈开发者并且愿意拥抱这套约束收益会非常明显。3. 核心实现细节数据库、接口与鉴权3.1 Prisma 数据模型设计数据库逻辑其实不复杂就三张表用户、片段、标签。难点在“标签”的设计。比较偷懒的做法是给片段表加一个tags字符串数组但逗号分隔容易出现歧义也不利于将来统计每个标签下有多少片段。所以我选了标准的多对多关系Prisma Schema 如下generator client { provider prisma-client-js } datasource db { provider postgresql url env(DATABASE_URL) } model User { id String id default(cuid()) name String? email String? unique snippets Snippet[] tags Tag[] } model Snippet { id String id default(cuid()) title String content String language String createdAt DateTime default(now()) updatedAt DateTime updatedAt userId String user User relation(fields: [userId], references: [id]) tags Tag[] index([userId, updatedAt]) } model Tag { id String id default(cuid()) name String userId String user User relation(fields: [userId], references: [id]) snippets Snippet[] unique([name, userId]) }几个设计细节值得专门说明。第一Snippet.userId上加了index([userId, updatedAt])复合索引。因为列表页最常见的操作是“查某个用户的所有片段按更新时间倒序”这个索引让查询直接走索引扫描不用全表过滤。个人项目数据量不大时效果不明显但养成顺手加索引的习惯成本很低等数据量真上来了再补就要面临在线 DDL 的复杂操作。第二Tag表的唯一约束是unique([name, userId])。也就是说同一个用户不能有重名标签但不同用户可以有同名标签互不干扰。这个设计对应了“标签归属用户”的模型配合后面的connectOrCreate使用非常顺手。第三所有主键都用cuid()而不是自增 id。自增 id 有两个问题一是会暴露数据量二是多环境同步数据时容易冲突。cuid 是全局唯一的本地开发环境、线上环境、临时环境的数据想合并就合并不需要协调 id 分配。3.2 tRPC 路由与 Zod 校验tRPC 的核心是定义一个 router 树每个方法对应一次远程调用。我这里按资源拆了三个 routersnippet、tag、auth。以创建片段为例完整代码如下这是整个项目里最有代表性的一个接口import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; import { z } from zod; export const snippetRouter createTRPCRouter({ create: protectedProcedure .input( z.object({ title: z.string().min(1, 标题不能为空).max(100), content: z.string().min(1, 内容不能为空), language: z.string().min(1).max(30), tags: z.array(z.string().min(1)).max(10).default([]), }) ) .mutation(async ({ ctx, input }) { const tags input.tags.map((name) ({ where: { name_userId: { name, userId: ctx.session.user.id } }, create: { name, userId: ctx.session.user.id }, })); return ctx.db.snippet.create({ data: { title: input.title, content: input.content, language: input.language, userId: ctx.session.user.id, tags: { connectOrCreate: tags }, }, include: { tags: true }, }); }), });tRPC 有几个和普通 HTTP 接口不一样的地方展开说一下。第一protectedProcedure是二次封装。基础版 procedure 不校验登录态我在server/api/trpc.ts里包了一层通过ctx.session拿到当前登录用户没有 session 就直接抛 UNAUTHORIZED前端就能拿到统一的错误信息。这个模式比在每个接口里手动判断if (!ctx.session.user)优雅得多也避免漏掉某个接口的鉴权。一个小团队或者个人项目里最容易出的安全问题就是“这个接口忘了加登录校验”用封装好的 procedure 可以从根上杜绝。第二Zod 校验在服务端强制执行。就算前端完全不用表单校验框架恶意请求直接打接口也会因为 Zod schema 不匹配而被拒绝。参数合不合法、数组长度超没超都在这一层挡住。前端可以配合同样的 Zod schema 做即时提示一套规则两端共用不会出现“前端验证过了后端又验一遍但规则不一样”的经典毛病。这种体验也是 tRPC 生态里我最喜欢的地方。第三connectOrCreate是实现“保存片段时自动创建标签”的关键。如果标签已存在就直接关联不存在才创建省掉一次先查标签再创建的往返。这一行代码节省了后面大量逻辑但也埋了坑第 5 章会专门讲它踩出来的 P2025 错误。3.3 NextAuth 会话与权限控制鉴权方案选的是 NextAuth提供者先接 GitHub后面又补了邮箱登录。配置上最需要注意的是 session 策略和ctx.session的类型安全。核心代码在server/auth.tsimport { NextAuthOptions } from next-auth; import GitHubProvider from next-auth/providers/github; import { PrismaAdapter } from next-auth/prisma-adapter; import { db } from ~/server/db; export const authOptions: NextAuthOptions { adapter: PrismaAdapter(db), providers: [ GitHubProvider({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), ], session: { strategy: database }, callbacks: { session: ({ session, user }) ({ ...session, user: { ...session.user, id: user.id }, }), }, };这里有个关键点默认session.user里没有数据库主键 id只有 email、name 这些由 OAuth 提供方带过来的信息。我的所有片段都挂在userId上所以必须在 session callback 里把user.id注入到 session前端页面和 tRPC 的ctx.session.user.id才能稳定取到。这个坑不踩一次很难意识到文档里写得不算显眼但漏掉它你的所有关联查询都会因为拿不到用户 id 而失败。权限控制方面除了路由层用protectedProcedure页面侧也做了双保险在需要登录的中间层用 middleware 重定向在页面组件里用useSession做兜底渲染。中间件负责体验未登录直接跳登录页接口鉴权负责安全不管谁发请求没有 session 都拒绝。两条链路各管一段效果比较踏实。4. 实操全流程从脚手架到部署上线4.1 项目初始化与目录结构初始化用的是 create-t3-app直接跑npx create-t3-applatest t3code交互式选项我勾了 NextAuth、Prisma、Tailwind、tRPCESLint 保留默认。这个 CLI 最大的好处是把 App Router、Tailwind、tRPC、Prisma、NextAuth 全部预装并配好省掉了手工对接的繁琐步骤装上就能跑通一个 hello router。你不需要自己写trpc.ts客户端初始化、Prisma client 单例这些样板代码脚手架全帮你处理了。生成的目录结构里我最常用的几个路径长这样t3code/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ # 页面与路由 │ │ ├── page.tsx │ │ └── snippets/ │ ├── server/ │ │ ├── api/ │ │ │ ├── auth/[...nextauth]/route.ts │ │ │ ├── routers/ │ │ │ │ ├── snippet.ts │ │ │ │ └── tag.ts │ │ │ ├── root.ts │ │ │ └── trpc.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── server.ts │ │ └── client.ts │ └── styles/ └── .env这里我特别建议新手不要一上来就改目录结构先按 T3 的默认约定跑起来理解了server/api/trpc.ts为什么包裹createTRPCContext再去调整布局。脚手架生成的是社区反复打磨过的结构比你自己拍脑袋设计的合理得多。初始化完第一步不是写业务而是配数据库。先改.env里的DATABASE_URL然后跑一遍prisma db push或者prisma migrate dev把初始表建好最后启动 dev server 验证 hello tRPC 能通。这几分钟很值等于给后续开发确认了“地基是稳的”。4.2 主链路功能实现保存、搜索、编辑保存片段的核心是上面贴过的snippet.createmutation。前端页面组合了 server component 渲染列表和 client component 处理表单调用trpc.snippet.create.useMutation()成功后用router.refresh()刷新服务端组件列表。这种混合模式要适应一下页面大部分逻辑在服务端跑只有需要交互的部分才在客户端。搜索这里值得单独说。一开始我打算用 Prisma 的contains做模糊查询因为实现最简单。实测之后发现两个问题第一contains的大小写行为依赖数据库配置PostgreSQL 下不配置经常会返回让人困惑的结果第二数据量大了之后LIKE %关键词%不走索引性能越来越差。所以我把搜索拆成两层标题栏用实时输入触发contains查询保证灵敏正文搜索用 PostgreSQL 的全文检索能力保证准确和性能。虽然早期数据量撑不起这么重的方案但提前把架构拉开后面塞数据的时候不用推倒重来。如果你刚开始做建议先只做标题搜索等有真实数据量再加正文全文检索避免过度设计。编辑功能相对简单一个updatemutation输入和 create 共用一套 Zod schema用include: { tags: true }把已有标签带出来前端渲染成可编辑的标签输入。删除是deletemutation这里需要注意级联关系片段删除时要把关联的 Tag 关系清理掉否则中间表里会残留孤儿数据。Prisma 里我通过关系字段的onDelete: Cascade配置解决测试删了几条确认中间表里干干净净才放心的。4.3 部署与上线配置部署平台选了 Vercel数据库用 Railway 的 PostgreSQL 实例。Vercel 对 Next.js 的适配接近零配置代码推到 GitHub 分支就自动构建发布回滚也方便。Railway 的数据库自带公网连接串磁盘备份配置也省心。部署时最容易出问题的三个环境变量变量名作用容易踩的坑DATABASE_URLPrisma 连接数据库忘了加连接池参数导致连接数打满NEXTAUTH_SECRETNextAuth 签名密钥随机生成后没保存重启后会话全失效NEXTAUTH_URLOAuth 回调地址本地写localhost:3000部署后忘了改成正式域名特别是 NEXTAUTH_URL本地跑得好好的一部署登录就报 callback URL mismatch或者直接转圈跳不回来九成是这个变量没改。上线之后我第一件事就是开飞行模式用手机浏览器试一遍登录专门验证跨环境下的 callback 逻辑。数据库连接这块还有个关键配置Railway 提供的连接串是直连地址Vercel 的 serverless 环境里会创建大量短连接很容易打爆数据库连接池。我处理的办法是在 Prisma datasource 的连接串加后缀参数控制连接池上限同时把 Prisma 客户端的连接反复用起来。具体配置因平台而异但核心思路是“serverless 环境一定要显式限制连接数”不然并发稍微上来数据库就罢工。5. 踩坑实录5 个真实问题与排查方法5.1 问题一部署后登录报错反复跳回登录页现象本地开发一切正常部署到 Vercel 后点击 GitHub 登录浏览器在/api/auth/callback和首页之间来回跳最后停在登录页。排查过程先看 Vercel 的日志NextAuth 在 serverless 里没有明显报错只是 session 一直拿不到。再检查 cookie发现__Secure-next-auth.session-token没有写进去。最终定位是 NEXTAUTH_URL 还停留在 localhost。这类问题的通用排查顺序是先看环境变量再看 cookie最后才怀疑代码。很多 NextAuth 的线上问题根本不是代码问题而是环境配置和本地不一致。建议项目部署后第一件事就是把浏览器 cookie 和 localStorage 清一遍再完整测试登录流程别在脏数据上排查。5.2 问题二Prisma 报 P2025 记录不存在现象保存一个带全新标签的片段时接口报 P2025错误信息提示找不到 Tag 的唯一记录。排查过程报这个错基本可以锁定是connectOrCreate的 where 条件写错了。Prisma 对connectOrCreate有个隐性要求where 里必须用满足唯一约束的字段组合。我一开始写的 where 是{ name: xxx }但 Tag 表上并没有 name 单独唯一索引真正的唯一约束是(name, userId)组合Prisma 在执行时不知道应该匹配哪一行于是报 P2025。修复方式是把 where 改成组合唯一键where: { name_userId: { name, userId: ctx.session.user.id } }这个坑建议所有用 Prisma 的朋友提前知道凡是connectOrCreate报 P2025第一反应去查 schema 里的unique几乎都是 where 没走唯一约束的锅。你在这个地方耗十分钟下次就能省一小时。5.3 问题三tRPC 接口返回 404现象某个 mutation 本地调试一切正常部署到生产环境刷新页面后调用接口返回 404。排查过程tRPC 的路由本质是 Next.js 的 HTTP 路由挂在src/app/api/trpc/[trpc]/route.ts下。这个文件是动态路由如果被平台静态化处理请求就不知道该分发给哪个 handler于是 404。解决方法是在那个 route 文件里显式标注动态渲染export const dynamic force-dynamic;这个坑的根本原因是 serverless 平台对静态化非常激进。好的一面是页面加载快坏的一面是像 tRPC 这种依赖 request 上下文的接口容易被打错标记。如果你在 Vercel 上遇到接口时好时坏先去检查这条标记是不是被优化掉了。5.4 问题四页面刷新后代码高亮样式丢失现象片段详情页用了 highlight.js 做代码高亮首次进入页面样式正常刷新后样式完全消失只剩纯文本。排查过程问题出在服务端渲染和客户端渲染对主题类名的加载时机不一致。highlight.js 的主题样式被加在某个只有客户端渲染阶段才加载的组件里服务端渲染出来的 HTML 不带主题样式刷新后客户端重新挂载时才补上但已经错过了注入时机。解决方法是把主题 CSS 改成全局引入放到layout.tsx的全局样式里而不是跟着组件按需加载。页面刷新后服务端渲染出来的 HTML 直接带主题样式稳定得很。这类“首次正常刷新坏掉”的问题大概率都和服务端、客户端的渲染时序有关排查方向可以往这里引。5.5 问题五CSP 拦截了内联样式现象部署上线后控制台出现大量 CSP 报错Tailwind 的某些内联样式被拦截页面布局错乱。排查过程这和数据无关是安全策略问题。Next.js 会默认提示 CSP 配置如果你按建议在响应头里加了 Content-Security-Policy却没把内联样式style-src unsafe-inline放行Tailwind 运行时生成的动态样式就会被阻断。解决办法是先在开发环境确认样式全部正常然后给页面响应添加合适的 CSP 头。需要注意过度收紧的 CSP 对使用大量动态 class 的 Tailwind 项目很不友好所以安全策略和生产效率之间要自己找平衡。我的建议是个人项目先把安全头配到“能挡住明显攻击”就行不要一上来就上最严格配置否则排查成本会吃掉你的开发效率。6. 一点真实的使用实感写到这里T3Code 的核心链路基本讲完了最后说点不太技术但很重要的体会。第一个感受是类型安全真的会改变写代码的方式。以前做前后端分离的项目接口字段名写错是常事要等运行时才发现前端报 undefined、后端报 null两边对着日志猜。T3 这套链路下来类型错误在编译期就暴露了错误信息精确到哪个字段不匹配。习惯了这种体验之后我很难再回去写没有类型推导的后端接口。建议所有还在手工维护接口文档的人体验一次哪怕只是个小 demo体感差异非常大。第二个感受是个人项目一定要控制范围。我一开始列的功能清单里有分享链接、数据导出、团队空间要不是老老实实把 MVP 限制在四个核心功能这个项目大概率现在还躺在开发分支里。做完核心链路再回头看那些砍掉的功能真的都不是必需品等真实使用中遇到了再说远比凭空臆想需求靠谱。最后分享一个小心得这种个人工具最容易被忽略的不是技术而是“自己真的会去用”。一个代码片段管理工具如果保存一段代码要填五个字段你用两次就懒得用了。我把表单简化到了极致标题、内容、语言三个必填项标签可空保存按钮就在输入框边上。工具的价值不是功能多而是用的时候顺手。这一点始终是我做这类项目的第一原则。