1. 为什么是 T3 Stack:t3code 这个名字背后的选型逻辑
t3code 是我最近把业余时间几乎全砸进去的一个全栈项目,定位是个人代码片段和开发笔记管理工具。名字很好拆:T3 指 T3 Stack,后面的 code 是我给这个项目定的核心场景——把平时散落在本地文件、聊天记录、各种临时仓库里的代码片段统一收进一个带标签、可搜索、有登录权限的地方。选这套技术组合不是因为追新,而是我手头正好有这么一个真实需求,与其继续在"找不到自己写过代码"这件事上浪费时间,不如直接做一个工具,顺便把 T3 Stack 这条链路完整跑一遍。
这篇文章适合两类人。第一类是听过 T3 Stack 但还没动手试过的全栈开发者,想看看这套组合在真实项目里到底顺不顺手;第二类是已经在用 Next.js,但前后端类型边界一直靠手写接口文档或复制粘贴类型定义来维持的开发者,想换个更省事的思路。我会把 t3code 从初始化、建模、写接口、做登录、写页面到部署上线的完整过程摊开讲,重点说清楚每一步为什么这么选、实际操作中踩了哪些坑,以及哪些地方和官方文档里写的不太一样。
1.1 T3 Stack 到底是什么
先给还不清楚概念的读者补个底。T3 Stack 不是一门新语言,也不是某个单独框架,它是一套以 Next.js 为主体的全栈 TypeScript 组合方案,核心成员是 Next.js、TypeScript、Tailwind CSS、tRPC、Prisma,按需搭配 NextAuth(现在叫 Auth.js)。这套方案由 t3.gg 社区推动,核心理念概括成一句话就是"typesafe by default"——尽量让类型错误在编译期就暴露,而不是等到线上接口返回一个结构对不上的 JSON 才知道出了问题。
T3 Stack 里每个成员干的事很明确:Next.js 负责页面渲染和路由,TypeScript 负责全链路类型约束,Prisma 负责数据库建模和查询,tRPC 负责前后端接口的类型安全调用,Tailwind 负责写样式,NextAuth 负责登录认证。这个组合最打动我的地方在于:它把"前端定义一个类型、后端又手写一份同样的类型、中间再维护一份文档"这种重复劳动直接干掉了。你只需要在服务端定义一次路由和输入输出结构,前端拿到的是完全推导出来的类型,改一处,另一端跟着变。
我这几年做项目的体感是:大部分全栈项目的时间不是花在业务逻辑上,而是花在"前端不知道后端返回什么结构"的沟通成本上。t3code 选 T3 Stack,本质上就是想把这部分成本压到最低。
1.2 create-t3-app 初始化时的选项怎么勾
初始化 T3 Stack 项目很简单,一条命令:
npm create t3-app@latest命令跑起来之后会有几个交互选项:项目名、包管理器、是否使用 Tailwind CSS、Prisma、tRPC、NextAuth,以及选择 App Router 还是 Pages Router。我第一次看到这个界面的时候愣了一下,心想这也太简洁了,和以前搭建 React 项目时手动配 webpack、babel、eslint 的体验完全两个世界。
t3code 的选项我是这样勾的:
| 选项 | 我的选择 | 理由 |
|---|---|---|
| TypeScript | 强制启用 | 没有 TypeScript 就没有 T3 的意义 |
| Tailwind CSS | 启用 | 标签排版、列表布局用工具类非常顺手 |
| Prisma | 启用 | 我需要关系型数据建模,多对多的标签关系靠它省事 |
| tRPC | 启用 | 本项目的核心选型,前后端类型打通 |
| NextAuth | 启用 | 登录权限是刚需,内部工具也要区分用户 |
| App Router | 选择 App Router | 新版默认方向,Server Components 对列表页性能有帮助 |
这里要提醒一句:ESLint 选项建议直接启用。create-t3-app 内置的 ESLint 配置里有一条专门针对 TypeScript 的规则,会把任何any类型标红。开发的时候觉得烦,但事后回头看,正是这条规则逼着我把所有接口边界都写清楚了。顺带一提,初始化完成后它生成的.env里会有空的数据库地址和认证密钥,不补全这些变量,项目是跑不起来的。
1.3 生成的目录结构里,哪些文件是主角
create-t3-app 生成的项目目录比大多数脚手架都要克制,我列几个真正的主角文件:
src/app/api/trpc/[trpc]/route.ts:tRPC 和 Next.js 桥接的入口,接收所有 API 请求并转发给 tRPC 的 appRouter 处理。src/server/api/root.ts:汇总所有子路由,导出唯一的 appRouter。src/server/api/trpc.ts:tRPC 实例和上下文(context)创建逻辑所在。src/server/auth.ts:NextAuth 的完整配置。src/server/db.ts:Prisma Client 实例,里面用了全局缓存技巧防止服务端热更新时连接泄漏。prisma/schema.prisma:数据库建模的唯一事实来源。src/trpc/server.ts和src/trpc/react.ts:分别封装了服务端调用和客户端调用的 API 入口。
我第一次看完这套结构的感觉是:每个文件职责都很单一,几乎没有多余的抽象层。和以前那种"service 层、dao 层、controller 层"层层套娃的工程相比,它更接近"按需求直接落代码"的直觉。这个结构对我这种一个人维护全栈项目的情况非常友好,因为代码量少,出问题的时候定位特别快。
2. 数据先行:Prisma 建模与数据库表结构的设计过程
2.1 先建模还是先写页面:我的顺序
很多个人项目开发者的习惯是先把页面画出来,再倒推数据长什么样。t3code 我反过来了,先花了一个晚上把数据模型定死,再开始写页面。原因很简单:在 T3 Stack 这条链路里,数据模型是所有类型推导的源头。Prisma schema 定了什么字段,数据库就是什么结构,tRPC 接口的输入输出就围绕这些字段转,前端的表单和列表也顺着这个结构来。如果先写页面,页面改一版,数据模型跟着改一版,迁移文件堆成山,还容易在某个步骤里漏掉一处导致类型对不上。
我知道有人会觉得先写页面更有成就感,但在这个技术组合里,先建模的边际成本其实很低。Prisma 的迁移工具足够成熟,后面加字段、改关系都是常规操作,可一旦业务代码已经写了不少,再回头改模型,牵动的东西就多了。
2.2 需求拆解:t3code 需要哪些表和关系
t3code 的核心需求拆下来就四件事:用户能创建代码片段;片段要有标题、代码内容、语言类型和备注;片段可以打多个标签;标签可以复用,首页支持按标签筛选。数据量不大,但关系是典型的多对多——一个片段可以有多个标签,一个标签可以挂在多个片段上。
对应到 Prisma 模型,处理多对多关系有两种方案:一种是 Prisma 的隐式多对多,写两行snippets Tag[]就能自动生成中间表;另一种是显式建一个中间模型,可以放额外的业务字段。我选了显式中间表,理由很实际:我的场景里,未来很可能需要记录"这个标签是谁在什么时候加到这个片段上的",中间表TagOnSnippet里可以随时补createdAt、userId这类字段,而隐式关系表加不了。
prisma/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 code String @db.Text language String @default("typescript") description String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) tags TagOnSnippet[] } model Tag { id String @id @default(cuid()) name String slug String @unique snippets TagOnSnippet[] } model TagOnSnippet { 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]) }有几个细节值得展开说。Snippet.userId和User.snippets是一对多关系,我在onDelete上选了Cascade,用户删账号的时候他的片段会被一并清掉,对个人工具来说是合理行为。Tag.slug设了唯一约束,因为标签名可能重复,但 URL 里的标识必须唯一。language字段我用了字符串而非枚举,因为代码语言种类太多,枚举的维护成本高于收益,这也是一个面向实际的取舍。
2.3 迁移命令与第一次跑通
模型写好之后,跑迁移:
npx prisma migrate dev --name init这个命令做三件事:把 schema 和当前数据库做 diff,生成一个带时间的迁移文件;把迁移应用到真实数据库;重新生成 Prisma Client,让 TypeScript 类型和当前 schema 完全同步。第一次跑的时候如果数据库连接串没配好,会直接报错,这反而是好事,因为问题暴露得越早,排查范围越小。
迁移完成后,可以用npx prisma studio打开一个本地可视化界面,直接看到新表长什么样。我在这个阶段花了十几分钟把几个测试片段塞进了数据库,确认标签关联表的数据能正常写入。这一步很有必要——tRPC 接口还没写之前,先用 studio 验证数据层没问题,等后面接口报错的时候,就能把问题归因到逻辑层而不是数据层。
3. tRPC 接入实录:类型安全的 API 层如何一步步跑通
3.1 Router、Procedure 与 Context 的关系
tRPC 是 T3 Stack 里最容易让新人困惑的部分,因为它的概念和 REST 风格接口差别很大。我用自己的话梳理一下:Router 是接口的集合,一个子路由对应一类资源,比如snippetRouter管所有片段相关的接口;Procedure 是单个接口,分query和mutation两种,query 拿数据,mutation 改数据;Context 是每次请求的"上下文环境",里面装着数据库实例和当前登录用户信息,所有 procedure 都能通过ctx访问。
t3code 的上下文创建逻辑在src/server/api/trpc.ts里,核心代码很短:
export const createTRPCContext = async (opts: { headers: Headers; }) => { const session = await getServerSession(authOptions); return { db, session, ...opts, }; };这段代码每次有请求进来,先拿当前会话,再把 Prisma 实例挂上去。这样后面的每个 procedure 里,我直接ctx.db查库、ctx.session?.user拿当前用户,不需要在每个接口里重复做认证。
Procedure 又分为公共和受保护的两种。publicProcedure任何人能调,protectedProcedure会先检查 session,没登录直接抛错。这个区分是 tRPC 在设计上的一个小巧思:把权限判断前置到 procedure 定义层,而不是散落在每个接口的业务逻辑里。
3.2 用 Zod 给接口加输入校验
tRPC 的输入校验默认用 Zod,这也是我觉得它比 REST 接口顺手的核心原因之一。t3code 里创建片段的接口大概是这样的:
import { z } from "zod"; const createSnippetSchema = z.object({ title: z.string().min(1, "标题不能为空").max(80), code: z.string().min(1), language: z.string().default("typescript"), description: z.string().max(500).optional(), tags: z.array(z.string()).default([]), }); export const snippetRouter = createTRPCRouter({ create: protectedProcedure .input(createSnippetSchema) .mutation(async ({ ctx, input }) => { const snippet = await ctx.db.snippet.create({ data: { title: input.title, code: input.code, language: input.language, description: input.description, userId: ctx.session.user.id, }, }); if (input.tags.length > 0) { await connectTagsToSnippet(snippet.id, input.tags); } return snippet; }), });Zod 的好处一眼就能看出来:所有字段的校验规则全在一个 schema 里定义,前端表单可以直接复用同一个 schema 做校验,后端也不用再手写一坨if (!title) return 400之类的代码。更关键的是,这个 input 类型会被自动推导到前端,表单里如果传了一个不在 schema 里的字段,编译直接报错,根本跑不到发请求那一步。
我第一次用的时候还担心过一点:Zod 会不会成为性能瓶颈?实测下来,对 t3code 这种规模的输入对象,校验开销可以忽略不计。真要处理超大入参的场景,那是另一个优化话题了,至少个人项目完全不用担心。
3.3 前端调用的类型推导与 React Query 集成
tRPC 在前端的使用方式对用过 React Query 的人来说非常熟悉,它本质上就是把 React Query 的useQuery和useMutation包了一层,但参数和返回值都有完整的类型推导。t3code 的片段列表页里一段典型的调用:
"use client"; import { api } from "~/trpc/react"; export function SnippetList() { const { data, isLoading } = api.snippet.getAll.useQuery({ tag: selectedTag, }); const utils = api.useUtils(); const createSnippet = api.snippet.create.useMutation({ onSuccess: async () => { await utils.snippet.getAll.invalidate(); }, }); // ...渲染逻辑 }这里有几个细节我要特别说明。api.snippet.getAll.useQuery的入参{ tag: selectedTag }会被类型检查严格约束,这个接口只认识tag这一个参数,传多了传少了都会在编辑器里标红。返回的data也已经是服务端定义好的类型,不需要手动标注泛型。onSuccess里调用invalidate会让列表数据自动重新拉取,相当于一个免手写的刷新机制。
tRPC 还支持在 Server Component 里直接调用接口,用的是api.snippet.getAll()这种 server caller 形式,能拿到和客户端完全一样的数据,但不需要经过 HTTP 网络层。t3code 的首页详情页就是这么做的——首屏直接由服务端把数据渲染成 HTML 返回,客户端不需要再发一次请求。不过要注意,server caller 拿到的数据默认不会被缓存,如果同一个数据要在多个服务端组件里用,要考虑缓存策略,不能无脑调用。
4. 登录与权限:NextAuth 接入过程中踩过的坑
4.1 第三方 OAuth 还是账号密码
登录方案我一开始纠结了很久。图省事的话,用 NextAuth 的 CredentialsProvider 配合邮箱密码登录,五分钟就能配好。但有一个隐藏的问题:CredentialsProvider 登录后拿不到数据库里的 session 记录,用户信息全部塞在 JWT 里,这意味着我无法方便地在数据库层面查"谁登录过""哪个用户的 session 还有效",而这正是一个带多用户的工具迟早要面对的需求。
另一个问题更实际:自建账号密码体系意味着要自己处理密码哈希、找回密码、验证邮件这一整套流程,对个人项目来说成本太高。所以 t3code 最后用了 GitHub OAuth——用户用 GitHub 授权登录,NextAuth 负责整个 OAuth 流程,用户资料存到自己的 User 表里。对一个小众开发工具来说,目标用户几乎都是开发者,人人都有 GitHub 账号,这个选择在体验上一点也不违和。
src/server/auth.ts里的核心配置:
export const authOptions: NextAuthOptions = { session: { strategy: "database", }, providers: [ GitHubProvider({ clientId: process.env.GITHUB_CLIENT_ID ?? "", clientSecret: process.env.GITHUB_CLIENT_SECRET ?? "", }), ], callbacks: { session: ({ session, user }) => ({ ...session, user: { ...session.user, id: user.id, }, }), }, };4.2 Session 类型扩展是必做的一步
接 NextAuth 几乎必然要踩的一个坑是类型问题。session.user默认只有 name、email、image 这些字段,没有数据库里的id。但我在 tRPC 里要拿ctx.session.user.id去创建 snippet,这行代码在 TypeScript 眼里就是非法的。
解法是在项目里声明扩展类型:
import { DefaultSession } from "next-auth"; declare module "next-auth" { interface Session { user: { id: string; } & DefaultSession["user"]; } }这个文件路径通常放在src/types/next-auth.d.ts。别跳过这一步,否则后面每个需要用户 id 的接口都会被类型错误卡住,而解法就是这五六行代码。很多人觉得 NextAuth 配起来麻烦,其实一多半的"麻烦"都来自这个类型扩展,解决了就顺畅了。
4.3 保护路由与实际遇到的坑
页面级的权限控制,我在 t3code 里用了两层。第一层是服务端组件里直接判断:
const session = await getServerSession(authOptions); if (!session) { redirect("/login"); }第二层是中间件保护,用next-auth/middleware的withAuth包裹,在src/middleware.ts里导出:
export { default } from "next-auth/middleware"; export const config = { matcher: ["/dashboard/:path*", "/snippets/:path*"], };这里有一个非常容易踩的坑:next-auth/middleware默认只对匹配到的路径生效,如果你忘了写config.matcher,你会发现中间件根本没起作用,受保护页面直接裸奔。而且中间件里拿到的 session 和getServerSession拿到的不是同一套体系,中间件适合做粗粒度的路径拦截,精细的业务权限判断还是要回到服务端组件或 tRPC 的 protectedProcedure 里做。
部署阶段还有两个和认证相关的坑值得专门说。一是NEXTAUTH_SECRET必须在一开始就设置好,如果不设置,NextAuth 会在开发环境生成一个临时密钥,每次重启就换一个,表现为"昨天还能登录,今天全掉线"。二是 OAuth 回调地址,本地开发用http://localhost:3000,部署到生产环境后必须去 GitHub OAuth App 设置里把生产域名加进回调地址列表,否则用户点登录会卡在 redirect_uri 相关的错误上。这两个坑我都踩过,都是那种"搜索引擎一搜就能看到很多人问,但官方文档里不容易注意到"的类型。
5. Tailwind 与页面渲染:前端样式实现中的取舍
5.1 "use client" 指令与组件边界的把握
写完接口和权限,接下来就是页面。t3code 里最该讲清楚的一个经验是"use client"指令的边界。Next.js App Router 下,所有组件默认是服务端组件,不能使用 state、事件和浏览器 API。一开始我图省事,把所有功能组件都加了"use client",结果列表页的渲染逻辑也被拖进了客户端组件,服务端渲染的优化基本没吃到。
后来我把组件层拆成了两层。列表页的壳是一个服务端组件,直接通过 tRPC 的 server caller 拿数据;只有真正需要交互的部分——创建按钮、标签筛选下拉框、搜索输入框——才单独拆成客户端组件。这样做的效果很明显:首屏 HTML 里已经包含了完整的列表内容,搜索引擎和直接打开链接的访客都能立刻看到内容,客户端只负责接管后续的交互。
一个实用的判断标准:如果你这个组件里没有任何useState、useEffect、onClick,那它大概率不应该加"use client"。老老实实当服务端组件,数据在服务端拿,渲染在服务端做,性能自然不会差。
5.2 Tailwind 里用得最顺的功能
t3code 的界面不复杂,核心组件是代码列表、标签筛选、创建表单和代码展示。Tailwind 在这种场景下效率很高,几个具体用法:
- 标签 chip 用
rounded-full bg-blue-50 px-3 py-1 text-sm text-blue-700,一行搞定,不需要单独写 CSS 类。 - 响应式布局用
grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3,手机一列、平板两列、桌面三列,不用写媒体查询。 - 交互状态用
disabled:cursor-not-allowed disabled:opacity-50,按钮的禁用态不用额外写逻辑。 - 暗色模式用 Tailwind 的 class 策略配合
next-themes,在根组件挂一个 ThemeProvider,所有颜色用dark:前缀的类,切换主题时 Tailwind 自动切换。
老实说,Tailwind 的缺点我也体会到了:页面写多了之后,className 越来越长,可读性会下降。我的处理办法是把重复出现的组合抽成 React 组件,比如TagChip、Card,而不是去抽成 CSS 类。这样既保留了 Tailwind 的灵活性,又没让模板变得不可维护。
5.3 代码高亮与第三方库的整合
t3code 的核心是展示代码,高亮组件绕不开。我试了几种方案,最终选了 Shiki。它和 Prism 这类传统高亮库的区别在于:Shiki 基于 TextMate 语法解析,高亮结果是一段带 class 的 HTML,不需要浏览器端做词法分析,性能更好,而且支持 TextMate 主题,样式非常接近 VS Code 的观感。
在 Next.js 里接入 Shiki 要注意包体积。Shiki 的体积不小,直接 import 进服务端组件会拖慢构建。我用了next/dynamic做按需加载,并且关闭了 SSR:
const CodeViewer = dynamic( () => import("~/components/code-viewer"), { ssr: false, loading: () => <div>加载代码预览...</div> }, );这一步让首页的 JS 体积下降了不少。个人项目没人给你做性能预算表,但自己打开 DevTools 看到加载时间的时候,这个优化值不值得做,心里自然有数。
6. 部署上线:从本地调试到 Vercel 生产环境的完整链路
6.1 环境变量整理与首次部署
本地跑通之后,部署到生产环境我选的是 Vercel,理由很直接:Next.js 项目在 Vercel 上的部署体验是最顺滑的,推送 GitHub 仓库、配好环境变量、点一下部署就行,不需要自己管服务器和 Nginx。
t3code 需要配置的环境变量有五个:
| 变量名 | 用途 | 本地值 | Vercel 值 |
|---|---|---|---|
DATABASE_URL | Prisma 连接数据库 | 本地 Postgres | 托管 Postgres 连接串 |
NEXTAUTH_URL | 认证回调地址 | http://localhost:3000 | https://你的域名 |
NEXTAUTH_SECRET | 会话加密密钥 | 任意随机串 | 与本地不同的随机串 |
GITHUB_CLIENT_ID | GitHub OAuth 应用 ID | 已创建 | 同一个 |
GITHUB_CLIENT_SECRET | GitHub OAuth 密钥 | 已创建 | 同一个 |
首次部署最常见的失败现象是页面能打开,但一访问依赖登录的页面就报错或 500。九成原因是NEXTAUTH_URL和NEXTAUTH_SECRET没配对,或者数据库连接串指向的是本地数据库。排查思路很简单:把生产环境的错误日志打开,看是认证报错还是数据库连接超时,方向就清楚了。
6.2 Prisma 在 Serverless 下的连接管理
Vercel 的无服务器函数有一个特性:每个请求都可能由一个全新启动的实例来处理,用完即弃。在这种环境下,如果每个请求都new PrismaClient(),数据库连接数会在并发稍高的时候瞬间打满。
create-t3-app 生成的src/server/db.ts已经内置了全局缓存方案:
import { PrismaClient } from "@prisma/client"; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; export const db = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = db; }这段代码的作用是:在开发环境的热更新中复用同一个 Prisma 实例,避免每次修改代码都建立一堆数据库连接。生产环境同理,函数实例被复用期间,连接也复用。这个模式是社区公认的正解,不要自己改成每个模块新建实例的写法。
我实际还遇到过一次连接数超限的报错,原因是免费数据库的并发连接上限很低,而 t3code 的列表页有几个查询会同时跑。解法是把数据库换成了带连接池能力的托管服务,并且在连接串上加了连接池参数,让 Prisma 尽量复用连接而不是每查询都新建。这里我不点名具体服务商,但你选托管 Postgres 的时候,一定要确认它提供连接池,否则 Serverless 环境下这个问题迟早会出现。
6.3 构建阶段容易忽略的 Prisma 步骤
Vercel 构建 Next.js 项目时,prisma generate默认不会自动执行,除非你在构建命令里显式加上。很多人的部署失败都是栽在这里:本地跑得好好的,一部署就报PrismaClient is not configured to run in this environment。
解决办法是在 Vercel 的构建命令里配置:
prisma generate && next build如果你在生产环境还要执行数据库迁移,那就把迁移命令也加进去。不过我更建议迁移在一个独立的步骤里手动执行,而不是塞进构建命令——构建随时可能回滚,但数据库迁移一旦执行了,回滚是件麻烦事。把"代码构建"和"数据迁移"解耦,是部署流程里值得坚持的原则。
除了 Prisma,构建阶段的另一个教训是依赖体积。t3code 的代码高亮库被next/dynamic分包后,首页构建产物明显变小。如果你发现某个第三方库让构建时间飙升,先怀疑它是不是被整个打进了主包,用next/dynamic拆出去通常立竿见影。
7. 写在最后:t3code 开发中的真实体验与后续计划
7.1 三个让我改变认知的细节
整个项目做下来,有几点和我最初预想的不太一样。
第一,类型安全不是银弹。tRPC 能保证前后端数据结构一致,但保证不了业务逻辑的正确性。我在 tRPC 接口里写过一次重复插入标签的逻辑,类型检查全部通过,但数据就是不对。类型系统解决的是"接口契约错位"这一类问题,业务 bug 该排查还是得排查。
第二,create-t3-app 生成的默认配置质量很高。我一开始出于"熟悉一下内部实现"的心态,动过几处默认代码,后来发现改回原样才是对的。比如src/server/api/trpc.ts里对 session 的判断逻辑,官方在异常处理上已经考虑得很周全,自作主张简化反而引入了边界问题。对脚手架生成的核心代码,我的建议是:先不动,跑通了再改。
第三,开发时间的分布和想象中不一样。我原以为类型安全的工程会节省大量联调时间,实际也确实省了,但省下来的时间基本都又花在了 UI 细节上——标签的间距、暗色下的对比度、加载状态的闪烁。类型安全解决的是"前后端沟通"的痛点,它不会替你解决"用户体验"的痛点。
7.2 后续计划:t3code 还可以怎么扩展
t3code 第一版上线后,我给自己列了三个待办。一是公开分享页,把某些片段设置为公开,生成一个只读链接给朋友看,解决"想给别人展示一段代码却要先发截图"的尴尬;二是全文搜索,目前只支持按标题和标签筛选,加一个基于关键词的全文检索并不难;三是从这里扩展出一套"标签推荐"逻辑,根据已有标签推断新片段适合挂哪些标签。
这三件事的共同特征是:都是在现有数据模型和 tRPC 接口上做增量,而不是推倒重来。这也是当初选 T3 Stack 的隐含收益——数据层、接口层、页面层的边界清晰,后续加功能不需要动手术式的重构。
老实说,t3code 这个项目让我对"一个人维护全栈项目"这件事有了更具体的感知。过去那种"前端写一版接口文档、后端照文档实现、出了问题两边互相猜"的流程,在 T3 Stack 下几乎不存在了。接口改一个字段,编辑器立刻在另一个文件里告诉你哪行代码需要同步改。这种体验一旦习惯了,再回到手写类型定义和接口文档的项目里,会觉得效率落差非常明显。
最后分享一个小技巧:如果你也打算用 T3 Stack 做个人项目,别在初始阶段追求完美——先跑通一条完整的链路,哪怕只有一个列表页和一个创建表单。把登录、数据库、部署这些基础能力真正串联起来之后,后面每加一个功能,边际成本都会越来越低。这也正是 t3code 一路走过来最真实的路径:从一个能用的骨架,慢慢长成一个称手的工具。