
T3code这个项目名一开始只是个临时代号后来用着用着就不想换了。事情起因很简单我平时写代码积累了大量片段散落在本地笔记、VS Code的代码片段文件、GitHub Gist、甚至微信收藏里想找的时候要么翻半天要么干脆重新写一遍。于是我用 T3 Stack 做了个轻量 Web 应用专门解决代码片段的统一管理问题。项目不大但把 T3 Stack 的核心组件完整跑了一遍这篇文章就把整个从选型、建模、实现到部署的过程以及我踩过的坑详细拆给你看。如果你想快速了解 Next.js、TypeScript、tRPC、Prisma、Tailwind CSS 这一套组合怎么做实际项目或者你手头正想写一个内部小工具但不确定技术选型这篇应该能给你不少参考。整个项目所有代码都是我自己一行行写的没有套后台模板所以谈到的问题都是真实会遇到的问题不是文档里的理想流程。1. 整体设计与技术选型1.1 先想清楚这个工具到底要解决什么问题写代码久了的人都会有一个体会代码片段这种东西很“轻”但收集渠道特别散。最常见的几种存放方式各有各的问题本地仓库或者 wiki适合长文档但不适合快速录入和检索。VS Code 内置 snippets只绑定在当前编辑器里换个环境就得重新配置。GitHub Gist分享方便但私有 gist 要登录才看得到标签功能也比较弱。浏览器书签能存链接不能存代码本身。笔记软件能存文字但代码高亮、格式化、快速复制都不顺手。我需要的其实是这几个能力能快速粘贴一段代码、能打多个标签、能全文搜索、能一键复制、能生成一个公开链接发给同事。更进一步我希望这个工具本身足够轻打开页面输入几个字就能用不需要复杂的后台。所以 t3code 的定位就出来了一个面向开发者个人使用的代码片段管理工具支持标签分类、全文检索、语法高亮和链接分享。它不追求做成本地 IDE 那样的重型工具也不想做成社交平台就是尽量把一个具体的、我自己每天都会用到的场景做到顺手。1.2 为什么是 T3 Stack而不是自己拼一套T3 Stack 严格来说不是一个框架而是一组约定好的技术组合。它的核心是 Next.js TypeScript再配上 tRPC、Prisma、Tailwind CSS 和 NextAuth.js。我选择它而不是自己手动拼一套脚手架主要有三个原因。第一类型安全是端到端的。以前做全栈项目前后端之间靠 REST API 加手写类型接口稍微一多前后端类型就很容易脱节。tRPC 的模型是你直接在后端定义一个 procedure前端调用的类型自动推导出来这意味着我改一个字段类型编辑器立刻能在前端代码里标红提示。对于 t3code 这样频繁改动的个人项目这个优势太值钱了。第二约定大于配置省掉大量决策成本。我前些年做项目喜欢每个环节都自定义最后光配置就花了好几天。T3 Stack 把数据库 ORM、API 方式、认证方案、样式方案都替你想好了组合方式经过社区验证踩坑概率低。对个人项目来说能在拿到代码后半小时内跑起来比什么都重要。第三生态成熟后续扩展空间大。Next.js 可以做 SSR 也可以做静态导出Prisma 能适配 SQLite、PostgreSQL、MySQL 多种数据库NextAuth.js 支持的登录方式很多。这意味着如果 t3code 以后要加团队功能、部署到内网服务器基本不用推翻重构。当然T3 Stack 不是没有代价。它对 TypeScript 的依赖很深如果你不习惯类型推导或者团队里大多数人只是后端出身学习成本会偏高。而且在纯单人项目里tRPC 带来的类型安全可能感受不明显反而会让人困惑“为什么不用一个简单 fetch”。我的看法是如果你想长期维护一个项目类型安全带来的收益会在改需求的时候集中体现。1.3 功能边界哪些做哪些坚决不做很多类似项目最后臃肿起来是因为什么都想做。我在设计 t3code 时给自己定了几个边界。第一不做多人在线协作。个人工具就是要快、要私密协作会导致权限体系、邀请机制、实时同步等一系列复杂度这不是当前阶段需要的东西。第二不做积木式的“分享动态”。分享可以是一个简单的公开只读链接访问者能看代码、能复制代码但不能编辑。第三不做重型的项目管理系统。片段就是片段不需要关联 Issue不需要燃气不需要流水线。确定边界的一个好处是数据模型会很干净。t3code 的核心对象只有 snippet围绕它的就是标签、用户、分享记录没有一堆互相牵扯的业务对象。实际上如果你准备做类似工具我建议你也把“不做什么”写下来哪怕只在 README 里写三行字需求蔓延的时候真的有用。秉着这个设计思路我最终的技术选型清单是这样的模块选型理由前端框架Next.js 14App RouterSSR、API、前端一体部署简单语言TypeScript全栈统一类型和 tRPC 是绝配API 层tRPC端到端类型安全写 CRUD 极快ORMPrisma数据库 schema 定义清晰迁移方便数据库SQLite / PostgreSQL可选单机开发用 SQLite部署可切 PG样式Tailwind CSS写页面速度快主题定制容易认证NextAuth.js支持 GitHub OAuth无需自己存密码校验Zod与 tRPC 配合输入输出校验统一2. 核心细节解析与实操要点2.1 数据模型设计一张 snippet 表还是多张关系表代码片段管理工具的数据模型其实比看起来要烦。一些片段可能对应多种编程语言也可能一个片段有多个标签。如果简单地做成”snippet 表里加一个 tags 字段“看起来方便后面做标签筛选和关联统计时就会很痛苦。我采用的多对多关系包含三张核心表snippet片段本体存储标题、语言、代码、说明。tag标签每个标签有独立的 name 和 slug。snippet_tag关联表维护片段和标签的多对多关系。Prisma schema 的关键部分可以这么写model Snippet { id String id default(cuid()) title String language String default(plaintext) code String note String? isPublic Boolean default(false) userId String user User relation(fields: [userId], references: [id]) tags SnippetTag[] createdAt DateTime default(now()) updatedAt DateTime updatedAt index([userId, language]) } model Tag { id String id default(cuid()) name String slug String unique snippets SnippetTag[] } model SnippetTag { snippetId String tagId String snippet Snippet relation(fields: [snippetId], references: [id], onDelete: Cascade) tag Tag relation(fields: [tagId], references: [id], onDelete: Cascade) id([snippetId, tagId]) }这里有几个容易忽略的细节我把language字段做成了字符串而不是枚举原因是前端代码高亮库支持的语言列表经常变化硬编码枚举反而每次都要迁移数据库。isPublic是一个简化设计。公开链接本身是只读的所以不需要再做复杂的访问记录表。关联表两个外键都设置了onDelete: Cascade否则删除 tag 时会经常报约束错误。检索性能上如果片段数量到十万级用 SQLite 默认的LIKE %keyword%会开始变慢。到时候可以给code字段上FTS5或者直接切换 PostgreSQL 用 pg_trgm 索引。前期完全没有必要过度设计。2.2 tRPC 路由设计把 CRUD 写得像在写函数调用t3code 的 API 层完整走了 tRPC 的流程。tRPC 和传统 REST API 最大的区别在于你不需要单独定义 URL、请求方法、请求体结构。后端定义一个查询前端像调用本地函数一样调用它类型自动同步。我按资源划分路由核心路由长这样// server/api/routers/snippet.ts import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; const SnippetInput z.object({ title: z.string().min(1, 标题不能为空).max(100), language: z.string().default(plaintext), code: z.string().min(1, 代码内容不能为空), note: z.string().max(5000).optional(), tagNames: z.array(z.string().max(50)).optional(), isPublic: z.boolean().default(false), }); export const snippetRouter createTRPCRouter({ list: protectedProcedure .input(z.object({ keyword: z.string().optional(), tag: z.string().optional() })) .query(async ({ ctx, input }) { const where buildWhereClause(ctx.session.user.id, input.keyword, input.tag); return ctx.prisma.snippet.findMany({ where, include: { tags: { include: { tag: true } } }, orderBy: { updatedAt: desc }, }); }), create: protectedProcedure .input(SnippetInput) .mutation(async ({ ctx, input }) { return ctx.db.$transaction(async (tx) { const snippet await tx.snippet.create({ data: { title: input.title, language: input.language, code: input.code, note: input.note, isPublic: input.isPublic, userId: ctx.session.user.id, }, }); const tagNames [...new Set((input.tagNames ?? []).map((t) t.trim().toLowerCase()))]; for (const name of tagNames) { if (!name) continue; const slug slugify(name); const tag await tx.tag.upsert({ where: { slug }, update: {}, create: { name, slug }, }); await tx.snippetTag.create({ data: { snippetId: snippet.id, tagId: tag.id } }); } return snippet; }); }), delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const snippet await ctx.prisma.snippet.findUnique({ where: { id: input.id } }); if (!snippet || snippet.userId ! ctx.session.user.id) { throw new TRPCError({ code: FORBIDDEN }); } return ctx.prisma.snippet.delete({ where: { id: input.id } }); }), });这里最值得说的是购物车问题。一开始我把标签处理写成了多个独立 await后来发现如果前端传入的 tagNames 里有重复或者空字符串会出现一半标签创建成一半失败的情况。改成$transaction之后要么全部成功要么全部回滚数据一致性明显提升。权限控制也应该放在路由层而不是页面层。页面隐藏按钮只是用户体验真正的权限校验必须体现在 procedure 里。比如delete这个 mutation 里我只允许删除userId等于当前登录用户的记录。部署后你会感谢当初在每个接口都加上了这层判断。2.3 认证与授权NextAuth 和 OAuth 的接入细节t3code 使用的是 NextAuth.js并且默认接入了 GitHub OAuth。之所以没有自己做账号密码体系很简单个人工具再维护一套密码重置、邮箱验证、验证码逻辑是纯粹的负担。用 NextAuth 之后用户体系的核心工作变成了配置 OAuth App 和读取 session 用户 ID。配置 auth 时有几点需要特别注意环境变量里的AUTH_SECRET不生成好开发环境会出现间歇性登录出错。可以用openssl rand -base64 32生成一个稳定值。GitHub OAuth App 的 callback URL 必须写到http://localhost:3000/api/auth/callback/github或者部署后的真实域名路径填写不对会导致登录后跳转异常。NextAuth 的 session 数据里默认只带 id、email、name如果要存更多用户字段记得在callbacks.session里手动扩展。我并不是只允许 GitHub 账号登录。因为 t3code 是我身边同事也在用的工具我后续还加了邮箱登录选项用 Resend 发验证链接。这一项扩展在 NextAuth 里也就是加一个 providers 数组元素的成本但收益是同事即使没有 GitHub 账号也能用。登录之后的流程很简单所有需要操作自己数据的地方都用protectedProcedure它会自动从 session 里取出用户 id。登录用户最多只能看到自己创建的片段。公开链接走publicProcedure直接按 snippetId 查数据不要求登录。2.4 前端交互代码高亮、搜索框和数据加载t3code 前端没有用特别复杂的状态管理库。这个项目的状态分两类服务器端数据和本地 UI 状态。服务器端数据完全由 tRPC 的 query 管理UI 状态只有弹窗开关、编辑是否打开等本地 small state用 React 自带的useState就够了。代码高亮我一开始在react-syntax-highlighter和shiki之间纠结过。前者的渲染体积比较大首屏加载大量语言包时明显变慢后者虽然打包配置麻烦一点但基于 TextMate 语法高亮效果和我 VS Code 里看到的几乎一致。我最终选了 shiki并用动态导入的方式按需加载语言包import { createHighlighter } from shiki; let highlighter: AwaitedReturnTypetypeof createHighlighter; export async function getHighlighter() { if (!highlighter) { highlighter await createHighlighter({ themes: [github-light, github-dark], langs: [javascript, typescript, python, go, sql, bash, json], }); } return highlighter; }代码高亮服务端渲染有个好处页面源码里直接包含高亮后的 HTML首屏就能看到完整样式不会像客户端渲染那样闪一下。代价是 SSR 的时候 CPU 会多花一点时间。对 snippet 这种界别的内容量来说代价完全可以忽略。如果你做更重的富文本渲染要考虑给高速缓存加应用层缓存我当时直接跳过了这一步。搜索框的实现我用的是 tRPC query 里带keyword参数配合useDeferredValue做防抖。具体做法是 React 部分const [keyword, setKeyword] useState(); const deferredKeyword useDeferredValue(keyword); const { data: snippets } api.snippet.list.useQuery({ keyword: deferredKeyword, tag: selectedTag ?? undefined, });这样做的好处是输入框更新时不会立刻触发请求React 会在渲染空闲的时候再发起请求用户感知到的卡顿明显减少。搜索逻辑后端通过 Prisma 的contains实现const where: Prisma.SnippetWhereInput { userId: userId, ...(tag ? { tags: { some: { tag: { slug: tag } } } } : {}), ...(keyword ? { OR: [ { title: { contains: keyword } }, { code: { contains: keyword } }, { note: { contains: keyword } }, ], } : {}), };关键词多表查询的 OR 条件在 SQLite 下可以正常工作数据量大了以后还是建议换成专门的全文搜索引擎但对个人工具来说完全够用。2.5 表单校验与状态管理Zod 一处定义到处复用表单校验我没有用传统的前后端两套校验。Zod 和 tRPC 配合使用后在路由层定义的SnippetInput可以同时用于后端校验也可以被前端表单库消费。t3code 用的表单库是 React Hook FormZod 作为 resolver这样改一次校验规则所有调用方同步生效不可能出现前后端校验不一致的 bug。实际开发里我最常碰见的表单问题反而是“标签输入框”的处理。用户输入React, Next.js,全栈这样的字符串要拆分成数组、去掉重复、去掉首尾空格。这里面有个坑是全角逗号和中文括号所以我在前端做了统一的清洗函数再交给 tRPC 路由端做二次清洗。双保险function parseTags(raw: string): string[] { return raw .split(/[,;]/) .map((t) t.trim().toLowerCase()) .filter(Boolean); }另外我必须提醒一句即时后端有 Zod 做了校验前端也一定要控制提交按钮的 loading 状态否则用户在弱网环境连点三次提交会创建出三个重复片段。虽然这次我会 utils 触发 revalidate刷新后只显示一个但数据表里垃圾数据已经写进去了。3. 实操过程与核心环节实现3.1 用 create-t3-app 初始化项目骨架T3 Stack 官方脚手架是create-t3-app执行命令以后可以选择是否引入 Prisma、NextAuth、Tailwind、tRPC。我建议第一次做就直接全选省得后期补配置补起来往往会动到不少文件。npx create-t3-applatest t3code cd t3code npm run dev初始化之后目录里会有一套很干净的约定结构。短期内你只需要关注几个目录src/server/api/routers/按资源拆分 tRPC 路由。src/server/db.ts统一导出 Prisma client。src/app/App Router 下面的页面和 API 入口。src/trpc/给 React 用的 provider 和 hooks。首次跑起来后我建议先不要改任何业务代码而是把 prisma 的 schema 改成自己要用的数据模型然后跑迁移。跑迁移之前保证DATABASE_URL环境变量已经配置好本地开发我直接用的是 SQLite。3.2 写好数据库迁移脚本和种子数据prisma schema 改完执行迁移npx prisma migrate dev --name init_snippet_tag npx prisma studio这段命令会生成一张迁移记录表让数据库状态和 schema 保持同步。开发阶段我基本每天都在跑迁移如果你在做自己的项目也记得每次修改 schema 之后都要第一时间迁移否则前后端类型一会儿对不上一会儿又对上排查起来特别耗费时间。种子数据我建议随手写一点点。手动在页面里点击创建测试数据当然也行但写一个prisma/seed.ts能让你反复删除库之后快速恢复数据npx prisma db seed种子数据里不要全部是模板片段我特意加了几条中文标签的测试数据比如“全栈”、“踩坑记录”、“面试题”这样能顺便测试搜索框的全文检索效果。3.3 实现片段列表和详情页t3code 的主页面由三个部分组成左侧是标签筛选栏中间是片段列表右侧是详情编辑器。片段列表的数据直接从 tRPC 查出来之后用 Tailwind 做响应式卡片布局。卡片上显示标题、一段代码预览、语言标签、更新时间。预览代码我用line-clamp限制只显示前几行如果全都渲染出来列表会非常长页面滚动体验很不好。详情页是项目里样式最复杂的部分。输入标题我用普通的 input代码区我用 textarea 加等宽字体预览高亮区用 shiki 生成 HTML。你会发现编辑时和预览时如果字号、行高不一致光标会错位复制代码也会出问题。所以我已经固定了font-mono text-sm leading-relaxed作为代码区域的统一样式。这里顺便分享一个细节代码复制按钮最好直接用navigator.clipboard.writeText。复制code字段的原始内容是靠谱的但是如果你复制的是“高亮后的 HTML”就会把标签也复制进剪贴板。我实际测下来大多数人对这个细节是不敏感的直到他粘贴到 Markdown 文件里才发现问题。3.4 页面鉴权和公开分享实现私密片段需要在页面层判断是否登录。因为 t3code 本身就是需要登录才能使用的工具我将整个主应用都放在了一个需要 session 的布局下本地实现得相对粗暴直接在根布局里判断 session未登录则渲染一个登录按钮。公开链接的路径是/s/{id}对应的页面不要求登录。查询数据时要注意两点必须有isPublic: true条件否则任何人猜出 id 就能读到私密片段这个是最容易漏掉的隐患。公开页面的分享信息只能暴露片段内容不要返回 userId、updatedAt 之外的多余字段。虽然你只是在拼一个个人项目但安全习惯应该从第一行代码养成。3.5 部署到服务器Prisma 迁移和环境变量管理部署我选了 Vercel 上的 Next.js数据库用的是云上 PostgreSQL。整体配置文件里其实没有太多额外的代码要注意的就是环境变量和部署钩子。在 Vercel 的 Project Settings 里我配了几个环境变量DATABASE_URL指向远程 PostgreSQL。AUTH_SECRET登录密钥必须在生产环境重新生成。AUTH_GITHUB_ID/AUTH_GITHUB_SECRETGitHub OAuth 的密钥。NEXTAUTH_URL生产环境下必须是公开访问的域名不能带 http://localhost。数据库迁移方面我建议在部署前手动在本地对远程数据执行一次npx prisma migrate deploy如果忘了这一步应用部署成功但数据库里没有表页面会直接抛PrismaClientInitializationError。这种错误定位起来很快但如果你第一次接触会在 Vercel 日志里反复检查代码白白浪费时间。4. 常见问题与排查技巧实录4.1 tRPC 类型不同步改了后端前端不认这个问题的频率出乎我的意外。前端写调用时已经在对象里看见新字段可是编辑或者 build 的时候仍然报错。实际上原因多半是 tRPC 的类型推倒是延迟的或者你的 Next.js 正在用缓存。排查步骤检查src/server/api/root.ts是不是已经 export 了新加的 router。检查src/trpc/server.ts里的AppRouter类型有没有被重新加载建议杀掉整个 dev server 再npm run dev。确认是在路由文件里返回了新的字段而不要把字段写在 Prisma select 里却忘在 tRPC 输出层。如果你改完类型之后前端页面没有报错但数据就是不出来多半是 React Query 的缓存问题。可以在 query 里加一个trpc-react-query返回的utils.invalidate或者手动指定refetchOnWindowFocus: true。4.2 Prisma client 在本地和线上行为不一致最典型的例子是cuid()和uuid()。SQLite 本地生成的 id 是字符串PostgreSQL 线上如果用cuid()也支持。但如果你把 schema 里改成default(uuid())并且数据库是 SQLite本地跑没问题部署到 PostgreSQL反而报错因为 PostgreSQL 默认没有生成 UUID 的函数除非你有插件。我建议固定使用cuid()避免这种跨平台差异。另一个常见的坑是连接数。用 Serverless 部署到 Vercel 时Prisma 每次冷启动都创建新连接而云数据库连接池有上限。超过以后日志里会频繁出现P2024错误。解决办法有两个要么直接使用支持连接池的连接字符串比如 Supabase 或 Neon 给的?pgbouncertrue后缀要么就别把所有页面都做成动态渲染尽量让 Next.js 缓存静态部分。4.3 NextAuth 登录后回跳地址错误我在测试 GitHub 登录时踩过一个诡异问题登录后地址栏变成了http://localhost:3000/api/auth/callback/github然后白屏。排查后发现是我在 OAuth App 里填的 callback URL 带了额外路径和 NextAuth 默认的路径不匹配。NextAuth 的 callback 路径是固定的/api/auth/callback/{provider}如果你用自定义前缀必须同步修改配置否则登录流程会在最后一步死掉。这类问题看日志不一定直观最好的方式是打开浏览器 Network 面板看最后一次重定向到哪个地址然后对比环境变量里的NEXTAUTH_URL是否和实际访问一致。4.4 代码高亮首屏渲染慢如果你按我的方案用 shiki第一次请求时会明显感觉到页面渲染变慢因为要异步加载语言包和主题。解决思路是缓存 highlighter 实例已经在上文提到了。另外shiki 支持shikijs/rehype在 Markdown 解析阶段直接做高亮如果你的片段说明带 Markdown可以统一交给 rehype 处理避免前言不搭后语。前端预览代码的行数如果特别多渲染上万行的语法高亮也会卡顿。我给代码预览区加了一个最大高度和滚动条同时用useMemo缓存高亮结果实际上 shiki 本身没有 useMemo但你可以通过memo(CodeBlock)来避免无谓的重新渲染。4.5 标签筛选、URL 同步和浏览器返回到问题我一开始的标签筛选只是存在 useState 里后来发现一个问题用户选中了一个标签刷新页面还处在选中状态在个人工具里可能无所谓但当你把公开链接分享出去时看到的标签状态完全不一样就奇怪了。我后来把标签筛选同步到了 URL query 上比如/ ?tagtypescript。这样刷新不丢失分享链接也能直接定位到标签维度。要注意的是和router.push同时更新查询参数和列表数据时不需要重新触发 query只要把tag作为 query 的输入参数React Query 会自动处理。4.6 数据一致性和 XSS 隐患代码片段工具里不能只想着高亮还要警惕 XSS。虽然我们用 textarea 收集代码但代码最终会被渲染成 HTML。我强烈建议在服务端对代码内容做一下转义处理或者在渲染时使用专门的高亮库它会对代码里的特殊字符做 escape。短期内你可能只是给自己用但如果以后开放注册这种问题会自动变成安全漏洞。同样地公开链接的title标题也要做编码。我在早期版本遇到过一个问题用户给片段起了一个包含/title的标题整个页面的标题直接被截断。后来我统一对标题做转义再用 React 的普通文本渲染这个问题才彻底消失。5. 后端扩展方向与个人体会5.1 下一步准备加的功能t3code 当前版本已经满足我自己的日常需求后面确实也想过几个比较有价值的方向。一个是浏览器插件。大家遇到代码片段时往往是在浏览文档、看博客的时候这时候如果有个浏览器插件可以直接右键收藏代码录入成本会低很多。插件通过 OAuth 拿到 token调用的还是 t3code 现有的 tRPC 接口。另一个是 AI 辅助。当前的片段检索虽然支持全文搜索但是对“模糊记忆”比较弱比如记得一段正则大概效果是什么却说不出关键词。接入语义搜索或者直接用 AI 给片段生成摘要搜索体验能提升一个档次。如果要做我大概率还会用 tRPC 包一层 AI 服务保持类型安全。还有一个是离线模式。虽然基本没人会在没有网络的时候搜索代码片段但如果我们把常用标签的前 50 条缓存到 localStorage确实能让工具体验更接近原生应用。Next.js 的 PWA 方案我也在关注只是还没动手。5.2 做到现在我建议你也关注一下这些事如果你准备模仿 t3code 做类似的项目我给你几个实操层面的建议都是我自己反复折腾后总结出来的。数据库从 SQLite 切到 PostgreSQL 要趁早。我自己前期图省事一直用 SQLite后面为了部署到多家云厂商改成 PostgreSQL 时遇到了几个类型相关的小问题。如果你预计项目会部署到公网直接用 PostgreSQL 起步可能会更顺。日志和监控不要等到用户反馈问题才开始配。tRPC 本身有中间件机制我建议在根 router 上挂一个轻量日志中间件每次请求记录一下耗时和路径。个人项目也不用接很重的监控系统Vercel 自带的日志面板对我来说就够用了。还有一点比较主观但我觉得很重要自己造的工具一定要“常用”。很多人做完内部工具就不再维护了因为总觉得运行在别人的产品里。其实像 t3code 这种个人工具只要你能做到每天打开、每周处理几条新片段就说明这个项目真的在给你创造价值。我目前就是把它固定在了浏览器新标签页的常用站点里慢慢用顺手了。5.3 说说我对 T3 Stack 这套东西的看法这个项目做完我个人对 T3 Stack 的好感度明显提升了。它并不是让你少写代码而是帮你在代码规模变大之后仍然能保持一个清晰的结构。类型安全的传递效果是最值得称道的尤其是你改了 Prisma schema 之后前端、后端、校验层几乎全都同步更新让重构的恐惧感小了很多。不过我也要说点实话。T3 Stack 对项目规范的约束是双刃剑。如果你希望项目有非常特殊的架构或者团队习惯用 Redux、自定义验证体系那这套组合反而会有点“较劲”。我自己做个人项目的时候喜欢拿它作为默认配置因为它已经帮我把大部分“无聊决策”做了。最后分享一个我刚开始写 t3code 时没预料到的小技巧在 tRPC 路由文件里你可以直接使用ctx.db而不必每次都从ctx.prisma重新解构。这个看似很小的差异在写大量的时候能省掉很多重复代码。祝你也早点把自己的常用工具做成理想的样子。