t3code是什么?T3技术栈全栈开发实战与踩坑指南

发布时间:2026/10/27 23:05:25
t3code是什么?T3技术栈全栈开发实战与踩坑指南

t3code这个关键词最近在技术社区里频繁出现,我一开始以为是哪个新出的低代码平台,搜了一圈才发现它并不是一个官方产品名称,而是大家对T3技术栈代码开发方式的一种通俗叫法。所谓T3,核心就是那三板斧:TypeScript、Tailwind CSS和tRPC,再往下延伸通常还会带上Next.js和Prisma。说白了,这是一套从数据库到浏览器全程类型安全的全栈开发组合,也是这几年TypeScript社区里呼声很高的一整套工程实践。

我最近正好把一个内部工具项目从老的Express+React分开写的架构迁到了这套栈上,前前后后折腾了两周,踩了不少坑,也算把t3code这套工作流完整走了一遍。这篇文章不打算做什么科普性质的概述,就纯粹聊聊我在这个过程中的真实体会、具体操作和那些文档里不会告诉你的细节。如果你正在犹豫要不要用这套技术栈,或者刚开始接触create-t3-app、tRPC这些工具,我踩过的坑应该能帮你省下不少时间。

1. t3code到底是什么:一次对T3技术栈的重新认识

1.1 一个看起来像产品名,其实是社区黑话的搜索词

先说个有意思的事。t3code能上热搜,很大程度是因为这个词本身充满了模糊感,大家搜它之前脑子里想的可能是:一个代码生成工具?一个AI编程助手?一个开发者社区?但真正点进去就会发现,这个词指向的其实是T3这个技术栈的设计理念,以及围绕它形成的开发模式。

T3这个叫法最初来自海外一个技术主播的推演,核心主张就一句话:想让TypeScript的价值发挥到最大,就不要在前后端之间人为制造类型断层。传统全栈开发里,前端写接口请求,后端写路由处理,两边各维护一套类型定义,改一个字段要翻半天文档,这其实是所有类型系统的浪费。T3的解法是把API层本身变成类型的一部分,让前端调用接口就像调用本地函数一样,全程有代码提示,参数错了编译期就会报错。

我理解的t3code,就是围绕这套理念展开的完整工程实践。它不是一个开箱即用的框架,而是一组工具的组装方式,说它是种"开发心法"可能更准确。对只想快速验证业务逻辑的团队来说,这套东西的上手成本不算低,但一旦跑通,迭代速度确实会明显不一样。

1.2 我从三个方向验证这套工作流

为了搞清楚t3code到底值不值得投入,我特意从三个方向做了验证。

第一个方向是最直接的,拿create-t3-app脚手架起了一个全栈项目。这个脚手架把所有主流选项都做成了交互式勾选,包括Next.js、Tailwind、tRPC、Prisma、NextAuth这些,选完直接生成一套结构完整的代码,前后端类型已经打通,跑起来就能看到效果。这种"开箱即用但又没把路走死"的设计,让我觉得T3团队对开发者的痛点摸得很准。

第二个方向是反着来的,我去看了社区里唱衰T3的帖子。有人说这套技术栈过度依赖单一作者,有人说tRPC只在全栈项目里有用,遇到前后端分离就废了。这些批评确实有其道理,但也让我想明白了一个问题:T3并不是要替代所有方案,它最适合的是那种由一个团队同时掌控前后端的场景,也就是所谓的"全栈单仓库"模式。如果你的团队结构本身就是前后端分离的,那直接上REST或者GraphQL才是正道。

第三个方向是我自己动手迁移那个内部工具。这个工具本身不大,只有七八个数据模型和二十多个接口,但经历了从零搭建到上线的完整过程。这一步让我真正体会到了"类型安全不是玄学,是生产力"这句话的含义。

1.3 适合什么人,又不适合什么人

经过这一轮验证,我对t3code的适用边界有了比较清晰的判断。如果你的项目满足下面几个条件,这套技术栈会让你很舒服:

  • 团队规模不大,前端后端是同一拨人,或者有明确的技术owner能打通整条链路
  • 项目的数据模型不算特别复杂,但接口数量不少,类型一致性收益明显
  • 重度依赖数据库操作,希望ORM和类型定义能完全对齐
  • 对开发体验要求高,希望新成员能看一遍代码结构就上手改功能

反过来,如果你面临的是以下这些情况,就得谨慎了:

  • 前后端完全分离,甚至部署在完全不同的基础设施上,那tRPC的类型同步优势会被严重稀释
  • 必须给第三方提供公开API,还是老老实实用OpenAPI/REST那套
  • 团队里TypeScript基础参差不齐,类型报错可能会变成新人的噩梦
  • 需要和其他团队共享接口定义,纯靠代码生成出来的类型没法作为跨团队契约使用

我把这套栈想得越清楚,就越觉得它注定不是万能药,但也绝对不是玩具。它就是一套有鲜明主张的工程方案,像是给"全栈TypeScript极简主义者"准备的一份礼物。

2. 为什么是Next.js + tRPC + TypeScript这个组合:选型背后的逻辑

2.1 类型安全不是玄学,是生产力

很多人一听到"类型安全"就觉得这是洁癖型程序员的口号,觉得"我明明能跑就行,你非得让我定义类型"。我原来也这么想,直到这次项目里出了一个典型事故。

老项目里有一个用户列表接口,后端返回了一个字段叫avatarUrl,前端代码里用的是avatarURL,大小写差了一个字母。这个错误在编译期完全不会暴露,因为前后端类型断开,前端对接的时候只能靠口口相传或者翻Swagger文档。结果直到接口联调阶段才被发现,当时已经写死了十几个组件引用。改起来倒是不难,但这种低级错误带来的沟通成本和心智负担,其实比错误本身更伤人。

T3这套组合把API的输入输出类型定义放在后端,前端通过类型推导自动拿到完全一致的签名。编译器能在你写错字段名的那一瞬间就喊停,而不是让错误潜伏到运行时,由用户来发现问题。我迁移完这个内部工具后,最大的感受是前端调用接口时不再需要反复确认"这个字段叫什么来着",光标悬停上去,所有类型信息都在编辑器里躺着。

2.2 tRPC相比REST和GraphQL,解决了什么

聊到API层设计,就绕不开和REST、GraphQL的对比。我做这个项目之前,其实对tRPC是持怀疑态度的,感觉它就是个玩具,不适合正经业务。用完之后我承认,自己之前对它的理解有偏差。

REST的精髓是资源导向,它有一套清晰的语义体系(GET、POST、PUT、DELETE),这对大规模系统和跨团队协作非常友好。但它的问题也很明显:针对一个页面的聚合数据,你可能要发好几个请求,或者需要做一层额外的聚合层。GraphQL解决了这个问题,引入了schema和resolver的概念,但代价是引入了相当复杂的运行时、缓存和权限体系,小项目根本撑不起这个架构复杂度。

tRPC的思路是彻底抛弃对传输语义的抽象,它直接把后端函数变成可调用的"远程过程",你定义一个query,前端就能像调用本地异步函数一样调用它。没有schema文件要维护,没有codegen步骤,没有额外的类型生成工具链。类型就是你后端代码里的那一个return语句的返回类型,前端自动同步。

当然,如果你需要做跨平台对外开放API,tRPC这个方案就不合适了,因为它没有标准的HTTP语义,别人拿到你的接口也不知道怎么对接。本质上,tRPC是一个把"全栈类型安全"做到极致、但放弃了通用性的工具。

2.3 不吹不黑:这套组合的短板在哪里

我用了两周之后,还是要客观说几个它的短板。

第一个是调试体验。传统REST接口可以用Postman直接发起请求,看返回结构,tRPC调试则得借助Playground或者直接在代码里调用,这就多了一步。好在现在有基础的浏览器插件能缓解一些问题,但和REST生态相比还是差了一截。

第二个是升级风险。tRPC的API不稳定,版本升级的破坏性变更时有发生。我在项目中后期就把依赖升了一级,结果有部分router定义方式变了,改起来虽说不难,但确实会打断开发节奏。如果你不是全栈项目,这个风险会更明显,因为类型生成代码一旦失效,整个流程就卡住了。

第三个是认知门槛。这套技术栈要求团队成员同时理解Next.js的渲染模型、Prisma的数据库操作和tRPC的过程定义方式,三个工具叠加起来的学习曲线,远比单独学一个框架陡峭。新人入职如果只懂React不懂数据库,光看Prisma的model定义就会愣一下。说白了,这工具有"全栈能力要求"。

我也把REST、GraphQL和tRPC放在一个表格里对比过,方便后来人参考:

维度RESTGraphQLtRPC
类型安全需要额外工具(如openapi-typescript)schema驱动,需维护.graphql文件原生最强,代码即类型
对外API支持最标准,各种语言都好对接标准,生态成熟基本不适合对外暴露
简单资源CRUD很直接偏重,需写query和resolver最直接,函数定义就是接口
复杂聚合查询需多次请求或增加聚合层最强,一个query搞定需要在router里手动组装
缓存策略HTTP层缓存成熟客户端缓存强大运行时缓存靠自己管理
调试工具Postman全家桶GraphiQL等浏览器扩展支持有限
适用团队接口多、前后端分离数据形态复杂、需求多变全栈单仓、类型控团队

这个表做完之后,我心里基本有数了。T3这套组合是给特定场景的极致优化方案,适合的人会很喜欢,不适合的人会觉得它浑身都是缺点。但要理解它为什么存在,你得先站在"全栈TypeScript开发"的立场上去看问题。

3. 从create-t3-app入手:初始化一个可上线的全栈项目

3.1 脚手架选项不是随便勾的

我先示范一下整体起步过程,跑下来你就知道这套技术栈搭一个项目有多简单。

pnpm create t3-app@latest my-app

执行之后,命令行会弹出一系列交互选项。这里有几个关键选择值得展开说,因为很多人第一次会踩坑。第一项是选择是否使用Next.js,这个基本必选,因为T3理念里服务端渲染和API层是绕不开的一部分。第二项是Tailwind CSS,如果你只想做API后端,这项可以不选,但凡是带界面的项目,我建议都选上,它和Next.js搭配的体验很好。第三个是tRPC,这是整个技术栈的灵魂,必须选。接着是Prisma、NextAuth和Drizzle,Prisma我选了,因为后续要对接数据库,ORM能省很多事;NextAuth如果是做带账号体系的系统,建议勾上,不然以后手搓登录会很痛苦;Drizzle是新出的轻量ORM,但生态还没完全成熟,我这次没选。

生成的项目目录结构和我以前习惯的"前后端分目录"完全不同,它是按功能模块来组织的。核心部分长这样:

my-app/ ├── src/ │ ├── app/ │ │ ├── _components/ │ │ └── page.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ │ ├── post.ts │ │ │ │ └── user.ts │ │ │ └── root.ts │ │ ├── db.ts │ │ └── auth.ts │ ├── trpc/ │ │ ├── server.ts │ │ └── client.ts │ └── env.js └── prisma/ └── schema.prisma

一眼扫过去,src/server存所有服务端逻辑,src/trpc存前后端共享的类型打通逻辑,prisma放数据库模型定义。这种结构强迫你把后端逻辑放在一个统一的地方,而不是散落在各个page文件里,对后续维护很友好。

3.2 环境变量和数据库:最容易卡住新手的第一道坎

脚手架生成完之后,第一步不是跑界面,而是配置环境变量。项目根目录有个.env.example文件,你需要把它复制一份改成.env,然后填上数据库连接串。我第一次没注意,直接启动了项目,结果所有依赖数据库的接口全部报错,提示找不到DATABASE_URL。查了半天才发现,Next.js默认有一个安全机制,服务端环境变量如果是undefined,不会在错误信息里直接告诉你,而是不断抛出"Internal Server Error",排查成本很高。

如果你本地没有现成的PostgreSQL,最简单的办法是用Docker起一个:

docker run --name t3-postgres \ -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_USER=postgres \ -e POSTGRES_DB=t3db \ -p 5432:5432 \ -d postgres:16

然后把.env里的数据库连接串改成:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/t3db?schema=public"

改完之后跑一次pnpm prisma migrate dev,它会根据schema生成数据库表结构,同时更新Prisma Client的类型定义。这一步很关键,Prisma的模型定义是整个项目前后端类型安全的源头,数据模型怎么定义,前端就能直接拿到对应的TypeScript类型。

我强烈建议你在初始化之后第一件事就是跑通这条链路:数据库启动、env配置、migrate、然后页面里随便查一条数据展示出来。别急着堆业务代码,这条链路是整个项目的地基,地基不稳后面全是坑。

3.3 一个最小可运行的tRPC接口:从定义到前端调用

接下来用一个最小的todo示例,展示tRPC在项目里的实际写法。先在prisma/schema.prisma里定义模型:

model Todo { id String @id @default(cuid()) title String completed Boolean @default(false) createdAt DateTime @default(now()) }

然后定义一个router,这个就是你的"后端接口":

// src/server/api/routers/todo.ts import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/trpc/server"; export const todoRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.todo.findMany({ orderBy: { createdAt: "desc" }, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1).max(100) })) .mutation(async ({ ctx, input }) => { return ctx.db.todo.create({ data: { title: input.title } }); }), });

注意到这里没有写任何URL路由、没有controller、没有service,就是一个普通的async函数,接收input参数,返回数据。校验用的是zod,zod的schema既定义了运行时校验规则,也通过TypeScript推断出输入类型。

在React组件里调用:

"use client"; import { api } from "~/trpc/client"; export function TodoList() { const utils = api.useUtils(); const { data: todos, isLoading } = api.todo.list.useQuery(); const createTodo = api.todo.create.useMutation({ onSuccess: () => { utils.todo.list.invalidate(); }, }); if (isLoading) return <p>加载中...</p>; return ( <ul> {todos?.map((todo) => <li key={todo.id}>{todo.title}</li>)} </ul> ); }

关键点在这一行:api.todo.list.useQuery(),前端不知道后端接口长什么样,全靠类型推导能够直接感知到todos数组里每个元素的字段结构。如果你在组件里写了todo.taitle,编辑器会立刻划红线。

类型安全不是有多少辅助工具,而是你的IDE和编译器能在你犯错之前就拦住你。对我来说,这比任何花哨的运行时console.log都有价值。

4. Prisma和数据库层:数据模型的类型源头

4.1 为什么PRISMA在t3code里那么重要

在T3这套技术栈里,Prisma不仅仅是ORM,它承担了一个我一开始没意识到的关键职责:它是所有领域类型的唯一真实性来源。

打个比方,你定义了一个User模型,里面有id、name、email三个字段。Prisma会根据这个model自动生成完整的TypeScript类型,tRPC查询返回的ctx.db.user.findMany()结果,天然就带上了这些字段类型。你再也不用在前端自己定义User接口,也不用在某个types.ts文件里维护一份"前端版本的User"。

这件事对开发效率的提升是立竿见影的。以前我写后端接口时,心里想的是"接口要返回什么",前端对接时想的是"接口会返回什么",两个人之间全靠文档对齐。现在呢?数据库model定义完,两边拿到的类型是同一个,完全不存在差分。数据模型变更时,前端挂在某个字段名上的代码该改哪里,TypeScript编译器会逐个帮你标出来,而不是靠上线之后日志里报"Cannot read property of undefined"。

4.2 从查询到事务:一套完整的数据库操作体验

我在实际项目里用过几种不同的数据访问模式,可以分享一下它们在T3工作流里的样子。

最简单的列表查询,像用户列表、订单列表这种,直接在query过程里findMany就行,tRPC会帮你处理序列化和响应返回,不需要像REST那样手动包装一个response对象。

有筛选条件的列表,可以用zod定义输入参数:

list: publicProcedure .input( z.object({ status: z.enum(["ACTIVE", "BLOCKED"]).optional(), page: z.number().default(1), }) ) .query(async ({ ctx, input }) => { return ctx.db.user.findMany({ where: { status: input.status }, skip: (input.page - 1) * 10, take: 10, }); })

这种写法非常直观,参数校验直接写在接口定义旁边,前端传进来的垃圾数据在进数据库之前就被拦截了。

再复杂一点,涉及多表关联写入的场景,我用过一次$transaction。T3和Prisma组合下,事务操作其实和普通prisma客户端没有区别,但要注意tRPC的mutation整体是在一个请求解析周期里执行的,如果事务内有异步任务,必须确保它们在同一个$transaction回调里完成,不能拆开放到外面。

4.3 一个差点翻车的事务处理经历

在迁移过程中我真的遇到过一次差点翻车的事。当时要做一个"创建订单并扣减库存"的操作,按照习惯我写成了两步:

createOrder: publicProcedure .input(z.object({ productId: z.string(), quantity: z.number() })) .mutation(async ({ ctx, input }) => { const product = await ctx.db.product.findUnique({ where: { id: input.productId }, }); if (!product || product.stock < input.quantity) { throw new Error("库存不足"); } await ctx.db.order.create({ data: { productId: input.productId, quantity: input.quantity }, }); await ctx.db.product.update({ where: { id: input.productId }, data: { stock: product.stock - input.quantity }, }); return { success: true }; })

表面逻辑没问题,但这两个数据库操作之间没有事务包裹。如果order.create成功了,product.update因为网络超时失败了,那就会出现"订单已创建但库存没减"的数据不一致。这个锅在生产环境可大了,因为用户可能下单成功,买到的却是超卖的东西。

改成事务版其实很简单:

createOrder: publicProcedure .input(z.object({ productId: z.string(), quantity: z.number() })) .mutation(async ({ ctx, input }) => { return ctx.db.$transaction(async (tx) => { const product = await tx.product.findUnique({ where: { id: input.productId }, }); if (!product || product.stock < input.quantity) { throw new Error("库存不足"); } const order = await tx.order.create({ data: { productId: input.productId, quantity: input.quantity }, }); await tx.product.update({ where: { id: input.productId }, data: { stock: product.stock - input.quantity }, }); return order; }); })

这里有个小细节值得注意:事务回调函数里的db参数是tx,不是外层的ctx.db。如果粗心用了外层的client,事务一样会失效,因为那个操作不参与这个事务。这种错误很难发现,因为不是每次都会触发问题,只在极端操作并发或某个步骤失败时才会暴露。我把这条写入自己团队的Code Review清单里,凡是涉及多步写操作,第一眼先看它有没有用到$transaction。

4.4 数据库索引和迁移管理:Less Is More

T3脚手架默认生成的Prisma schema比较简单,如果你的数据模型里有明显的查询热点,不要一开始就堆索引,先把功能跑通,再看实际慢查询,最后按需加索引。我在项目里一开始对不需要联查的字段都加了索引,结果写入性能反而受影响,后来撤掉了,只保留了常用的外键索引和唯一约束。

迁移管理方面,prisma migrate dev在开发环境很好用,但生产环境的迁移建议走prisma migrate deploy,不要手动改数据库。两者的区别在于,dev会根据你的schema变化动态生成迁移记录,deploy只执行已存在的迁移SQL,不会额外生成新文件。部署过程中,迁移顺序错乱会导致表结构和model定义不一致,轻则接口报错,重则数据写不了,所以建议把数据库迁移纳入CI/CD流程里,跟代码一起发布。

5. 部署上线时最容易被忽略的几个问题

5.1 环境变量在服务端的三种坑

这个项目在上线阶段让我最头疼的不是业务代码,而是环境变量的配置。T3项目同时包含客户端和服务端,客户端代码会打包进浏览器,所以任何以NEXT_PUBLIC_开头的变量才能暴露给前端,其他的变量默认只能在Node环境里读取。有一次我把数据库连接串写在了.env.local里,本地开发没问题,部署到服务器上时却直接报错连接不到数据库。查了半天才发现,服务器上的环境变量根本没设置,本地和线上配置不一致,这种问题不会在任何测试阶段暴露,只能靠运维兜底。

建议的做法是,把部署所需的全部变量列一个清单,包括DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL等,在上线前逐个确认服务器端已经配置好。如果用的是容器平台,环境变量要从平台后台注入,另一个容易忽略的是NEXTAUTH_URL必须与实际访问域名一致,多一个斜杠或少了https协议都会导致认证回调失败。

5.2 Next.js服务端渲染和tRPC的边界问题

T3项目默认使用Next.js的App Router,页面默认是Server Component。这意味着在Server Component里,你可以直接写await查询然后渲染,因为它跑在Node端,没有跨域问题也没有网络开销。但在Client Component里,一旦要获取数据就得走useQuery,它会通过HTTP请求到你的tRPC endpoint。

这里有一个常见的边界问题:同一个页面,Server端可以调用tRPC router里的procedure吗?答案是能,但不建议通过HTTP调用。正确做法是在服务端直接调用数据库或导出的内部函数,而不是再走一次tRPC的公共入口。我在项目里碰到过一次"服务端渲染时所有数据都能显示,但客户端交互时数据为空"的情况,原因就是tRPC的endpoint没有在客户端正确注册,或者请求路径配错了。

要时刻记住,Server Component不是浏览器的一部分,它和Client Component之间有严格的边界。写服务端代码时可以直接操作数据库,写客户端代码就只能通过tRPC暴露的API。这个思维转换,是我认为T3迁移过程中最难的一关。

5.3 一处本地正常、线上异常的经典案例

我们上线时还遇到一个问题:本地环境一切正常,部署到线上后,某些接口偶尔返回500,重启后又能正常。这种"幽灵Bug"排查起来很痛苦。

后来通过观察日志发现,错误集中在Prisma数据库连接池上。本地开发时只有你一个人访问数据库,连接池默认10个连接完全够用。线上有多个实例并发,每个实例都开一个连接池,很快数据库的连接数就满了,后进来的请求只能排队等超时。

Prisma默认的连接池数量在datasource块里看得到:

datasource db { provider = "postgresql" url = env("DATABASE_URL") }

解决办法是在连接串里加上连接池参数:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/t3db?schema=public&connection_limit=5"

另外,如果你用Serverless平台部署,每个请求实例都是独立的,连接池几乎无法复用,建议接一个连接代理层(比如PgBouncer),不然数据库连接数会非常容易被击穿。这个坑我这边踩过之后才发现,服务器的数据库连接数已经被打爆了好几轮,查日志的时候都是"remaining connection slots are reserved"。

6. 踩坑实录:我这几周遇到的具体问题

6.1 编译期没拦住业务错误的反思

虽然T3的类型安全很强大,但它也给我造成了一种"虚假的安全感"。有一个印象很深的例子:我定义了一个mutation,输入参数用了z.number(),但在前端引用的地方传的是字符串"1",因为zod默认不做类型强制转换,这个错误被成功拦下来了,很舒服。可是另一个问题它没拦住:我把一个逻辑上的"是否启用"字段在数据库里定义成了Boolean,但在前端逻辑中判断条件写成了if (user.enabled === true && user.plan === 'pro'),这个逻辑本身在业务上是错的,类型系统再强也管不了业务逻辑对不对。

类型安全的本质是保证"类型的一致性",而不是"逻辑的正确性"。我在迁移后期适应了这套思维之后,反而更注重写单测和Review了。工具再强,也只是把低级的低级错误前置暴露,真正的业务判断还是要靠人。

6.2 热更新卡顿和内存狂飙的排查

开发阶段遇到的问题也不少。项目跑了一阵子之后,保存代码热更新越来越慢,偶尔还会卡到无响应,内存占用从1.5G飙升到4G以上。排查了很久才发现是Next.js开发模式的一个已知毛病:tRPC的上下文会在请求时重新创建数据库连接,而热更新会触发多次不必要的重新初始化。

这个问题没有完美的解决办法,但我用了一套组合拳把影响降到了最低。首先是减少不必要的路由注册,把子路由挂在root router上的时候尽量按需导入,不要用循环把所有文件都加载一遍。其次是在开发模式下把日志输出级别降低,减少控制台IO阻塞。最重要的一点是,尽量把多人协作的本地数据库拆开,避免多人同时操作一张表导致锁竞争,间接拖慢热更新。

6.3 给后来者的一份实践清单

最后整理一份我认为比较通用的实践清单,任何时候把项目迁移到T3技术栈上,都可以对照着检查:

  • 初始化后用git做过一次完整提交,保证脚手架生成的基线代码可回退
  • .env和.env.production完全隔离,且生产环境变量在服务器上已经配置完毕
  • Prisma迁移文件纳入版本控制,生产环境用deploy不跑dev
  • 所有涉及多次写操作的地方都改成了$transaction
  • tRPC router按业务领域拆分,不要把所有procedure塞进一个文件里
  • Server Component里不通过HTTP调tRPC,直接调内部服务函数
  • 客户端组件只通过api对象调用接口,禁止直接在组件里fetch
  • 云数据库连接串带上connection_limit参数,避免连接池被打满
  • 生产环境部署前先跑一遍pnpm build,看有没有类型错误或服务端代码引用客户端模块的问题
  • 把tRPC相关依赖的版本锁死,不要随意升级,这个工具链还不够稳定

如果你能照着这份清单走下来,t3code这套工作流基本能稳稳跑起来。

我在把这个内部工具迁到T3技术栈之前,其实花了不少时间纠结"要不要换",当时的顾虑在于团队里没人用过这套东西,怕踩坑时间比省下的时间还多。两周实践下来,我个人的体会是:前期的学习曲线确实陡,但一旦把脚手架、数据库、环境变量和部署这套基建打通,迭代速度的提升是实打实的,类型错误减少带来的体验改善尤其明显。如果让我给个建议,我会说别拿生产项目直接试水,先拿一个小工具练手,把整条链路跑通之后,再决定要不要全面拥抱t3code这套开发方式。