1. t3code 是什么:一个围绕 T3 Stack 的实践项目
先把这个标题拆开说清楚。t3code,从字面理解,就是围绕 T3 体系做的一整套编码实践项目。T3 这个命名在目前的 Web 开发圈子里,基本指向一个特定的技术栈组合:TypeScript、Tailwind CSS、tRPC,后面再挂上 Next.js 当作主框架,配上 Prisma 管数据库、NextAuth 做认证,整套组合在一起,就是社区里经常提到的 T3 Stack,也叫 T3 技术栈。
我最初听到 T3 这个概念的时候,第一反应是这又搞了个新框架。后来真正上手才发现,它并不是一个具体框架,而是一套把现代全栈开发中"最不容易出错"的组件组合在一起的使用哲学。t3code 这个名字的做法,本质上就是在把 T3 这套东西落地成真实项目,拿 Next.js 做应用外壳,TypeScript 做全链路的类型约束,tRPC 打通前后端的类型安全接口,再配合 Tailwind 做样式系统,Prisma 操作数据库。
这个概念能解决的问题很实在:前后端类型不统一导致的联调痛苦、API 文档维护成本高、数据库表和类型定义来回同步的体力活。传统做法里,后端写一套接口文档,前端照着再定义一遍类型,两边一有改动,文档和代码就开始脱节,联调时经常因为字段名拼错、数据类型对不上浪费大量时间。T3 这套组合从源头规避了这些问题,因为类型是由代码生成并全程共享的,不太需要人为维护一份可能过时的接口契约。
这篇文章适合三类读者:第一类,一直在用 Next.js 做前端,但还没尝试过全栈集成,想看看 tRPC 怎么把开发体验提升一个档次;第二类,后端出身,对 Prisma 和数据库设计比较熟,但想了解前端框架和样式方案怎么配合;第三类,刚接触 TypeScript 不久,想找一个完整的、能落地的项目来练习,t3code 类型的项目是很好的学习素材,因为它每个环节都有类型约束兜底,出错了编译器会直接告诉你。
2. T3 Stack 技术选型拆解:为什么是这六个核心
很多初学者看到"全家桶"式的技术选型,第一反应是这是不是过度设计。我个人的理解是,T3 这套组合不是单纯堆料,而是围绕一个目标:让类型安全贯穿应用开发的每一个环节。下面逐个拆开讲。
2.1 TypeScript:全栈类型安全的地基
T3 这个 T 之所以排在第一位,是因为整个技术栈的核心逻辑都是从 TypeScript 出发的。你可能会想,没有 TypeScript 是不是也能用 Next.js、tRPC 这些工具?当然可以,但优势会打很大折扣。T3 栈的核心理念之一是"类型即契约",后端定义好的类型,前端从过程性代码里推导出来,调用时自动提示,改字段时编译器直接报错。
这里的生活化类比是:TypeScript 相当于施工图纸,没有图纸你也能拿砖头盖个房子,但盖到三楼时你想改一楼的一堵墙,没有图纸就必须靠记忆去猜哪些地方会受影响,改完大概率会出问题。TypeScript 的严格模式配合项目里的共享包,实际上就是给了你一张自动更新的图纸。
t3code 这类项目里有一个细节值得注意,就是严格模式下对"unknown"和"any"的处理。很多刚转 TS 的开发者图省事,遇到复杂类型就丢一个 any,这在普通前端项目里可能混得过去,但在 T3 栈里会把 tRPC 的类型推导整个拖下水,因为你一旦用了 any 或 unknown 做中间层,后续所有从那里推导出来的类型都会失去保护。
2.2 Next.js:应用框架的骨架
Next.js 在 T3 栈里的角色,我倾向于理解成:它负责应用的路由、页面渲染、服务端逻辑承载等基础设施。Next.js 13 之后的 App Router 引入了 Server Component 和 Client Component 的区分,这一点和 T3 栈是天然契合的。
创建 T3 项目时,默认就会选择 App Router。为什么?因为 tRPC 的服务端逻辑其实就是挂在 Next.js 的路由处理器(route handler)下面,App Router 的这一层抽象,让你可以把 API 逻辑和页面逻辑一起放在同一个应用里,而不需要单独起一个 Node 服务。
我在实际使用 t3code 类似项目的过程中体会到,Next.js 做全栈的好处是:你不需要维护两个 Git 仓库、两套部署流程、两个域名。前端页面、后端接口、数据库操作代码放在同一个代码库里,共享同一套类型定义,改一处提交就能同时更新前后端逻辑,部署时也只需要一个环境。
2.3 Tailwind CSS:样式不阻塞开发
T3 技术栈里被低估的是 Tailwind,因为它的角色不是"能不能写样式"这么简单,而是"样式系统的侵入性到底有多低"。很多人对 Tailwind 的第一印象是"一堆工具类名,代码看着乱",但实际用久了你会意识到,它对开发心智负担的削减很可观。
Tailwind 的价值在工作流层面体现得更明显:你在做 t3code 这类全栈项目时,代码的注意力应该集中在类型安全、接口设计、数据库结构上,而不是花时间纠结 CSS 类名怎么起、样式文件放哪里、scoped 怎么写。直接用工具类在组件内部解决问题,样式层就不太会形成独立的复杂度。
另外 Tailwind 的 JIT 模式(即时编译)对构建速度很友好,顺带支持了深色模式的切换,写暗色主题时直接用dark:前缀,不需要维护独立的主题样式文件。这对小团队和个人项目开发者是很现实的好处。
2.4 tRPC:端到端类型安全的 API 层
tRPC 是 T3 栈里最核心,也是最容易被误解的一环。很多人一听会觉得这是不是又一种 GraphQL,其实不是。tRPC 做的事情很简单:让你像调用本地函数一样调用后端的接口,但类型是完全共享的。前端写一个createPost(...)调用,它能自动知道参数应该是什么结构、返回值是什么类型,不需要手写接口文档,也不需要手动维护请求函数。
tRPC 的实现原理并不玄乎:它用 TypeScript 的泛型和类型推导能力,把后端路由定义好的 procedure(也就是接口)类型信息"投影"到前端。你后端定义了一个输入类型为{ title: string, content: string }的 mutation,前端 import 对应的 hooks 方法后,参数的类型提示和编译期校验就自动生效了。
我真实感受最深的一次,是给 t3code 项目加一个"获取当前用户信息"的接口。传统方式下我要写后端路由、写前端请求封装、写类型定义,然后两边联调时还容易因为命名不一致出 404 或类型报错。用 tRPC 的写法,我只需要在服务端 router 里加一个 procedure,然后在前端页面里直接调用trpc.auth.me.useQuery(),返回值就会被自动推断成用户对象的类型,不再需要任何手动类型的对接。
2.5 Prisma:数据库操作的类型安全
Prisma 在这套栈里的角色是"数据访问层"。它通过 schema 文件定义数据模型,然后生成对应的 TypeScript 类型和数据库客户端。这里的核心价值在于:数据库表结构变成了一份类型安全的 ORM 客户端,你在写查询的时候,传入的字段名和 where 条件都会有编译期校验。
举个例子,在 t3code 类似项目里写一个按邮箱查用户的逻辑,Prisma 的写法是prisma.user.findUnique({ where: { email } })。如果你的email字段名写成了mail,编译器会直接告诉你不行,而不是等到运行时数据库报错。这对于刚从裸 SQL 或非类型安全 ORM 转过来的开发者来说,体验上是一个很明显的提升。
另外 Prisma 还有一个非常好用的能力:migration(数据库迁移)。它会把 schema 文件的变更生成 SQL 迁移文件,团队成员只需执行prisma migrate deploy,就能把数据库结构同步到最新版本,不需要手工去各个环境执行 SQL 脚本。
2.6 NextAuth:认证方案
NextAuth(也就是现在的 Auth.js)负责认证流程。T3 栈推荐 NextAuth 的原因,倒不是说它功能多么全,而是它的设计思路不强制绑定某一家身份服务商。你可以在本地用邮箱+密码的方式配合 Credentials Provider,也可以接 GitHub、Google 等 OAuth Provider,后续再无缝切换。
认证这块在整个 t3code 项目里的重要性,很多时候要到你把功能上线,开始接受真实用户注册的时候才能真正体会。自己在用户系统上瞎搞加密存密码、手工拼 Session,出安全问题的风险太高,不值得省这个功夫。
我还想多说一句,T3 选型的核心理念是"循序渐进"。官方文档里反复强调一个词——boring(无聊)。T3 追求的就是"这套技术组合已经成熟到有点无聊",每一个组件的边界和职责是清晰的,你不需要在做业务功能时分心去发明轮子。
3. 从零搭建 t3code:create-t3-app 实操全流程
前面把 T3 技术栈的各成员角色讲清楚了,接下来就是大家最关心的部分:怎么把一个 t3code 类型的项目从零建起来,并且跑通一个完整的全栈功能。我以自己的实操过程为例,带大家走一遍完整流程。
3.1 初始化项目,选择你要的模块
T3 社区提供了一个脚手架工具create-t3-app,可以快速生成一个按照 T3 Stack 最佳实践配置好的项目骨架,省去手动集成的时间。
npm create t3-app@latest执行这条命令后,它会提示你输入项目名称,比如我这里输入my-t3code-app,然后会让你选择要包含的模块。我的建议是,不要贪多。如果你还没有明确要用到数据库,可以直接不带 Prisma 启动,后续需要再加也不迟。我实际用的组合是:
- Next.js(App Router)
- TypeScript
- Tailwind CSS
- tRPC
- Prisma
- NextAuth
一次性把六个模块全选上,可以在一个项目里完整体验 T3 的全链路类型安全。模块选完后脚手架会自动安装依赖并生成项目文件,等待时间取决于你的网络状况。
启动项目的命令很简单:
cd my-t3code-app npm run dev然后浏览器打开http://localhost:3000,你会看到一个默认的 T3 首页。它的页面源码里包含了一些 Tailwind 样式演示和基础布局,通常是符合 T3 视觉风格的深色背景加渐变文字效果。到这一步,一个基本的骨架就立起来了。
3.2 环境变量配置与 Prisma 初始化
项目生成之后,你会在根目录看到一个.env文件,里面默认会有DATABASE_URL、NEXTAUTH_SECRET等几个环境变量的占位。你需要把DATABASE_URL改成自己数据库的真实地址。以 PostgreSQL 为例,t3code 项目里常见的形式是:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/t3code_db?schema=public" NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="your-secret-here"NEXTAUTH_SECRET是用来加密 session 的密钥,生产环境必须换成一个足够强的随机字符串,不能在多个环境共用。你可以用 openssl 命令生成一个:
openssl rand -base64 32数据库连接串配置好之后,还需要检查 Prisma schema 文件,它的默认位置是prisma/schema.prisma。默认生成的项目里通常会有一个示例模型(比如 Post),你先执行迁移命令把表建好:
npx prisma migrate dev --name init这一步会做两件事:根据 schema 生成迁移 SQL 并执行到数据库,同时生成 Prisma Client 的 TypeScript 类型。如果你改了 schema 文件,之后每次都要重新跑一下迁移命令,让数据库和 Prisma Client 保持同步。
按 T3 的约定,Prisma Client 会被封装成一个单例工具类,放在src/server/db.ts里。项目里所有依赖数据库的地方都会从这个工具类导入db,而不是直接new PrismaClient()。有些旧项目或教程会在多个模块里直接实例化 Prisma Client,导致开发模式下连接数膨胀甚至报错。T3 脚手架默认就做对了这件事,不需要你手动去封装。
3.3 从写一个用户注册接口开始,理解 tRPC 的工作流
骨架跑通后,我拿"用户注册"这个最常见的需求来演示 tRPC 的完整工作流。T3 项目里,tRPC 的定义文件分布在两个目录:src/server/api/放路由定义和根路由聚合,src/trpc/放 React hooks 封装和类型导出。
打开src/server/api/root.ts,你会看到一个createCaller函数和一个trpcRouter包裹后的appRouter。我们新建一个用户注册相关的 procedure,通常的做法是在routers/目录下新建一个auth.ts文件:
import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { TRPCError } from "@trpc/server"; export const authRouter = createTRPCRouter({ register: publicProcedure .input( z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().min(1), }) ) .mutation(async ({ ctx, input }) => { // 1. 查一下邮箱是否已被注册 const existing = await ctx.db.user.findUnique({ where: { email: input.email }, }); if (existing) { throw new TRPCError({ code: "CONFLICT", message: "该邮箱已被注册", }); } // 2. 创建用户(密码哈希这里省略,生产环境请务必加盐哈希) const user = await ctx.db.user.create({ data: { email: input.email, passwordHash: input.password, name: input.name, }, }); // 3. 返回去掉敏感信息的用户对象 return { id: user.id, email: user.email, name: user.name, }; }), });注意我用了zod来做输入校验。T3 项目里 zod 是默认集成的,它可以直接配合 tRPC 做参数验证。z.object定义的结构,在 tRPC 内部会自动转成类型,所以前端调用register这个 mutation 时,参数类型会精确到{ email: string; password: string; name: string },并且这些校验逻辑只写一处,前后端共用同一套约束。
写完后,你还需要把authRouter挂到root.ts的appRouter里:
import { authRouter } from "./routers/auth"; export const appRouter = createTRPCAppRouter({ auth: authRouter, });这样authRouter就注册成了auth.*命名空间下面的路由了。
3.4 前端页面接入 API:React Query 与 tRPC hooks
后端 procedure 定义好之后,前端接入的部分可能是整个 T3 项目里体验最爽的环节。T3 脚手架默认集成了@tanstack/react-query作为请求状态管理,tRPC 有一层封装让 API 调用变得像用 React hooks 一样自然。
在src/app/register/page.tsx(如果用 App Router 按路由建页面)这样的客户端组件里这样写:
"use client"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { api } from "~/trpc/react"; const schema = z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().min(1), }); export default function RegisterPage() { const registerMutation = api.auth.register.useMutation({ onSuccess: (data) => { console.log("注册成功", data); // 做业务处理:跳转登录页 / 自动登录等 }, onError: (error) => { alert(error.message); }, }); // react-hook-form 的接入 const form = useForm({ resolver: zodResolver(schema), }); const onSubmit = (values: z.infer<typeof schema>) => { registerMutation.mutate(values); }; return ( <form onSubmit={form.handleSubmit(onSubmit)}> <input {...form.register("email")} placeholder="邮箱" /> <input {...form.register("password")} type="password" placeholder="密码" /> <input {...form.register("name")} placeholder="昵称" /> <button type="submit" disabled={registerMutation.isLoading}> {registerMutation.isLoading ? "注册中..." : "注册"} </button> </form> ); }api.auth.register.useMutation()这个 hook 是从~/trpc/react里导出的,它是 T3 项目自动生成的类型安全 tRPC 客户端。这里但凡register在服务端定义了 zod 校验规则,前端mutate函数的参数类型就会被推导出来。字段名写错,编辑器直接报红。
你可能会疑惑,registerMutation里的isLoading、error、data这些状态是从哪来的。tRPC 的useMutation内部基于 React Query 实现,所以它天然继承了 React Query 的isLoading、isError、mutateAsync、reset等能力,你不需要再手动封装一个请求状态管理。
T3 项目的整个数据流是:组件调用 tRPC hooks,tRPC 客户端通过 HTTP 请求调用 Next.js 上的 API route(通常是/api/trpc/*),然后由appRouter分解到对应的 procedure,为 procedure 内部的ctx.db执行 Prisma 查询数据库,数据再沿原路返回,类型一路由编译器保驾护航。
我也建议在跑通这个流程后,顺手把官方文档里的 query 部分再看一遍,因为 tRPC 里useQuery的用法跟useMutation略有不同。useQuery调用时机、依赖更新、缓存失效策略的处理非常影响全局体验。t3code 这类项目跑上一阵子后,你也需要提前思考如何清缓存、如何做乐观更新,这些都不是凭空掌握的,而是在项目里多做几个功能慢慢体会的。
3.5 实现一个"查询当前用户"的接口,体会受保护路由
注册用户之后,你很可能需要一个获取当前登录用户信息的接口,这也涉及到 T3 项目中如何处理认证状态与受保护路由。
在 tRPC 中,protectedProcedure是预留的概念,如果你创建项目时选了 NextAuth,src/server/api/trpc.ts里已经有protectedProcedure的实现了。它的含义是"没有登录就拒绝访问"。它的实现逻辑简单直白:从session.user中读取当前登录用户,如果不存在就直接抛UNAUTHORIZED错误,如果存在则从数据库里把用户信息取出并放进ctx,后续逻辑可以直接用。
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; export const authRouter = createTRPCRouter({ me: protectedProcedure.query(async ({ ctx }) => { return ctx.db.user.findUnique({ where: { id: ctx.session.user.id }, select: { id: true, email: true, name: true }, }); }), });到这里,你已经完成了"注册用户—查询当前用户"这一套最基础的认证闭环。接下来再加上 NextAuth 的登录页面、Session 提供者和 OAuth 配置,一个完整的用户系统就成型了。
4. 踩坑实录:我在这几个地方花了最多时间
作为一个实操型博主,我在做 t3code 类似的 T3 项目时踩过的坑,值得单独拎出来说。很多坑都不是语法错误,而是缺乏对这套工具链协作方式的理解。
4.1 Prisma Client 没生成,类型一直报红
这个问题出现频率最高,尤其是 clone 项目或换了电脑继续开发的时候。现象是:编辑器里所有prisma.user.create这类调用都标红,PrismaClient无法导入,但你明明执行过 npm install。
原因其实不复杂:Prisma 的 TypeScript 类型和客户端代码不是安装依赖时自动生成的,而是prisma generate生成的。create-t3-app 生成的模板默认在 postinstall 里会有生成步骤,但如果你改动了schema.prisma或者用了git clone的方式,旧生成的客户端可能已经过时或不存在。
解决办法:
npx prisma generate如果执行过程中提示找不到 schema 文件,就用--schema参数指定:
npx prisma generate --schema prisma/schema.prisma我也建议把这行命令加到package.json的 scripts 里,比如"postinstall": "prisma generate",这样团队新成员 clone 代码后执行 npm install 时,Prisma Client 也会自动生成出来,减少不必要的沟通成本。
4.2 tRPC infer 类型老是不对,怎么排查
用 tRPC 时很少遇到类型完全不通的情况,但一旦遇到,往往会让人挠头一阵子。常见症状是:在某个组件里使用apihooks 时,方法的参数类型和返回值类型是any,或者干脆显示错误。
这个问题的根源通常是:你没有把AppRouter类型正确传递到createTRPCReact。T3 项目中,src/trpc/react.tsx会从AppRouter类型创建客户端实例,src/trpc/server.ts则负责服务端调用。如果这两处引用的类型不是同一个,类型推导就会断掉。
一个排查思路是:确认~/server/api/root.ts里的appRouter被正确导出为类型AppRouter,并且src/trpc/react.tsx里的泛型参数用的是AppRouter而非某个具体 router。如果还是报错,直接把相关文件用注释逐一关闭,缩小问题范围,通常很快就能定位。
我觉得 tRPC 的类型系统有一个心智模型值得记住:类型是沿着"router 定义"流动的,而不是沿着"运行时请求"流动的。只要服务端 router 的类型能推断到什么程度,前端就能拿到什么程度。所以排查类型问题时盯住 router 定义即可,组件侧不用反复折腾。
4.3 部署到生产环境时,环境变量和构建顺序的坑
T3 项目本地跑得欢,一部署就报错是家常便饭。最常见的三个问题:
环境变量缺失:t3code 这类项目依赖的
DATABASE_URL、NEXTAUTH_SECRET、AUTH_SECRET在生产环境各自单独配置。如果你的部署平台(比如 Vercel 或 Docker 容器)没有设置这些变量,构建时可能不报错,但运行时接口会直接 500。我建议在项目的src/env.js里严格校验所有环境变量,让缺失在启动阶段就报错,而不是等到页面访问时才炸。Prisma migrate 没有执行:很多人在本地
prisma migrate dev执行过,但生产数据库没有执行对应的迁移,表结构不存在,部署后一查数据库就崩。生产环境的正确姿势是:
npx prisma migrate deploymigrate deploy是幂等操作,它只执行未迁移过的历史迁移文件,不会像dev模式那样清库或生成新的迁移,适合在 CI/CD 的构建阶段执行。
- 构建时无法连接数据库:如果你的部署流程是在服务器上跑
next build,而构建过程中有页面逻辑会去读取数据库(比如静态生成时调用了 tRPC query),那么构建阶段就需要一个可访问的数据库。这一条特别容易踩,因为本地开发时数据库就在 localhost,但生产环境的构建机可能连不上内网数据库。我的建议是:不要在 getStaticProps 或服务端渲染代码里直接触发数据库查询,如果有必要,也要确保该代码在构建时能被外部数据库访问。
4.4 常见问题速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Prisma 类型红 | 客户端未生成或过期 | 执行npx prisma generate |
| tRPC hooks 无类型 | AppRouter 类型未正确传递 | 检查trpc/react.tsx的泛型引用 |
| 生产环境接口 500 | 环境变量缺失 | 在src/env.js配置校验 |
| 数据库表不存在 | 生产库未执行迁移 | 执行npx prisma migrate deploy |
| NextAuth 登录回跳失败 | AUTH_URL或NEXTAUTH_URL和实际域名不一致 | 检查环境变量是否与线上一致 |
| Tailwind 样式不生效 | 内容路径配置缺了大括号,未包含新目录 | 参照tailwind.config.ts默认配置补齐 glob 规则 |
| react-query 缓存一直不更新 | mutation 成功后没有使缓存失效 | 在useMutation的onSuccess里调用utils.post.getAll.invalidate() |
最后一栏是实际项目中经常会遇到的一个隐性坑。你用了 tRPC 的useQuery去获取列表,之后执行一个 mutation 修改了数据,页面上的数据如果不变,多半是 React Query 的缓存没有失效。在 T3 项目里,invoke 一条数据的失效方法尤其管用,比如:
const utils = api.useUtils(); await utils.user.getById.invalidate({ id: userId });这样指定了参数的失效,比全量清除缓存更高效。
4.5 我的独家避坑心得
说完具体常见问题之后,我再分享三个平时文档里不太会写到的实战心得。
先写 schema,再写页面。这是我在 t3code 项目里反复体会到的顺序。T3 栈的类型安全是一层衔接一层的,schema 是 Prisma 的全部语义来源,而 tRPC 的输出又依赖 Prisma 的返回类型。如果你先写页面、临时拼字段,后面再改 schema,很多类型和查询逻辑都得推倒重来,成本比一开始就把 schema 定义清楚高出不少。
保证 zod schema 和 Prisma schema 的一致性。这两者是 T3 项目里最容易出现重复描述的地方。zod管输入,prisma.schema管持久化。像"邮箱格式"这种校验规则只放 zod,"唯一索引"这种数据库约束只放 Prisma。当你需要修改一个字段时,记得同时检查两处是否需要同步更新。比较推荐的联动方式是在 procedure 内部直接把 Prisma 的select或include结果返回,让返回类型自动随数据库模型走,避免手动维护两份输出类型。
遇到类型推导混乱时,先跑一遍tsc --noEmit。有时候编辑器扩展的报错不一定准,或者出现了跨文件提示异常,用命令行重新检查整个项目可以排除缓存干扰,可能会发现真实问题其实在其他文件里。养成在 CI 构建前加一道 TypeScript 类型检查的习惯,能让很多问题在上线前就暴露出来。T3 脚手架默认的构建脚本里已经有next build,不过我还喜欢再加一个独立的typecheck脚本专门做检查,这样本地调试时可以分开使用。
我最初做 t3code 类似的项目时有段时期会频繁因为小问题打断思路,后来发现绝大部分原因都是类型系统在背后给了太多帮助,反而让我忽略了"服务器实际发的请求是什么样"这一层。这里有个建议:如果你始终搞不清请求状态、请求体字段,可以打开浏览器开发者工具 Network 面板,看 tRPC 的实际载荷,很多问题反而更容易定位出来。
5. 做完一个 t3code 项目后的真实体会
写到这里,你可能已经对 t3code 这类 T3 技术栈项目具备怎样的全貌有了一个比较清楚的认识。它最大的优势,在于让你不再关心"接口对接"这件在传统前后端分离开发中极度消耗精力的事情,而是把注意力重新放回功能本身。
但我也要坦诚说的是,T3 这套组合并不适合所有场景。如果你的团队前、后端是严格分工的,后端需要同时面向多个渠道(Web、小程序、App)提供接口,那独立 API 服务可能更适合你;如果你的业务有大量非 JavaScript 生态的伙伴参与,tRPC 的"共享类型"优势也会被削弱。说到底,T3 栈最适合的场景,正是像 t3code 这样追求"全栈一人搞定"的实践项目,或者小团队内部快速迭代的工具型产品。
在做这个项目的过程中,我最享受到的,是在增量修改一个字段后,编译器立刻帮我找出所有需要同步改动的位置。那种"改一处,全局自动响应"的感觉,是长期写普通 CRUD 开发时很难体会到的。我也越来越倾向于把 tRPC 当作一种开发范式来理解,而不只是一种 RPC 工具。
如果你正打算动手做一个全栈项目,我建议直接从 create-t3-app 开始跑一个最小闭环:建表、写一个 query、再写一个 mutation、配一页前端,走完这条链路后,你对整个 T3 的工作方式会有更立体的理解。后面再叠加认证、文件上传、部署这些环节时,应该会顺畅很多。