基于T3 Stack的代码片段管理工具t3code开发实践
1. t3code 是什么:从 T3 Stack 出发的项目定位与选型思考
第一次看到 t3code 这个名字,很多人会以为它又是个轮子型的代码生成器,或者某个在线编译器。实际情况是,我在业余时间用它做了一个基于T3 Stack的代码片段管理工具,核心逻辑很简单:把日常开发中零散的命令、配置片段、正则表达式、Shell 脚本统一收进一个带标签体系的 Web 应用里,需要的时候直接用浏览器搜出来复制,不用再翻聊天记录和本地笔记。
T3 Stack 是近年全栈开发圈里讨论度很高的一套组合拳,主推Next.js + TypeScript + Tailwind CSS + tRPC,这个组合解决了我最头疼的问题:类型安全。传统的全栈开发里,前端调接口传参、后端返回数据,总有一个环节要靠手写类型定义或者阅读接口文档来对齐,稍不注意字段名拼错就是线上 bug。而 tRPC 的思路是让前端直接调用后端函数,类型从数据库到 UI 一路贯通,改一个字段,前端立刻标红。这在 t3code 这种以"增删改查"为主的工具类项目上,收益非常直观。
我原本考虑过用 Remix 或者 Nuxt,但评估下来,T3 Stack 对 TypeScript 的拥抱程度最彻底,而且 Next.js 的生态足够成熟,Vercel 部署几乎零成本。t3code 项目本身没有复杂的业务规则,它是一个典型的 CRUD 应用,非常适合拿来体验 tRPC 的完整工作流。如果你也想做一个个人工具型全栈项目,T3 Stack 的性价比很高。
整个项目的技术选型不是拍脑袋定的,我列过一张对比表,最终敲定的原因后面会展开讲。
| 方案 | 类型安全程度 | 前后端分离度 | 部署成本 | 适合场景 |
|---|---|---|---|---|
| 传统 REST API + React | 弱,需手写类型 | 高,需维护 API 文档 | 中 | 大型项目、多端复用 |
| GraphQL + Apollo | 强,但配置复杂 | 高,需维护 Schema | 高 | 复杂数据聚合 |
| Next.js 全栈 API Routes | 中,接口和类型分离 | 低,前后端同仓 | 低 | 简单应用、SSR |
| t3code(T3 Stack) | 极强,端到端类型安全 | 低,函数直调 | 极低 | 个人工具、中小型全栈应用 |
这个选择的过程,本质上是在"开发效率"和"架构复杂度"之间做取舍。t3code 不需要开放 API 给第三方,不需要多客户端复用,前后端同仓就是最好的形态。
2. 从零初始化:create-t3-app 的配置细节与工程结构
初始化 t3code 的过程非常顺滑,官方提供了脚手架命令,直接在当前目录创建项目。
pnpm create t3-app@latest t3code脚手架会列出一堆可选项,我当时勾选了NextAuth、Prisma、Tailwind CSS、tRPC,没有勾 Drizzle(Prisma 生态更成熟,后面会展开说),Tailwind 必选,因为它是 T3 视觉风格的一部分。初始化完成后,目录结构已经帮你把依赖注入和主流程文件搭好了,不用自己从零配置 tsconfig、tailwind.config 和 trpc 客户端。
这里有必要解释一下为什么 T3 Stack 默认帮你做这些事却仍然很灵活。它的核心理念是"你只需要关注业务代码,基础设施已经按最佳实践预置"。比如app/api/trpc/[trpc]/route.ts这个文件,脚手架直接帮你写好了 Next.js 的 route handler,把 tRPC 的请求接入到 Next.js 的 App Router 上。server/auth.ts里帮你把 NextAuth 的配置文件和 tRPC 的上下文串起来,用户登录状态会自动注入到每个查询里。这种"约定优于配置"的做法,对个人项目非常友好。
第一次启动时的命令也很常规:
pnpm dev打开http://localhost:3000,看到的是 Tailwind 生成的默认首页。此时 t3code 还只是个空壳,真正的搭建工作才开始。
脚手架生成的目录结构,我建议认真理解几个核心文件,因为后续所有代码都是在这些文件上扩展的:
src/server/api/root.ts:tRPC 的路由聚合入口,所有子路由在这里合并src/server/api/routers/:实际业务路由目录src/server/db.ts:Prisma 客户端实例src/trpc/server.ts:服务端调用 tRPC 时的辅助函数src/trpc/react.tsx:前端 React 的 Provider 配置
做 t3code 这种 CRUD 工具时,最值得关注的就是root.ts和routers/目录,因为所有业务逻辑都在这里定义。我后续的计划是:为代码片段建一个snippetRouter,里面写查询和变更逻辑,前端通过api.snippet直接调用对应的函数,完全不需要写 HTTP 请求代码。
3. 数据层的骨架设计:Prisma 模型、关系设计与会话绑定
t3code 的第一个版本,数据模型非常简单:用户、代码片段、标签。但即使简单,模型设计也得把扩展性考虑进去,避免后期重构。
我在schema.prisma里先定义了三个核心模型:
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 description String? code String language String tags Tag[] userId String user User @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Tag { id String @id @default(cuid()) name String @unique snippets Snippet[] }随后定义了 Snippet 和 Tag 之间的多对多关系,中间表由 Prisma 自动生成。因为 t3code 是个人工具,所以每个片段都绑定到创建者用户,通过userId字段关联到 User 表。
实际写完后,执行迁移:
pnpm prisma migrate dev --name init这个命令会创建数据库表结构,并且自动生成 Prisma Client。选择 Prisma 而不是直接写 SQL 的原因,在于 t3code 中的查询逻辑几乎都是从 UI 层触发的,Prisma 的类型提示和自动补全能让我少踩很多字段名拼写错误的坑。
一个值得注意的细节:Tag模型中的name字段我加了@unique约束,这样同一个标签在数据库里只会有一条记录,避免标签重复导致界面混乱。如果你做的是一个面向多用户开放的项目,这个约束可能需要改成@unique([name, userId])(复合唯一),让不同用户的标签互不干扰。t3code 是单用户工具,所以我保留了全局唯一。
用户和片段的关系绑定,是 t3code 的一个关键点。因为在 T3 Stack 中,tRPC 的 context 里已经注入了会话信息,服务端可以直接拿到当前登录用户的 ID,然后把这个 ID 写入 Snippet 的userId字段。整个写入过程是类型安全的,不用担心传错用户 ID。
4. tRPC 路由的编写节奏:query 与 mutation 在实际业务中的分工
搞定数据模型,接下来就是 t3code 最核心的编码环节:tRPC 路由。如果你第一次接触 tRPC,可以先把它理解为"把后端逻辑从 HTTP 接口层提升到了函数调用层"。每个router里定义的.query相当于传统的GET请求,.mutation相当于POST/PUT/DELETE请求,但调用方式变成了api.snippet.getAll(),并且返回值会带上 TypeScript 类型推断,编辑器里能直接看到字段。
t3code 的业务路由分为两类。第一类是无副作用的查询操作,例如获取当前用户的片段列表:
getAll: protectedProcedure .input(z.object({ tag: z.string().optional() }).optional()) .query(async ({ ctx, input }) => { return ctx.db.snippet.findMany({ where: { userId: ctx.session.user.id, ...(input?.tag ? { tags: { some: { name: input.tag } } } : {}), }, orderBy: { updatedAt: 'desc' }, include: { tags: true }, }); }),这里的protectedProcedure是脚手架默认提供的一个高阶过程,它要求用户必须登录才能调用。如果未登录,tRPC 会直接抛异常,前端也会收到统一的错误提示。z.object来自 Zod,用于给输入参数做运行时校验。这个设计让我不用再写一堆 if/else 去判断参数是否合法,Zod 自动完成。
代码片段列表通常是个人笔记类应用的请求大头,这里没有做复杂的分页,直接用orderBy updatedAt倒序排列,对个人项目来说远没到性能瓶颈。后续如果片段量大了再考虑 cursor 分页。
第二类是 mutation,即创建、更新、删除。t3code 的创建片段是整个应用最常用的操作,我写了一个create路由:
create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), description: z.string().max(500).optional(), code: z.string().min(1), language: z.string().default('plaintext'), tags: z.array(z.string()).optional(), }) ) .mutation(async ({ ctx, input }) => { const snippet = await ctx.db.snippet.create({ data: { title: input.title, description: input.description, code: input.code, language: input.language, userId: ctx.session.user.id, tags: { connectOrCreate: (input.tags ?? []).map((name) => ({ where: { name }, create: { name }, })), }, }, include: { tags: true }, }); return snippet; }),这里最值得说的是connectOrCreate,它完美契合了标签这种"有则连接,无则创建"的业务场景。比如用户输入了一个新标签 "正则",如果标签表里没有这个名字,Prisma 会先创建这个标签,再把它和片段关联起来;如果已经有,就直接建立关联。这个操作在原生 SQL 里要写好几条语句,而 Prisma 用一条命令就搞定了。
更新和删除的写法大同小异,就不再贴代码了。核心思路是:所有带副作用的操作都放在 mutation 里,所有纯数据读取都放在 query 里,这个分工能让代码结构非常清晰,也方便后续加缓存策略。
4.1 服务端调用与前端调用的对称性
t3code 的前端调用方式,和服务端路由定义几乎是对称的。你不需要写任何 fetch 或者 axios 代码,直接用api.snippet.getAll这样的 Hook 就行。React Query 帮你处理了状态管理、loading、缓存和失效。这种开发体验,是我选择 T3 Stack 的最大理由。
前端调用代码很直白:
const { data, refetch } = api.snippet.getAll.useQuery({ tag: activeTag }); const createSnippet = api.snippet.create.useMutation({ onSuccess: () => { refetch(); }, });useQuery的结果会自动缓存,标签切换时如果查询参数变了,React Query 会重新拉取;useMutation的onSuccess回调里调用refetch,确保新片段创建后列表立即更新。整套链路下来,前端几乎不用关心数据请求的细节,业务状态都交给库去维护。
5. 界面层的 Tailwind 实践:从设计稿到组件落地的取舍
功能写完之后,最难啃的是界面。T3 Stack 自带 Tailwind CSS,好处是不用写单独的 CSS 文件,所有样式都放在 class 字符串里。坏处是如果设计系统不提前定好,写到后面 class 会越来越长,越来越乱。t3code 的定位是个人效率工具,所以我没追求花哨的视觉效果,核心目标是"一眼看清代码,三步内复制片段"。
5.1 布局设计与响应式
页面布局上,我用了 Tailwind 的 flex 和 grid 组合。整体分为三栏:左侧标签导航、中间片段列表、右侧详情预览。这个布局在桌面端体验最好,但手机端就需要重新调整。我用了 Tailwind 的响应式前缀来简化适配:
<div className="flex h-screen flex-col md:flex-row"> {/* 标签侧边栏 */} <aside className="hidden w-48 shrink-0 flex-col md:flex" /> {/* 列表区域 */} <main className="flex-1 overflow-y-auto" /> {/* 预览区域 */} <section className="flex-1 flex-col p-4 md:border-l" /> </div>hidden md:flex是手机端 Tailwind 布局里最常用的组合之一:默认隐藏标签栏,在桌面宽度才显示。这个改动比写 JavaScript 判断要优雅得多。
5.2 代码高亮与编辑体验
代码高亮是片段管理工具的脸面。t3code 最初用的react-syntax-highlighter,它在 Next.js 的 App Router 下有个坑:highlight 组件体积很大,打包时容易触发 Webpack 的性能警告。后来我换成了shiki搭配rehype-pretty-code。使用 shiki 时的基本姿势是:在服务端渲染时生成高亮后的 HTML,前端直接展示,既避免了客户端解析大依赖,又能支持很多现代主题。
适用 shiki 时,在服务端组件里这么写:
import { codeToHtml } from 'shiki'; const html = await codeToHtml(code, { lang: language, theme: 'github-dark-dimmed', });codeToHtml是同步 API,返回的结果是一段完整的 HTML,前端用dangerouslySetInnerHTML渲染即可。这里的取舍是:虽然牺牲了客户端动态高亮的能力,但极大提升了首屏速度,而且 t3code 的代码片段基本都是静态内容。
5.3 主题色与视觉细节
Tailwind 自定义主题色的姿势并不复杂。我在tailwind.config.ts里配置了自己的调色板,整体走 dark mode 风格,因为代码片段在浅色背景下高亮效果不够炫:
theme: { extend: { colors: { surface: '#1e1e2e', primary: '#cdd6f4', accent: '#f38ba8', muted: '#585b70', }, }, },配色这块没有用标准的前端库,而是参照了开源的 Catppuccin 配色方案,视觉上比较耐看。Tailwind 最大的优势就是"随手可调",改色板只需要改配置,界面全部实时生效。
6. 认证与权限:NextAuth 在 t3code 中的集成路径
t3code 虽然是个人工具,但我不希望任何人打开网址就能看到我的代码片段,所以登录验证是必须的。T3 Stack 脚手架默认集成了 NextAuth,支持 GitHub 第三方登录和 Credentials 登录。我优先选了 GitHub OAuth,原因很简单:个人项目的账号体系拿来即用,不用维护短信、邮箱、密码重置这一堆东西。
6.1 提供者配置
在server/auth.ts里配置 GitHub Provider:
import GitHub from 'next-auth/providers/github'; providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }), ],然后在 GitHub 的 Settings 里创建一个 OAuth App,回调地址填http://localhost:3000/api/auth/callback/github(生产环境换成自己的域名),拿到 Client ID 和 Secret,写进.env文件。
这个环节有个很容易踩的坑:回调地址写错,登录时会报 redirect_uri 不匹配。我之前在本地跑通后部署到 Vercel,忘了把回调地址从 localhost 改成线上域名,卡了整整一小时。建议在 GitHub OAuth App 配置多个回调地址,本地和线上分开,并逐个验证。
6.2 受保护路由与 tRPC 的联动
T3 Stack 里,NextAuth 的会话和 tRPC 的上下文是打通的。只要用户在 GitHub 登录成功后,ctx.session.user就是真实的用户对象。在 tRPC 路由里用protectedProcedure替代publicProcedure,就可以直接拦截未登录用户,让未登录请求要么被重定向到登录页,要么被返回错误。
我还做了一层额外的校验:由于 t3code 的片段都属于个人用户,即使登录用户不是本人,也不能访问别人的片段。所以在getAll查询里显式加上userId: ctx.session.user.id过滤。这个做法比依靠路由隐藏更安全,也不容易因为前端跳错了路由而泄露数据。
7. 部署到生产环境的几个关键卡片:Vercel 与数据库托管
t3code 的部署我选了 Vercel + PlanetScale(MySQL),但使用过程中,PlanetScale 后来调整了服务策略,所以如果你现在要部署,我更建议直接考虑:
- Vercel:部署 Next.js 应用,和 GitHub 仓库直接关联,push 后自动构建上线,自带 HTTPS 和域名绑定。
- Supabase:基于 Postgres,兼容 Prisma,免费额度对个人项目足够,而且自带 Storage 可以存文件。
- Neon:无服务器 Postgres,自动暂停和扩展,冷启动略有延迟,但个人项目无碍。
我在部署时踩过一个和 Prisma 相关的重要坑:Vercel 的无服务器环境默认不允许保持数据库连接。如果你的 Prisma 客户端每次都新建连接,很快会打满数据库连接数。解决办法是:在src/server/db.ts里给 Prisma 客户端加上全局单例缓存。
import { PrismaClient } from '@prisma/client'; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; export const db = globalForPrisma.prisma ?? new PrismaClient({ log: ['query', 'error'], }); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;这段代码的原理是利用 Node.js 的globalThis对象做缓存:生产环境中,每个 Serverless 函数实例复用同一个 PrismaClient 实例,避免重复创建;开发环境则每次都创建新的,方便热更新。没有这个缓存,你的数据库连接池会莫名其妙地被 Vercel 断开,然后在日志里看到一排PrismaClientInitializationError。
部署完还要记得设置环境变量。Vercel 的 Project Settings 里,把DATABASE_URL(数据库连接串)和GITHUB_CLIENT_ID、GITHUB_CLIENT_SECRET全部配置好。这个步骤很容易被忽略,一旦漏配,线上应用会在启动时立刻抛错。
8. 踩坑记录:我在 t3code 开发中遇到的实际问题
工具类的项目,开发过程中最大的成本往往不是业务逻辑本身,而是那些"看起来不复杂但极其消耗时间"的环境问题。我完整跑完 t3code 之后,整理了三个最典型的坑。
8.1 Prisma 和 Next.js 热更新的兼容性问题
开发时,如果改了schema.prisma并运行prisma migrate,在 Next.js dev server 还开着的情况下,会发生模型缓存没有完全更新、页面白屏或运行时异常。解决办法是每次跑完 migrate 后手动刷新页面,仍然出问题就直接重启 dev server。这属于踩过才知道的坑,官方文档提到过但描述不详细,开发时容易误以为是代码逻辑 bug。
8.2 NextAuth 在 App Router 下的 session 检查
t3code 的页面我有两种渲染模式:列表页用服务端组件,方便首屏直接拿到数据;表单页用客户端组件,交互更灵活。但在 App Router 下,你没法在服务端组件里直接调用useSessionHook,只能改用server/auth.ts里导出的getServerSession:
import { getServerSession } from 'next-auth'; import { authOptions } from '~/server/auth'; const session = await getServerSession(authOptions);如果不在服务端组件里认真判断 session,某些页面会闪烁登录后内容,很影响体验。T3 Stack 的脚手架虽然预置了getServerSession的封装,但最佳实践是:服务端组件负责拿 session 做渲染决定,客户端组件负责交互,不要再在全局布局里混用。
8.3 Tailwind 的 class 扫描范围
Tailwind 在 t3code 中默认扫描src/**/*.{ts,tsx},如果你把某些样式写在更外层目录,比如放在lib/里动态拼接 class,Tailwind 默认不会生成对应的 CSS,界面会突然没有样式。这种 bug 的排查成本极高,因为 look like 是"样式没生效",实际是"Tailwind 根本没把你那个 class 编译进产物"。把需要静态存在的知名 class 全部放到content配置里能避免这种问题。
content: ['./src/**/*.{ts,tsx}'],另外,t3code 的所有片段数据没有做服务端搜索,后续如果要做全文检索,可以考虑加 Postgres 的全文索引,或者直接用 Supabase 的向量检索做语义搜索。那是另一个阶段的事了,目前先保持简单。
9. 从开发到使用:t3code 的日常体验与后续路线
t3code 从代码结构设计到所有功能落地,我前后用了不到一个周末的时间。核心工作集中在 tRPC 路由、Prisma 模型、前端 React 组件三个部分。真正让开发速度提升的部分,是 T3 Stack 把类型安全和数据层基础设施全都铺好了,我只用关心业务逻辑本身。相比以前做前端项目时要花大量时间联调接口、排查字段名,这种"全栈类型安全"的体验是一种结构性提速。
日常使用的体验是:我在浏览器打开 t3code,左侧选标签,中间扫列表,右侧直接复制代码。新增片段时用表单填写标题、粘贴代码、输入标签名,提交后列表自动刷新。整个过程不超过十秒。数据库里已经存了上百条常用的正则和 Shell 命令,再也没出现过"找到一条好命令但忘了存哪"的情况。
如果接下来继续迭代,我会考虑这几个方向:给代码片段加星标收藏、支持按语言筛选、做站内全文搜索。搜索是一个比较核心的功能,等数据量上来后再补也不迟。目前的 t3code 已经很契合我的个人使用习惯,这也是做个人工具最爽的一点:不用迁就需求文档,自己就是产品经理。
最后再分享一个小经验:如果你也想做一个类似的个人工具,不要纠结于技术栈是不是"最新最热"。T3 Stack 的好处在于它把所有东西收敛成了一门手艺,TypeScript 从后端到前端全程护航,Prisma 管数据,Tailwind 管样式,tRPC 管接口。你唯一要做的就是把业务逻辑写好,别的交给框架。这种"带着镣铐跳舞"的开发体验,反而比一上来就自己拼一套前后端分离的脚手架要高效得多。