t3code 这个名字最初只是我随手敲的一个仓库名。T3 是它背后的技术栈代号code 是它真正管的东西——一个用来整理我散落在各处代码块的小工具。平时写项目时好的代码片段总是随手扔进备忘录、临时文件、甚至聊天记录里等真正要找的时候翻半天。t3code 就是我为这个痛点做的个人项目用 T3 Stack 搭一个带登录、能分类、支持语法高亮的代码片段管理工具。如果你也想入门全栈开发或者想看看 Next.js、tRPC、Prisma 这些技术怎么组合成一个真实可用的项目这篇实操记录应该能给你完整的参考。1. 项目定位t3code 到底在解决什么问题先说清楚这个项目是干嘛的。t3code 不是一个通用的代码托管平台它更像一个“私人的代码收藏夹”。我在开发中经常遇到三类场景临时调试用的脚本、反复用到的工具函数、看到别人项目里不错的设计模式。这些零散内容以前都分散在各种地方既不便于搜索也没有语言分类更别提语法高亮和分享。t3code 的目标就是把它们统一收拢到一个 Web 应用里。整个项目的核心功能我一开始就定得很克制功能模块具体说明用户登录接 GitHub OAuth区分公开片段和私有片段片段创建标题、代码内容、语言类型、可见性标签管理给每个片段打标签按标签筛选搜索按标题和代码内容做简单搜索语法高亮服务端渲染高亮分享出去也不丢样式一键复制查看详情时快速复制代码我希望 t3code 保持轻量不做复杂的团队协作、评论、投票之类的东西。原因很简单个人工具追求的是解决问题不是堆功能。很多项目做到最后失控就是因为一开始什么功能都想塞进去。t3code 从立项到能用我给自己定的时间是两个晚上功能边界清晰才能跑这么快。从技术栈角度看这个项目选的是 T3 Stack 全家桶Next.js、TypeScript、tRPC、Prisma、Tailwind CSS再加上 NextAuth.js 做认证。这套组合最大的特点就是类型安全贯穿全链路写后端接口和写前端调用之间几乎没有断层。如果你之前是“前端写页面、后端写接口、两边对着文档联调”的工作方式上手 t3code 这种模式会有一个很明显的体感变化很多类型错误在编译期就被拦下来了。项目适合谁来参考我觉得分三类。第一类是刚学完 React、想接触全栈开发的人t3code 能展示一个现代全栈项目的标准形态。第二类是正在纠结技术选型的团队看完这篇可以评估 T3 Stack 是否适合自己。第三类是自己手里有一堆代码碎片、想做个内部工具解决的开发者可以直接拿 t3code 改造成自己需要的样子。2. 技术选型T3 全家桶背后的取舍逻辑2.1 Next.js App Router为什么不用纯前端方案最开始我也犹豫过一个代码片段工具用 React Vite 不是更简单吗答案是t3code 需要服务端能力和分享能力。如果片段是私有工具Vite 单页应用也能跑但一旦我想把一个公开片段分享给同事就需要有服务端渲染来保证链接打开就有内容还要有数据库来持久化。Next.js 把前端、服务端、API 层放在同一个项目里对于 t3code 这种体量的项目来说是最省事的选择。我选的是 Next.js 的 App Router。t3code 的页面结构不算复杂但 App Router 的 Server Component 让我可以在服务端直接查询数据库、渲染代码高亮然后一次性把 HTML 发给浏览器。这对代码展示类应用非常友好因为代码高亮本身是 CPU 密集型操作放在服务端做能让客户端省下不少解析时间。用 App Router 还有一个实际体验嵌套布局很自然。t3code 只有一个主布局顶部放导航栏下面是内容区布局文件里的代码保持得很干净。如果是新手我建议直接从 App Router 开始学因为它是 Next.js 现在的默认方向生态里的新工具都在往这个模式上靠。2.2 tRPC让 API 层从数据契约里解放出来t3code 里最让我上头的技术是 tRPC。以前写前后端分离项目最烦的就是 API 文档维护。前端要调后端接口得先看文档了解入参和出参后端改了字段前端经常要等半天才能发现类型对不上。tRPC 的做法非常直接后端用 TypeScript 写一个 Router前端直接调用这个 Router 的方法类型从后端一路传达到前端根本不需要中间定义一份契约。打个比方REST 接口像两个人通过传纸条沟通纸条上的格式必须提前约定好tRPC 像是两个人直接打电话你说的话对方当场就能听懂。t3code 里我创建片段的接口前端调用时的参数类型、返回值类型、出错类型全是自动推导的改后端代码的时候前端代码里立刻就会报错提醒。这种体验一旦习惯了很难退回 REST 手动类型定义的模式。当然 tRPC 不是万能的。它的前提是前后端都使用 TypeScript而且它更适合 BFFBackend for Frontend这类场景。如果项目里有大量第三方对接、开放 API 需求还是需要 REST 或者 GraphQL 做对外协议。但在 t3code 这种纯粹给 Web 前端消费的服务端场景里tRPC 是最优解。2.3 Prisma数据库建模像写文章一样直接数据存储这块我选了 Prisma原因非常实际建模直观、迁移方便、类型安全。t3code 的数据模型不复杂就是用户、片段、标签三者之间的关系。用 Prisma Schema 描述起来非常清晰而且改完模型后执行迁移命令数据库表结构就自动同步了。开发环境我用 SQLite磁盘上一个文件就是整个数据库零配置、零部署成本。等要上线的时候只需要把 DATABASE_URL 改成 Postgres 的地址重新执行一次数据库迁移Prisma 会自动处理方言差异。也就是说我开发调试和线上运行完全不用换一套代码逻辑。Prisma 的查询 API 也是我选它的重要原因。写 t3code 列表页的时候我需要把片段和它的标签一起查出来Prisma 一行 include 就搞定了返回的都是类型完整的对象。不需要手写 SQL也不需要做手工的对象映射。对小型全栈项目来说这能节省大量样板代码。2.4 Tailwind CSS Shiki样式与高亮一站式搞定样式方面T3 Stack 默认集成 Tailwind CSS我直接沿用了。t3code 的界面不复杂主要是列表、卡片、表单、详情页。Tailwind 的原子类让这套东西写起来很快而且不需要维护单独的 CSS 文件。代码高亮我用了 Shiki它内部基于 VS Code 的文本解析高亮效果和编辑器里的几乎一致支持的语言非常全。更重要的是 Shiki 服务端渲染很成熟可以在输出 HTML 时就把高亮颜色直接写进span里配合 t3code 的公开分享场景开链接不看 JS 执行也照样有完整的颜色。如果你之前依赖客户端高亮库建议试试这种服务端方案。客户端高亮经常遇到一个问题首屏先看到一段白板代码然后脚本执行后才上色体验很割裂。服务端渲染相当于把上色这一步提前完成浏览器拿到就是成品。2.5 NextAuth.js认证不用从轮子造起登录功能如果自己写要处理密码哈希、会话管理、OAuth 回调、Cookie 安全这些细节很容易出问题。t3code 直接用了 NextAuth.jsT3 Stack 官方推荐方案。我接的是 GitHub OAuth实现一个 providers.GitHub 配置再加上数据库会话持久化登录流程基本就成型了。注意 NextAuth 的配置有几个容易踩坑的地方后面会专门列。这里先说选型感受它把认证从最头疼的部分变成了一种配置选项而且和 Prisma 的 Session Strategy 结合得很好用户登录状态会存在数据库里服务端和客户端都能方便地读取。对于希望快速具备完整登录功能的个人项目没有比它更顺手的方案了。3. 核心功能实现从 Schema 到页面3.1 先建数据模型再写业务我习惯先定义数据再写业务逻辑。t3code 的 Prisma Schema 一开始只定义了三个模型User、Snippet、Tag。核心是 Snippet它和 User 是多对一关系一个作者多个片段和 Tag 是多对多关系一个片段可以打多个标签一个标签可以挂多个片段。generator client { provider prisma-client-js } datasource db { provider sqlite url env(DATABASE_URL) } model User { id String id default(cuid()) name String? email String? unique emailVerified DateTime? image String? accounts Account[] sessions Session[] snippets Snippet[] } model Snippet { id String id default(cuid()) title String content String language String default(typescript) isPublic Boolean default(false) authorId String author User relation(fields: [authorId], references: [id]) tags Tag[] createdAt DateTime default(now()) updatedAt DateTime updatedAt } model Tag { id String id default(cuid()) name String unique snippets Snippet[] }执行npx prisma migrate dev --name init之后SQLite 表结构就生成了。这里有个原则我反复跟人强调models 里放的是数据的静态结构业务的动态逻辑要全部放到 tRPC Router 里不要在页面里直接操作 Prisma Client。t3code 的结构遵循了 Single Source of Truth这样后续加权限控制也只在 Router 层集中处理。3.2 用 tRPC Router 封装所有业务逻辑下一步是写 tRPC Router。t3code 里我建了一个 snippetRouter只暴露四个方法列表查询、详情查询、创建、删除。以创建片段为例先用 zod 定义入参规则再用 protectedProcedure 保证必须登录才能调用然后通过 ctx.db 写入数据库。import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; export const snippetRouter createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) { return ctx.db.snippet.findMany({ where: { isPublic: true }, include: { tags: true }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1), content: z.string().min(1), language: z.string().default(typescript), 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, language: input.language, isPublic: input.isPublic, authorId: ctx.session.user.id, tags: { connectOrCreate: input.tags.map((name) ({ where: { name }, create: { name }, })), }, }, }); return snippet; }), });这段代码有几个值得注意的点。tag 的处理用的 connectOrCreate意思是如果标签已经存在就直接关联如果不存在就先创建再关联。这样就不需要单独写“查询标签是否存在”的逻辑。zod 的 default 可以在用户没传语言时自动补成 typescript减少前端负担。在 tRPC 里protectedProcedure 和 publicProcedure 的区分很关键。t3code 里创建片段必须是登录用户所以用 protectedProcedure它内部会读取 session如果没有登录直接抛错。而公开片段的列表用 publicProcedure不登录也能看这样才能满足分享场景。3.3 页面怎么调用 tRPC写页面时我把 t3code 的页面分成两类。一类是服务端读取数据的展示页比如公开片段列表详情页一类是客户端交互的表单比如创建片段页。展示页用 Server Component直接调用 tRPC 的查询方法。import { api } from ~/trpc/server; export default async function SnippetDetailPage({ params, }: { params: { id: string }; }) { // 在服务端查询数据 const snippet await api.snippet.queryById({ id: params.id }); return ( main classNamemx-auto max-w-3xl p-8 h1 classNametext-2xl font-bold{snippet.title}/h1 div classNamemt-4 flex gap-2 {snippet.tags.map((tag) ( span key{tag.id} classNamerounded bg-gray-100 px-2 py-1 text-sm {tag.name} /span ))} /div pre classNamemt-6 overflow-x-auto rounded-lg bg-gray-900 p-4 text-white code dangerouslySetInnerHTML{{ __html: snippet.highlightedHtml }} / /pre /main ); }这里的 highlightedHtml 是从服务端用 Shiki 生成的Schema 里并没有这个字段我是查询出来后动态计算的。这种模式非常适合代码展示SEO 也友好。如果你只需要客户端交互比如新建片段表单则用 api.snippet.create 配合 useMutation提交后调用 router.refresh 刷新列表。3.4 语法高亮和复制按钮语法高亮这块我单独做了个小模块封装了一个highlight.ts在服务端调用 Shiki。Shiki 第一次会加载词法解析器建议在模块顶层做单例避免每次请求都重新初始化。t3code 里我维护了一个高亮器缓存进程内只创建一次页面查询、详情页渲染都复用同一个实例。复制按钮我用的浏览器 Clipboard API。React 的 onClick 里异步调用 navigator.clipboard.writeText注意这个接口要求页面必须在 HTTPS 环境下运行本地开发 localhost 算例外如果部署到 HTTP 环境会失效。部署域名我建议直接用平台提供的 HTTPS 域名后面会讲到。复制按钮最简单的方式是给服务端渲染的 pre 包一层 div用一个客户端子组件监听点击。这里不要去 document.getElementById直接在组件内部绑定事件更符合 React 的习惯也避免多重渲染时 ID 冲突。4. 部署日志t3code 从上本机到公网4.1 环境变量是第一个拦路虎t3code 在本地开发一切正常推向生产后第一个问题就是环境变量。Next.js 的环境变量体系里只有 NEXT_PUBLIC_ 开头的变量会暴露给浏览器端其余只在服务端使用。t3code 里我用了四个关键变量DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL、GITHUB_CLIENT_ID 和 GITHUB_CLIENT_SECRET。我踩的第一个坑是 NEXTAUTH_SECRET。生产环境如果不设置这个值NextAuth 会在服务重启时随机生成一个密钥导致的结果就是用户登录状态一会儿有效一会儿失效症状非常诡异。解决办法是在部署平台的 Environment Variables 里手动设置一个固定随机字符串。本地我没写进.env线上设置的地址是.env/production两个环境的密钥必须统一不然会出现本地能登录线上不行的情况。还有个细节NEXTAUTH_URL 在 Vercel 平台上会自动生成不需要手动配置。但如果你用别的平台部署建议还是在环境变量里写完整的 URL。GitHub OAuth 里的回调地址也要填成线上域名不然会报 redirect_uri 不匹配这个问题我排查了快半小时才发现是 OAuth App 那边没同步更新回调。4.2 从 SQLite 切换 Postgres开发环境用的 SQLite线上我换成了 Postgres 数据库因为部署平台的文件系统通常是临时性的SQLite 的磁盘文件在重新部署或缩放时可能会丢。切换过程比我想象中简单把 DATABASE_URL 改成 Postgres 的连接串然后跑一次npx prisma migrate deploy。Prisma 会自动对照已经提交的迁移文件把生产库表结构建好。这里要特别提醒migrate dev和migrate deploy是两个不同的命令。前者会在开发时创建迁移文件并应用到本地库后者是到生产环境去应用已经存在的迁移文件。生产环境不要执行带 destory 危险的命令组合每次都是先跑 migrate deploy成功后再启动应用进程。4.3 构建与部署流程t3code 的构建流程是一条直线安装依赖、跑 Prisma 生成、构建 Next.js、启动服务。因为 T3 Stack 用的是 pnpm我在构建脚本里加了pnpm db:generate确保每次构建前都能重新生成 Prisma Client。部署过程中我最常查的就是构建日志里有没有出现类型错误。T3 Stack 的类型检查是构建的一部分tRPC 类型一旦对不上构建直接失败。这也是它为什么会让人觉得“写的时候麻烦一点点上线的时候省好多事”。如果构建报错先别急着改部署配置回来把 TypeScript 的报错逐条解决远比瞎猜环境变量配置有效。线上首屏性能我也测过一次。由于列表页和详情页都是 Server Component 服务端渲染一次加载就把 HTML 和高亮后的代码都拿回来了实际体验是两千多字节左右的代码片段页面Lighthouse 性能评分在 90 以上。对一个小工具来说这已经足够好了。4.4 上线后我仍然在用的两个性能优化上线之后我发现虽然服务端渲染很好但列表页返回了所有公开片段的高亮结果如果以后片段多了Payload 会越来越大。这里我做了两个优化。第一个优化是列表页不渲染高亮只显示标题、语言、标签和创建时间详情页才做高亮。这样列表非常轻量用户点击进去再等待高亮渲染。第二个优化是给列表查询加上 limit 和 offset也就是分页。t3code 目前量小用不上但我把查询参数留好了后面数据量上来再接入分页也是一行参数的事。经验是个人工具也要提前预留扩展点别等到数据几百条才开始加。5. 常见问题速查表与避坑技巧做 t3code 的过程中我踩了不少坑也帮几个朋友排查过相同问题。这里把出现频率最高的问题整理成一张速查表照着排查能省很多时间。问题现象可能原因解决办法页面一直提示未登录NEXTAUTH_SECRET 未统一检查线上和本地.env里的密钥是否一致GitHub 登录回调报错OAuth App 里的回调地址和线上域名不一致在 GitHub 开发者设置里把回调地址改成 https://你的域名/api/auth/callback/githubPrisma 查询报错 Table doesnt exist生产库还没跑迁移执行npx prisma migrate deploy刚部署完有数据重启后数据丢失使用了 SQLite 存在部署平台临时磁盘换成 Postgres 之类的独立数据库别依赖实例文件系统片段能创建但列表不显示创建时没有把 isPublic 设为 true而列表只查公开片段私有片段进详情页确认归属想展示就创建时勾选公开构建失败但本地正常tRPC 类型不一致或 Prisma Client 未生成先跑pnpm db:generate再检查 tRPC 路由名与调用名是否一致高亮代码里有奇怪的转义符号把代码直接放进了 JSX 文本而非 HTML使用 Shiki 的 highlight 生成 HTML然后用 dangerouslySetInnerHTML 注入再补两个独家技巧。第一个是 tRPC Router 内部如果发现类型不匹配优先检查入参 zod schema 和页面传入参数是否一致。我最常犯的错误是页面传了一个{ tagIds: [] }但 Router 要求的字段叫tagNames这种错误构建期就能暴露可一旦用any逃过检查上线后就会变成诡异的数据缺失。第二个技巧是 Next.js 环境变量的前缀。所有需要浏览器访问的变量比如高亮主题切换这种前端配置一定要用 NEXT_PUBLIC_ 开头。数据库连接串、OAuth Secret 绝对不要带这个前缀否则会被打包进客户端脚本。我曾经见过一个项目把数据库密码打成 NEXT_PUBLIC_DATABASE_URL等于直接把密钥发给了所有访问者。这个错误非常隐蔽因为本地运行没有任何报错但安全上已经是事故级别。最后再说点题外话。t3code 这个项目虽然小但做完之后我对全栈开发的体感完全不一样了。以前总觉得全栈是两种语言的拼接前端一套后端一套中间还要维护 API 文档。在 T3 Stack 里前端、后端、数据库、认证全都在一个类型系统里流动改一处别的立刻能感知到。这种确定性的开发体验很让人上瘾。如果你也想做一个自己的小工具建议不要一开始就追求复杂架构先把功能跑通再把数据模型、权限、部署这些细节扣细。一个能上线、能被自己每天使用的项目比任何教科书里的“最佳实践”都更值钱。