t3code这个名字乍一看像个随手起的代号,但如果把它当成一个真实落地的全栈项目来拆,背后其实藏着一整套关于类型安全、开发效率和工程化取舍的思考。我最初接触到t3code时,以为它只是个实验性脚手架,直到真正用它搭完一个带认证、数据库和API层的完整应用,才意识到它更像是一种“把TypeScript优势推到极致”的实践框架。这篇文章我就以t3code为项目代号,完整还原我基于T3技术栈(Next.js、TypeScript、Tailwind CSS、tRPC、Prisma)从零搭建一个全栈应用的思路、踩坑记录和可复用的经验。如果你正准备用现代全栈技术栈做内部工具、Saas后台或者中小型业务系统,这篇文章应该能帮你少走不少弯路。
1. 项目概述与需求拆解
1.1 t3code到底是什么
t3code这个名字,拆开看就是“T3”加“code”。T3在社区里通常指代由Theo创建的T3 Stack——一套围绕TypeScript构建的全栈技术组合,核心成员是Next.js、Tailwind CSS和tRPC,后续又加入了Prisma和NextAuth.js等常用搭档。而code则代表这是一个偏代码实践、模板工程或开发者工具的定位。所以t3code的合理定位是:一套基于T3技术栈、强调端到端类型安全的现代Web应用项目模板,或者是某个具体业务项目的代号。
我在实际使用中更倾向于把它当作一个“工程化基线”:当你厌倦了在REST API层手动维护类型定义、不想在前端再写一套axios封装、又希望整个项目从数据库到页面组件都能由TypeScript一个语言贯穿到底时,t3code就是那个可以直接拉起来跑的东西。
1.2 它解决了什么问题
先说痛点。传统的全栈开发通常是这么分工的:后端用Express/NestJS写REST接口,定义好Swagger文档,前端用axios或者fetch去调接口,再手动写interface类型。时间一长,问题就出来了:
- 接口路径写错、字段名拼错,运行时才暴露。
- 后端改了响应结构,前端类型没同步,Mock阶段没事,联调时炸锅。
- 认证逻辑前后端各写一套,逻辑不一致。
- 环境变量分散在各处,部署时配置一堆。
t3code选择用tRPC替代REST层,等于把“写API”这件事变成了“写函数”。前端直接调用后端函数,类型自动推导,不需要生成器,不需要手写文档。配合Prisma的数据库模型,整个链路从数据库表结构到页面props,几乎是天然一致的。这不仅仅是少写几行代码,而是消除了整类“类型失联”的Bug。
1.3 适合谁来用
我拉了三条适合人群的画像,你可以对照看看:
- 中小型团队的全栈工程师:不想维护独立BFF层,希望一条链路从数据库到UI全部用TypeScript打通。
- 独立开发者和创业者:做MVP、内部工具、后台管理类系统,要求开发速度快、结构清晰、后续维护成本低。
- 从前端转全栈的开发者:已经熟悉React和TypeScript,但对后端路由、鉴权、ORM不够熟,t3code能帮你把后端也控制在“TS函数”的认知范围内。
反过来,如果你的项目有非常复杂的第三方开放API需求、需要大量独立的微服务拆分,或者团队里有人对TypeScript不感冒,那t3code这类技术选型需要再评估。它擅长的是“整块业务用一套代码库搞定”的场景,而不是分布式复杂架构。
2. 技术栈选型与架构设计解析
2.1 为什么是T3这套组合
T3 Stack最核心的设计哲学是一句话:类型安全是开发效率的乘数,而不是可选项。我早期对这句话没有体感,直到用t3code做了两个完整项目后,才真正认同。
先说Next.js。它提供了App Router和Server Components,让大部分页面可以在服务端渲染。这意味着数据获取、权限校验可以前置到服务端完成,前端拿到的就是已经处理好的数据。配合React Server Components,还能去掉大量客户端状态管理的压力。
然后是Tailwind CSS。说实话,一开始我对原子化CSS是有点抗拒的,觉得HTML会变得很脏。但实际用下来,它最大的价值不是省CSS代码,而是让“样式”不再成为一个独立的、容易失控的抽象层。类名所见即所得,改起来直接,不会出现删了一个class导致全局样式飘了的情况。在t3code这种强调快速交付的场景下,这个优势非常实际。
最后是tRPC和Prisma。tRPC解决了API层的类型传递问题,它不生成schema、不依赖Swagger,直接用TypeScript的typeof推导类型。Prisma则把数据库Schema变成唯一真相源,表结构一改,类型全线感知。
这套组合的另一个隐性优势是:它的思维模型是统一的。你只需要学一套体系,不用在React、Express、TypeORM、Axios等不同工具之间来回做思维切换。对于小团队来说,认知负荷降低带来的效率提升非常明显。
2.2 t3code的目录结构与职责划分
我用t3code初始化后的目录结构,大致是这样一个分层逻辑:
src/ app/ # Next.js App Router页面与API路由 api/ # 特殊情况下的REST接口(如Webhook、文件上传) layout.tsx # 全局布局 page.tsx # 首页 server/ db/ # Prisma客户端实例 auth/ # NextAuth配置与鉴权逻辑 api/ # tRPC路由定义(root.ts、router) trpc/ query/ # tRPC服务端调用封装 react/ # 前端Hooks客户端 components/ # UI组件,按业务模块分目录 styles/ # 全局样式 types/ # 补充类型定义 prisma/ schema.prisma # 数据库模型定义这个结构看起来简单,但每个目录都有明确的职责边界,互不越界。
app/目录只管页面路由和布局,不在里面写业务逻辑。server/目录全部跑在Node.js侧,不打包到浏览器。trpc/目录是前后端的桥梁,前端通过它调用后端函数,类型从server/api自动导入。
关键约定是:前端组件不允许直接访问prisma或者auth,所有数据操作必须通过tRPC router暴露的函数。这保证了数据访问权限集中在服务端控制,而不是散落在各个组件里。
2.3 数据流与类型安全的完整链路
t3code最让人舒服的一次是这么一条链路:
- 在
prisma/schema.prisma里定义User模型,包含id、email、name等字段。 - 运行
prisma migrate dev同步数据库。 - 在tRPC router里写一个
getUserById的query函数,内部通过prisma.user.findUnique查询。 - 前端组件直接调用
api.user.getUserById.useQuery({ id: 1 }),返回的数据类型自动就是User | null,字段全提示,类型全检查。
整个过程没有写一行interface、没有定义任何API路径字符串、没有做任何数据转换。编辑器的自动补全直接贯穿数据库字段到渲染层。如果有拼写错误,编译期就红了,不需要Postman测一遍、前端跑一遍才发现。
为了验证这套链路在大一点项目里的表现,我有一次做了一个带多租户权限的业务后台,租户ID这个字段需要贯穿几乎每个数据查询。传统REST方案里,我要在后端每个接口校验租户、在前端每个请求头手动带上租户ID。t3code里我直接在tRPC的middleware里做上下文注入,前端调用时完全不感知,类型依然全通。这种“一次定义、全线受益”的体验,是t3code最核心的价值。
3. 核心功能拆解与实现细节
3.1 用户认证与Session管理
认证模块是几乎所有全栈应用的基础。t3code默认接入了NextAuth.js,在server/auth.ts里统一配置。我用Google OAuth和邮箱密码登录两种方式作为示例。
NextAuth的核心理解点是:Session不是傻傻存在内存里的,而是通过JWT或数据库Session两种策略管理。我的建议是小项目用JWT策略,减少数据库查询;中大型项目如果不需要服务端主动踢人,JWT也够用。t3code里我会这样配置:
export const authOptions: NextAuthOptions = { session: { strategy: "jwt", }, providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: "credentials", credentials: { email: { label: "邮箱", type: "email" }, password: { label: "密码", type: "password" }, }, async authorize(credentials) { // 验证逻辑,返回用户对象或null }, }), ], callbacks: { async jwt({ token, user }) { if (user) token.id = user.id; return token; }, }, };这里有个容易被忽略的坑:CredentialsProvider登录后,Session里默认没有user.id,必须在jwt回调里手动塞进去,然后再在session回调里把token.id吐给前端。
实际操作中,我还会再加一层getServerAuthSession()工具函数,专门供Server Component或tRPC context使用,避免在多个地方重复解析Session。
3.2 API层:用tRPC为每个业务域建Router
tRPC的核心概念是Router和Procedure。我的习惯是:每个业务域(user、post、invoice等)建一个独立的router文件,然后在根router里合并。
举例,帖子系统的tRPC router是这样组织的:
// server/api/routers/post.ts import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { z } from "zod"; export const postRouter = createTRPCRouter({ list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10) })) .query(async ({ ctx }) => { return ctx.prisma.post.findMany({ take: ctx.input.limit, orderBy: { createdAt: "desc" }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1), content: z.string() })) .mutation(async ({ ctx }) => { return ctx.prisma.post.create({ data: { title: ctx.input.title, content: ctx.input.content, authorId: ctx.session.user.id, }, }); }), });注意到两个细节:
一是publicProcedure和protectedProcedure的区分。这是t3code里权限控制的精髓。未登录用户只能访问list,创建帖子必须走protectedProcedure,拦截逻辑在middleware里统一做了,不需要每个方法里手写if (!ctx.user) throw。
二是用zod做输入校验。zod不是锦上添花,它是tRPC最重要的伙伴。它保证了API层的入参在运行时也被校验,而不是只靠TypeScript编译期。前端传入非法数据,会直接返回BAD_REQUEST,不会污染数据库。
3.3 数据建模与Prisma的使用细节
数据建模是决定后续开发体验的关键。t3code项目里,Prisma Schema的强弱直接决定整个后端API的设计空间。
我建议遵循几个原则:
- 先定义关系,再定义字段。Prisma的关联查询太方便了,提前设计好relation可以少写很多join逻辑。
- 枚举尽量用Prisma Enum,别用字符串常量。数据库枚举能在Prisma客户端生成TS常量类型,前端用起来自动提示。
- 软删除做成字段,不要物理删除。中后台系统里数据恢复需求和审计需求很常见。
- 时间字段统一用
DateTime @default(now()),不要手动传,避免时区和格式混乱。
这里举一个我实际踩过的例子:有一次建消息通知表,表里有readAt DateTime?字段用于标记已读时间。初始版本里我把readAt设计成BooleanisRead,后来发现需要展示“已读时间”时,又加了字段、又回填了数据,非常痛苦。如果一开始就设计成DateTime?,空值就代表未读,非空就是已读时间,一个字段解决两个需求。这种建模思维的差异,在t3code项目里会被Prisma放大,因为Schema一改,全链路类型跟着变,改起来很快,但依然不如一开始设计得当。
3.4 前端页面与组件的数据获取模式
t3code的前端组件通常有两种数据获取模式,我根据场景混着用。
第一种是Server Component直接调用服务端逻辑。比如一个不需要交互的数据列表页,我可以直接在page.tsx里调用tRPC的server-side caller:
// app/posts/page.tsx import { api } from "~/trpc/server"; export default async function PostsPage() { const posts = await api.post.list({ limit: 20 }); return <PostList posts={posts} />; }好处是首屏直出、无loading态、SEO友好。缺点是这个页面不能做筛选、排序等交互操作时实时刷新。所以它适合列表首页、详情页、Dashboard这类以展示为主的内容。
第二种是客户端组件配合React Query。适合筛选器、实时搜索、分页加载这类高交互场景:
// components/SearchablePostList.tsx "use client"; import { api } from "~/trpc/react"; export function SearchablePostList() { const [keyword, setKeyword] = useState(""); const { data, refetch } = api.post.search.useQuery({ keyword }); return ( <div> <input value={keyword} onChange={(e) => setKeyword(e.target.value)} /> <button onClick={refetch}>搜索</button> <ul> {data?.map((post) => <li key={post.id}>{post.title}</li>)} </ul> </div> ); }这里的api.post.search.useQuery不是普通的hooks封装,它内部还继承了React Query的缓存、重试、失效机制。比如创建新帖子后调用utils.post.list.invalidate(),列表会自动刷新,不需要手动更新state。这种数据获取模型,让前后端的数据一致性维护成本大幅下降。
4. 环境准备与项目初始化实录
4.1 本地开发环境的推荐配置
启动一个t3code项目,对本地环境有一定要求。我的推荐配置是:
| 工具 | 版本要求 | 备注 |
|---|---|---|
| Node.js | 18.17+ 或 20.x LTS特别推荐 | Next.js 14要求,低于这个版本直接报错 |
| pnpm | 8.x | t3code官方推荐,npm会偶尔出现幽灵依赖问题 |
| PostgreSQL | 14+ | t3code默认以PG为主,MySQL也可用但部分特性有差异 |
| Prisma CLI | 最新版 | 建议全局安装或通过npx使用 |
| Docker | 可选但建议装 | 本地起数据库非常方便 |
我特别推荐用pnpm。T3生态对pnpm的支持很积极,依赖安装速度快,磁盘占用低,最关键的是Next.js的@next/swc等native模块不会出现莫名其妙的版本错乱。
数据库方面,如果你不想手动装PostgreSQL,最省事的方式是Docker起一个:
docker run --name t3code-db -e POSTGRES_PASSWORD=devpass -e POSTGRES_USER=t3code -e POSTGRES_DB=t3code_dev -p 5432:5432 -d postgres:16-alpine这个命令一次搞定本地开发库环境,删除也干净,不会污染宿主机。
4.2 从零初始化t3code项目
初始化步骤非常简单,但有几个交互选项需要注意。
pnpm create t3-app@latest跟着交互提示走。中间的关键选项我逐个说:
- 选择包管理器:选pnpm。
- 选择Next.js的router方式:App Router,现在是默认推荐。
- 语言:TypeScript是必选项,不是可选项。
- 选中tRPC、Prisma、NextAuth.js、Tailwind CSS这些模块。Tailwind选不选看个人,但t3code的模板默认就带,我建议保留,就算不用也可以删。
- 在“App name”时输入t3code。
- 初始化数据库连接串时,可以直接用我上面Docker命令里的连接串模板:
postgresql://t3code:devpass@localhost:5432/t3code_dev?schema=public。
初始化完成后,先跑一次迁移让数据库表结构落地:
cd t3code pnpm db:push然后启动开发服务器:
pnpm dev打开localhost:3000,看到T3风格的欢迎页说明项目已正常运行。我第一次跑这套流程大概用了10分钟,相比传统全栈项目从零配Webpack、配Nodemon、配Babel那可舒服太多了。
4.3 环境变量与多环境配置经验
t3code对环境变量的管理有一套约定。.env只用于本地开发,.env.production等更推荐在部署平台的环境变量面板里配置,不要塞进代码库。核心变量有这些:
# 数据库 DATABASE_URL="postgresql://t3code:devpass@localhost:5432/t3code_dev?schema=public" # NextAuth NEXTAUTH_SECRET="生成一个随机长字符串" NEXTAUTH_URL="http://localhost:3000" # OAuth(如果用了Google/GitHub登录) GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_SECRET=""生成NextAuth密钥的简便方法:
openssl rand -base64 32这里有个常见问题:NEXTAUTH_URL在本地环境下如果不设,NextAuth会尝试从请求头推断URL,有时会推断成localhost:3000以外的值导致OAuth回调失败。所以本地也建议明确设置。部署到Vercel时这个变量会自动注入,本地和线上环境各管各的,不要混用。
5. 从零到可用:完整功能模块实现
5.1 实现一个带鉴权的待办事项应用
为了不泛泛而谈,我用一个“待办事项”来完整演示t3code的核心链路。目标功能是:用户能注册登录,登录后能创建、查看、完成、删除自己的待办事项。
第一步,定义数据模型。在prisma/schema.prisma里加:
model Todo { id String @id @default(cuid()) title String completed Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String createdAt DateTime @default(now()) }同时给User模型加上反向关系:
model User { id String @id @default(cuid()) email String? @unique name String? todos Todo[] }执行同步:
pnpm db:push留意db:push是开发期快速同步,如果已经有正式环境数据,要改用prisma migrate dev生成迁移记录,保证生产环境可重复执行。
第二步,定义tRPC Router。新建server/api/routers/todo.ts:
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { z } from "zod"; export const todoRouter = createTRPCRouter({ list: protectedProcedure.query(async ({ ctx }) => { return ctx.prisma.todo.findMany({ where: { authorId: ctx.session.user.id }, orderBy: { createdAt: "desc" }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1).max(200) })) .mutation(async ({ ctx }) => { return ctx.prisma.todo.create({ data: { title: ctx.input.title, authorId: ctx.session.user.id }, }); }), toggle: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx }) => { const todo = await ctx.prisma.todo.findFirst({ where: { id: ctx.input.id, authorId: ctx.session.user.id }, }); if (!todo) throw new Error("Todo不存在"); return ctx.prisma.todo.update({ where: { id: ctx.input.id }, data: { completed: !todo.completed }, }); }), delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx }) => { return ctx.prisma.todo.deleteMany({ where: { id: ctx.input.id, authorId: ctx.session.user.id }, }); }), });注意每个操作都加上了authorId条件。这是多租户数据安全里至关重要的一环:不止要防止别人删改,还要防止平行越权。很多人只校验“数据存在”,不校验“数据属于当前用户”,这是严重漏洞。用deleteMany代替delete可以在删不到数据时不抛异常,逻辑更安全。
第三步,在根router注册:
// server/api/root.ts import { todoRouter } from "./routers/todo"; export const appRouter = createTRPCRouter({ todo: todoRouter, });第四步,前端页面。这里我选择客户端组件来做,因为待办事项本身就是强交互场景:
// app/todos/page.tsx "use client"; import { api } from "~/trpc/react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; export default function TodosPage() { const { data: session } = useSession(); const router = useRouter(); if (!session) { router.push("/api/auth/signin"); return null; } return <TodoApp />; } function TodoApp() { const utils = api.useUtils(); const { data: todos, isLoading } = api.todo.list.useQuery(); const createTodo = api.todo.create.useMutation({ onSuccess: () => utils.todo.list.invalidate(), }); const toggleTodo = api.todo.toggle.useMutation({ onSuccess: () => utils.todo.list.invalidate(), }); if (isLoading) return <p>加载中...</p>; return ( <div className="max-w-xl mx-auto p-6"> <form onSubmit={(e) => { e.preventDefault(); const form = new FormData(e.currentTarget); const title = form.get("title") as string; if (title.trim()) createTodo.mutate({ title }); e.currentTarget.reset(); }} > <input name="title" className="border p-2 mr-2" placeholder="添加待办" /> <button type="submit" className="bg-blue-500 text-white p-2 rounded">添加</button> </form> <ul> {todos?.map((todo) => ( <li key={todo.id} className="flex items-center justify-between p-2 border-b"> <span className={todo.completed ? "line-through" : ""}>{todo.title}</span> <div className="flex gap-2"> <button onClick={() => toggleTodo.mutate({ id: todo.id })}> {todo.completed ? "取消完成" : "完成"} </button> <button onClick={() => api.todo.delete.useMutation().mutate({ id: todo.id })}> 删除 </button> </div> </li> ))} </ul> </div> ); }删除按钮那里我临时调了一个useMutation,实际项目里应该在顶部统一const deleteTodo = api.todo.delete.useMutation()再在按钮回调里引用。这个细节不重要,但逻辑一致更好。
5.2 中间件与权限控制的进阶玩法
待办示例只用到了“登录用户”这一层控制。真实业务的权限往往更复杂:管理员、编辑、访客等角色。t3code里我推荐通过tRPC的middleware实现RBAC。
定义两类procedure:
// server/api/trpc.ts import { initTRPC, TRPCError } from "@trpc/server"; const t = initTRPC.context<{ prisma, session }>().create(); export const requireAdmin = t.middleware(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } if (ctx.session.user.role !== "ADMIN") { throw new TRPCError({ code: "FORBIDDEN" }); } return next({ ctx: { ...ctx, session: ctx.session } }); }); export const adminProcedure = t.procedure.use(requireAdmin);使用方式极其简单:
create: adminProcedure.mutation(...)所有管理端接口天然受到角色控制,不需要每个方法里重复判断if (ctx.session.user.role !== "ADMIN")。这样做的好处是权限逻辑集中、可复用、可测试。相比在页面组件里做前端路由守卫,服务端procedure的强制拦截才是真正可靠的安全边界。
5.3 文件上传与第三方服务集成
t3code默认模板不含文件上传方案,但很常见。我习惯用S3兼容的对象存储配合一个tRPC mutation生成预签名URL。这个方案的优势是文件不过应用服务器,减轻带宽压力,上传过程走客户端直传。
核心思路:
- 在tRPC里提供一个
generateUploadUrlmutation,校验用户权限后返回一个带过期时间的预签名PUT URL。 - 前端拿到URL后直接向对象存储发
PUT请求,附带文件二进制。 - 存储服务商返回文件URL,前端再把URL通过另一个mutation写回数据库。
伪代码:
uploadUrl: protectedProcedure .input(z.object({ fileName: z.string(), contentType: z.string() })) .mutation(async ({ ctx, input }) => { const key = `user-${ctx.session.user.id}/${crypto.randomUUID()}-${input.fileName}`; const url = await s3.getSignedUrlPromise("putObject", { Bucket: process.env.S3_BUCKET, Key: key, ContentType: input.contentType, Expires: 60, }); return { url, key }; }),客户端再用fetch(url, { method: "PUT", body: file })直接传。这个模式绕开了Next.js API路由的请求体限制,上传大文件非常稳。
6. 常见问题与排查技巧实录
6.1 tRPC + Next.js类型不匹配导致的编译失败
我在t3code项目里遇到最多的问题就是类型不匹配,尤其是刚升级Next.js或Prisma版本后。典型报错长这样:
Type error: Type 'X' is not assignable to type 'Y'.排查思路有三步:
- 执行
pnpm prisma generate,确保Prisma客户端类型是最新的。 - 执行
pnpm tsc --noEmit单独跑一次类型检查,判断是编译问题还是编辑器缓存问题。 - 如果是tRPC procedure的返回值问题,检查是不是漏了
await。tRPC的异步函数返回值必须被实际resolve后才会被推导成具体数据。
另外,刚clone项目后第一次启动如果遇到类型报错,建议先rm -rf .next清一下缓存,Next.js的增量编译偶尔会保留旧类型。
6.2 Prisma迁移冲突与数据丢失的避坑
Prisma的migrate dev在开发期好用,但分支合并时容易产生迁移冲突。我的经验是:
- 多人协作时尽量用
prisma migrate dev统一串行操作,别手动改migrations文件夹。 - 分支合并产生冲突时,不要直接删迁移记录。正确做法是保留
migration_lock.toml不动,找到基准点,重新整理迁移链。 - 生产环境执行迁移前必须备份数据库,再执行
prisma migrate deploy。migrate dev会尝试重置数据,生产环境千万不能用。
我给生产环境加了一个保险习惯:每次部署前先跑一次prisma migrate deploy --preview-feature,干跑一次看看哪些迁移会执行,确认无误再执行真实迁移。
6.3 NextAuth回调地址与Session丢失之谜
这是一个非常多发而且隐蔽的问题。表象是:本地开发登录正常,部署到线上后点击登录跳转回来一直报“Callback URL mismatch”。
排查关键:
- 确认线上环境变量里
NEXTAUTH_URL设置正确,且没有多余尾部斜杠。 - 在Google OAuth后台把线上域名加进Authorized redirect URIs,格式为
https://你的域名/api/auth/callback/google。 - Vercel或Node平台部署时,注意环境变量是否区分Preview和Production环境。
还有一个Session丢失问题:浏览器里明明有Cookie,服务端却拿不到session。多半是因为NEXTAUTH_SECRET不同。比如本地和线上各生成了一份,或者换了平台导致secret自动重新生成。解决办法是固定一份环境变量。这一步不处理好,用户会时不时被强制登出,排查起来极其痛苦。
6.4 性能优化的三个常用操作
t3code模板虽然顺手,但默认形态性能未必最佳。我一般会做三件事:
第一,tRPC query写@tanstack/react-query的staleTime。比如列表页的默认刷新需求不剧烈,可以设staleTime: 30_000,减少不必要的重复请求。
第二,Prisma查询用select和include按需取字段。很多新手直接findMany()把整表数据拉回来,其实大部分用不到。t3code里缩写是按字段取数,反正类型也是安全的。
第三,Next.js动态导入适合重型组件。比如图表、富文本编辑器这类,用next/dynamic配置ssr: false按需加载,首屏体积能砍掉一大截。
7. 实际使用体验与后续扩展建议
我用t3code做了两个完整项目:一个内部运营后台,一个用户侧的内容管理平台。整体下来,如果让我用一个词评价,我会说“顺手”。它的顺手来自你对整条技术链路的掌控力——每个数据请求从页面到数据库,你都能在编辑器里跳转、检查、修改,而不用拉一个后端仓库再找半天接口文档。
但我也要坦白,t3code并不适合所有项目。如果你的团队前端、后端分工严格,后端是Java或Go体系,那强行引入t3code会造成技术栈割裂。它的最佳土壤是:一份代码、一个仓库、一个可以从数据库管到UI的开发者或小团队。
如果后续想基于t3code继续扩展,我建议优先做这几件事:
- 加入完善的错误处理中间件,统一tRPC的错误格式,让前端弹窗提示逻辑收敛。
- 接入任务队列。tRPC的mutation是同步过程的,耗时任务(邮件发送、文件转码)应该丢到队列异步处理。
- 把通用路由抽成npm包或独立仓库。比如auth、user、auditLog这类RTU风格的域,形成自己的业务基座,新项目直接复用。
最后再分享一个我个人的体会:t3code这类项目模板的价值不在“快”,而在于它帮你把那些容易出错的工程决策提前做对了。类型安全、数据流一致、权限集中管理,这些都是老项目后期才追悔莫及的坑。你不需要喜欢每一个具体的库,但值得在下一个全栈项目里试一试这套思路。就个人体验而言,一旦适应了这种“类型贯穿全局”的开发方式,再回到手动维护API类型的模式,会有一种明显的手工感落差。