理解t3code:基于T3 Stack的全栈类型安全工程实践与落地
发布时间:2026/10/28 16:09:41 锦皓数字建站

第一次看到 t3code 这个名字很多人第一反应是又一个开源新框架其实它并不是一个独立框架而是现在前端圈讨论度非常高的 T3 Stack 那套全栈类型安全方案的代码实践代称。T3 取的是 TypeScript、Tailwind、tRPC 三个以 T 开头的关键技术的首字母再配上 Next.js 作为应用底座t3code 在我和身边团队眼里就是把这套技术栈内化之后沉淀下来的一整套工程规范目录怎么分、数据从哪进从哪出、类型在哪一层统一、前后端契约怎么定、部署怎么走。这篇文章就是把这套东西的来龙去脉、落地细节和踩过的坑完整写出来无论你是刚接触全栈开发的新手还是想优化团队协作流程的老手都能在这里找到可以直接抄作业的部分。1. t3code 到底是什么先搞懂它要解决的问题1.1 T3 Stack 的来龙去脉t3code 的核心底座是 T3 Stack这套组合最早由开发者 Theo 在 2022 年前后带火它的理念非常直接在一个 Next.js 应用里用 TypeScript 做全站类型约束用 Tailwind CSS 解决样式问题用 tRPC 把前端调用后端接口的过程做成天然类型安全的通道。为什么叫 T3因为 TypeScript、Tailwind、tRPC 这三个首字母都是 TNext.js 作为承载它们的主框架反倒成了背景板。很多人容易误解 T3 是一个新框架实际上它更像一套优选组合方案。T3 官网给过一句很有名的话The best way to build a type-safe full-stack web app直译就是构建类型安全全栈应用的最佳方式。这句话有点狂但实操下来确实有它狂的资本。我最初看到这套组合时内心是有点不屑的。TypeScript 我会Tailwind 我也用Next.js 更是熟再加一个 tRPC 能有多大变化真正把项目从 REST axios JWT 的旧模式切过来后才发现最大的变化不在某个单一技术而在整套体系跑起来之后那种前后端类型处处对齐的开发体验。它解决的不是单个问题而是让一整条开发链路里最容易出错的信息传递环节从人肉保证变成了编译器保证。1.2 为什么偏偏是这四个而不是别的这里逐个拆一下选型逻辑方便你理解为什么 t3code 会选择这套组合而不是更传统的 Vue Koa MySQL 或者 React Express REST。Next.js 承担的是应用骨架。它有文件路由、SSR、SSG、API Routes以及后来的 App Router 和 Server Components。对于中小型全栈项目Next.js 一个应用就能搞定前端页面、服务端渲染、接口逻辑、部署配置不需要单独维护一个前端项目和一个后端项目心智负担小很多。尤其适合两个人开发一个内部工具、三个人维护一个面向用户的 SaaS 这种场景。TypeScript 是这条链路的粘合剂。它的作用远不止变量有类型在 t3code 里类型是前后端共同的契约。你在后端定义了某个输入参数的结构前端调用时编辑器里能直接提示字段名和类型漏传、多传、传错类型都被拦截在编译阶段。大量联调时接口字段写错了后端返回的字段名和文档不一致这类问题从根源上消失。Tailwind 是样式层面的效率工具。为什么选它而不是写传统 CSS 或者 CSS Modules核心原因是原子化类名让样式与组件结构保持在同一文件不需要频繁切换文件、起类名、排查样式污染。配合 JIT 编译生产包只包含实际用到的类体积可控。如果你不太习惯纯原子类名也可以在 t3code 规范里加上组件化的方式封装样式这点后面细说。tRPC 是整套方案的灵魂。它不像 REST 那样需要你定义 URL、Method、Request Body、Response Body再在前后端各维护一套接口文档。tRPC 直接导出一个 router里面的每个 procedure 就是一个可调用接口前端拿到同一个类型定义直接调用。它不重新发明传输协议底层还是 HTTP JSON但它把接口开发这件事的体验拉到了接近写本地函数调用的程度。为了更直观看差别我整理了一个对比表维度传统 REST 手动类型tRPC接口定义URL、Method、参数校验各写一遍Router Procedure 一处定义前后端契约手写类型 / 文档 / 生成代码从服务端自动推导到客户端参数变更后端改前端跟着改还容易漏改了服务端前端编译直接报错提醒联调成本经常需要 mock、抓包、沟通基本跳到编译层就暴露问题1.3 t3code 的另一层含义代码规范与协作边界除了技术选型t3code 在我这里还承载了一层团队协作约定的含义。很多团队用 T3 Stack 之后依然写得很乱有人把业务逻辑写在页面组件里有人直接在客户端组件里调数据库有人把 tRPC 当 axios 用还有人把 Tailwind 类名堆成灾难现场。所以 t3code 这个实践代称我习惯把它理解成三层约束目录约束src 下面怎么分 modules、pages、server、shared哪一层放什么代码。数据流约束客户端组件通过 tRPC 拿数据服务端组件和 Server Action 才能直接操作 ORM禁止绕过分层跨层调用。类型约束所有跨边界的数据结构都必须先定义类型参数校验统一用 zod不出现 any 满天飞。有了这三层约束t3code 就不只是用了一些新技术而是一套可以被新人快速上手、被审核者快速 review、被编译器兜底的工程规范。这也是这篇博文想传递给你的核心价值不止教会你搭脚手架更想让你理解每行规范背后的代价和收益。2. 从零搭建一套 t3code 风格项目脚手架与目录设计2.1 create-t3-app 初始化一分钟起步搭建 T3 Stack 项目最省力的方式是官方脚手架 create-t3-app。我在本地实测的命令是这样的pnpm create t3-applatest my-t3-app执行之后它会交互式问你选哪些功能模块常见选项包括 Next.js、Tailwind CSS、Prisma、tRPC、NextAuth、Drizzle。我第一次建项目时全部勾选结果生成的代码里有大量用不到的示例页面反而干扰理解。建议你第一次尝试时只勾选核心四项Next.js、TypeScript、Tailwind、tRPC后续需要 ORM 或鉴权再手动加入。初始化完成后的目录结构大致像这样my-t3-app/ ├── prisma/ # 数据库模型 ├── src/ │ ├── pages/ # Next.js 页面路由 │ ├── server/ │ │ ├── api/ # tRPC 路由入口 │ │ ├── auth/ # NextAuth 配置 │ │ └── db/ # Prisma Client 实例 │ └── styles/ # 全局样式 ├── .env # 环境变量 ├── next.config.js ├── package.json └── tsconfig.json这个目录结构看起来简单但它对什么代码放哪有很强的引导性。tRPC 相关的文件集中在 server/api数据库配置集中在 server/db页面只管渲染和调用不直接碰 ORM。新进来的人只需要看几个入口文件就能搞明白项目的数据流这比注释一百行文档都管用。2.2 核心依赖与配置文件为什么依赖这么少T3 Stack 给人最直观的感受是依赖极少。一个传统的 React 全栈项目往往要装几十个包axios、react-query、react-hook-form、zod、jotai、redux-toolkit……而一个 t3code 项目核心依赖可能就十来个。原因在于 tRPC 内部帮我们做了大量集成工作。我随便打开一个 t3code 项目的 package.json核心依赖大概是这些{ dependencies: { tanstack/react-query: ^5, trpc/client: ^11, trpc/next: ^11, trpc/react-query: ^11, trpc/server: ^11, next: ^15, react: ^19, superjson: ^2, zod: ^3 }, devDependencies: { typescript: ^5, tailwindcss: ^3, eslint: ^9, prisma: ^6 } }也许你会问react-query 不是额外依赖吗是的但它是 tRPC 客户端整合 React 状态管理的基础。tRPC 的 useQuery、useMutation 本质上是构建在 tanstack/react-query 之上的所以你省掉的是手动管理请求中、成功、失败状态那一大堆样板代码。superjson 这个依赖也需要专门说一下。tRPC 默认用 JSON 序列化数据但 JSON 没法直接表达 Date、Map、BigInt 这类类型。superjson 能把它们序列化成可传输的格式并在另一端还原于是你在前端拿到后端返回的Date对象时它就是真正的Date实例可以直接调.toISOString()不需要再new Date(item.createdAt)。这个体验在写列表页时间显示时特别舒服。2.3 目录结构与数据流一次请求的完整路径理解 t3code 项目最重要的是理解一次请求从界面到数据库的完整路径。我在给新人讲这套结构时会画这样一条链路React 组件客户端 ↓ 调用 tRPC 客户端 proxy tRPC 客户端自动携带类型定义、序列化 ↓ HTTP/2 或 HTTP1.1 JSON 请求 Next.js API Route/api/trpc 入口 ↓ 转发到 tRPC 服务端 router tRPC Procedure校验 input、执行逻辑 ↓ 调用 Prisma Client 数据库SQLite / PostgreSQL / MySQL在这条链路里类型安全是贯穿始终的。服务端 router 的返回类型会被自动推导到客户端你在组件里.useQuery()拿到的数据IDE 会自动提示字段不需要任何手写 interface。举个例子服务端有一个 todoRouter// src/server/api/routers/todo.ts import { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; export const todoRouter createTRPCRouter({ getAll: publicProcedure.query(async ({ ctx }) { return ctx.db.todo.findMany({ orderBy: { createdAt: desc }, }); }), add: publicProcedure .input(z.object({ title: z.string().min(1).max(50) })) .mutation(async ({ ctx, input }) { return ctx.db.todo.create({ data: { title: input.title, done: false }, }); }), });在客户端组件里可以直接这样调用import { api } from ~/utils/api; export function TodoList() { const { data: todos, isLoading } api.todo.getAll.useQuery(); if (isLoading) return p加载中.../p; return ( ul {todos?.map((todo) ( li key{todo.id}{todo.title}/li ))} /ul ); }注意几个细节api.todo.getAll.useQuery()这个调用链路实际上把路由名 入参类型 返回值类型全部绑定在了一起。如果后端的getAll返回类型改了比如从Todo[]改成了{ list: Todo[] }前端这段代码会立刻编译报错提醒你去处理页面取值逻辑。这个过程不需要任何额外工具纯靠 TypeScript 的类型推断。这种体验的实际意义在于它大幅减少了前后端定义的漂移。传统 REST 架构里前端 interface 和后端实体是两个独立的定义维护成本高还经常出现后端加了字段前端文档没更新的情况。t3code 里所有跨端类型都只存在于一行一源的定义中改一处处处生效。3. 核心功能实战一套可复现的 CRUD 权限 部署3.1 数据模型先行用 Prisma 定义业务实体t3code 项目里我习惯先从 Prisma schema 开始因为数据模型是整个系统的骨架。这里用一个待办事项的场景演示麻雀虽小但五脏俱全。// prisma/schema.prisma generator client { provider prisma-client-js } datasource db { provider sqlite url env(DATABASE_URL) } model Todo { id String id default(cuid()) title String done Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt userId String user User relation(fields: [userId], references: [id]) } model User { id String id default(cuid()) name String? email String unique todos Todo[] }这里我选了 SQLite 做演示因为零配置、可随时清掉重来。但在真实项目中生产环境通常会换成 PostgreSQL 或 MySQL开发环境保持 SQLite 也完全没问题Prisma 的迁移机制可以平滑切换。定义完模型后执行npx prisma migrate dev --name init这条命令会做两件事生成一个 SQL 迁移文件并应用到数据库同时重新生成 Prisma Client 类型。执行完迁移后你会在客户端代码里获得完整的类型提示比如ctx.db.todo.create会提示必须传title、done、userId等字段。这里有个细节很容易踩坑改了 Prisma schema 之后一定要重新生成 Prisma Client。只要迁移执行成功类型就会更新但如果你的 dev server 还在跑有些编辑器不会立刻刷新类型最常见的现象是明明改了字段但代码里还是报字段不存在。解决方法很简单要么重启 dev server要么手动执行npx prisma generate。3.2 用 tRPC 编写后端路由从工具函数到业务逻辑tRPC 的 router 组织方式推荐按业务域拆分而不是做一个巨大的全局 router。比如 todo 相关逻辑放todoRouter用户相关逻辑放userRouter然后在上层合并// src/server/api/root.ts import { createTRPCRouter } from ~/server/api/trpc; import { todoRouter } from ~/server/api/routers/todo; import { userRouter } from ~/server/api/routers/user; export const appRouter createTRPCRouter({ todo: todoRouter, user: userRouter, }); export type AppRouter typeof appRouter;这里AppRouter类型是整个前后端类型安全的关键。客户端正是通过这个类型自动推导出所有可调用的路由和返回类型。请不要在使用 any 或手动抽象它的结构保持它从 server 根路由直接推断是最稳妥的。在 tRPC 中procedure 分两种query用于读数据mutation用于写数据。这个划分不只是语义上的它会映射到 HTTP 的 GET 和 POST并且在 React Query 层面query 会被缓存复用mutation 在成功后可以主动触发 refetch。实际开发里我经常建议把业务校验放在 zod input 层而不是写一堆 if 判断。比如新增 Todo 时标题不能为空、长度不能超过 50直接在 input 定义里约束好调用端传错参数会得到一个非常明确的错误信息add: publicProcedure .input( z.object({ title: z.string().min(1, 标题不能为空).max(50, 标题过长), }) ) .mutation(async ({ ctx, input }) { ... })这么做的好处是校验逻辑跟随接口定义后端不用在函数体里再写一遍参数检查前端也能通过类型推断看到参数约束联调效率高很多。3.3 前端调用 tRPC类型穿透到 UI 组件接下来是前端调用部分。t3code 项目会在src/utils/api.ts里初始化一个带有类型信息的 tRPC 客户端// src/utils/api.ts import { createTRPCReact } from trpc/react-query; import type { AppRouter } from ~/server/api/root; export const api createTRPCReactAppRouter();然后你需要把 tRPC Provider 挂到应用顶层通常在_app.tsx或 App Router 的 layout 里完成import { api } from ~/utils/api; import { QueryClient, QueryClientProvider } from tanstack/react-query; import { httpBatchLink } from trpc/client; import superjson from superjson; const queryClient new QueryClient(); const trpcClient api.createClient({ links: [ httpBatchLink({ url: /api/trpc, transformer: superjson, }), ], }); export function TRPCProvider({ children }: { children: React.ReactNode }) { return ( QueryClientProvider client{queryClient} api.Provider client{trpcClient} queryClient{queryClient} {children} /api.Provider /QueryClientProvider ); }这里直接用/api/trpc作为本地接口地址开发和生产都通用不需要配置环境变量区分 baseURL这一点比传统 axios 方案省心不少。实际调用长这样function useAddTodo() { const utils api.useUtils(); return api.todo.add.useMutation({ onSuccess: () { utils.todo.getAll.invalidate(); }, }); }invalidate是 React Query 里的一个机制它表示让缓存失效并重新请求。在 tRPC 的封装下你不需要手动拼 URL、处理 loading 状态、写错误弹窗这些都被统一收进了 hook 里。团队协作时新人只要照着已有代码的模式复制一份就能很快写出风格一致的接口调用代码。3.4 接入 NextAuth 做登录鉴权与权限控制任何真实项目都绕不开登录。t3code 项目里最常见的鉴权方案是 NextAuth。先安装并创建一个配置文件// src/server/auth.ts import { NextAuthOptions } from next-auth; import GithubProvider from next-auth/providers/github; import { PrismaAdapter } from next-auth/prisma-adapter; import { db } from ~/server/db; export const authOptions: NextAuthOptions { adapter: PrismaAdapter(db), providers: [ GithubProvider({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), ], callbacks: { session({ session, user }) { if (session.user) { session.user.id user.id; } return session; }, }, };配置完 NextAuth 之后真正体现 t3code 优势的地方是tRPC 中间件可以直接读取当前登录用户并基于它做权限控制。例如只允许登录用户查看自己的 Todoimport { z } from zod; import { createTRPCRouter, protectedProcedure } from ~/server/api/trpc; export const todoRouter createTRPCRouter({ getMyTodos: protectedProcedure.query(async ({ ctx }) { return ctx.db.todo.findMany({ where: { userId: ctx.session.user.id }, }); }), });其中protectedProcedure是一个包装过的 procedure内部做了会话判断。未登录访问时会直接返回 UNAUTHORIZED 错误前端收到这个错误可以统一跳转到登录页。我强烈建议在项目一开始就把受保护路由和公开路由分开定义。不要先把所有 procedure 都写成publicProcedure等上线前再补权限那样你会漏掉一堆接口而且排查的时候特别痛苦。先定义好publicProcedure、protectedProcedure这两种基础类型后续写业务时顺手选一个安全边界从第一天就是明确的。3.5 部署方案Vercel 一键部署与 Docker 自托管t3code 项目部署有三种常见路径我分别说下适用场景。Vercel是最省心的方式。如果你的数据库用了托管服务项目推到 GitHub 后Vercel 会自动识别 Next.js 项目配置好构建命令和输出目录只要在环境变量里填好DATABASE_URL和 NextAuth 相关的密钥即可。每次 git push 到主分支就会自动构建发布回滚也方便适合团队快速迭代、产品验证阶段。Docker 自托管适合对部署环境有更高要求的用户比如数据必须放在自己机房或者需要走私有网络。一个基础的多阶段 Dockerfile 长这样FROM node:20-alpine AS base WORKDIR /app RUN corepack enable FROM base AS deps COPY package.json pnpm-lock.yaml* ./ RUN pnpm install --frozen-lockfile FROM base AS builder COPY --fromdeps /app/node_modules ./node_modules COPY . . RUN npx prisma generate RUN pnpm build FROM base AS runner ENV NODE_ENVproduction COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static COPY --frombuilder /app/prisma ./prisma EXPOSE 3000 CMD [node, server.js]这里有三个关键点构建阶段一定要执行prisma generate否则运行时 Prisma Client 缺少数据库模型定义。Next.js 需要在next.config.js中设置output: standalone这样构建产物才是一个可独立运行的服务否则next start依赖整个项目上下文。如果数据库是 SQLite需要确认数据卷挂载位置不然容器重建后数据就丢了。服务器部署还有一种轻量方案直接用next start跑在云服务器上配合 PM2 守护适合不想引入容器编排的小项目。总之部署层面 t3code 并不特殊它就是一个标准的 Next.js 应用常规方案都能覆盖到。4. 常见问题与排查技巧实录4.1 Next.js RSC 边界问题use client到底该加在哪T3 Stack 演进到 App Router 时代之后最大的心智负担就是服务端组件和客户端组件的边界。很多新人写代码会报错某个组件里用了useState或api.todo.getAll.useQuery()然后 Next.js 告诉你在服务端组件里不能使用 hooks。这里有个通俗的理解方式App Router 下所有组件默认是服务端组件只在文件顶部显式写use client的组件才会被当作客户端组件打包。我的经验是tRPC 的 useQuery 永远只在客户端组件里使用。如果某个页面是从服务端组件读取数据的就不要用 tRPC直接通过服务端代码访问 Prisma或者在服务端组件里调用 tRPC 服务端的 caller。有些团队为了图省事会把整个页面都标记为use client当时看起来没啥问题但你会失去很多服务端渲染带来的性能和 SEO 优势。更好的做法是先想清楚页面里哪些部分是纯展示服务端组件哪些部分需要交互客户端组件再决定use client的粒度。4.2 tRPC 类型不更新为什么改了 router 前端还是旧类型这个问题高发新手几乎必遇。现象是你在服务端加了一个新 procedure回到前端组件里却发现api.todo.xxx没有自动补全报类型不存在。原因基本是 tRPC 的类型是通过AppRouter从服务端推导到客户端的而 dev server 和编辑器类型检查进程缓存了旧的类型信息。解决方法按经验排序重启 dev server这一步能解决八成问题。如果还不行删除.next目录后重新pnpm dev。再不行确认是不是引入了循环依赖比如 root router 引了 todoRoutertodoRouter 又反向引用了 root router 里定义的类型这种情况会导致类型推导被阻断。还有一个容易被忽略的点如果你在src/utils/api.ts里写的是createTRPCReactAppRouter()但AppRouter是从server/api/root导入的要确保根路由文件本身没有明显错误。TypeScript 在根路由类型报错时会在前端表现为一切调用都失效。4.3 Prisma 迁移在运行时报 database error 的排查常见报错是Environment variable not found: DATABASE_URL或者Cant reach database server。前者多半是.env文件没有被加载后者如果是开发环境先检查数据库服务有没有启动SQLite 则检查路径权限。prisma migrate dev这个命令只能在开发环境用因为它允许重置数据库。如果把迁移命令写到生产环境启动脚本里很容易因为环境差异导致不可预期的问题。生产环境应该用prisma migrate deploy它只应用迁移文件不会做任何确认或重置操作。我记得有一次部署时反复重启都失败最后发现是 Docker 镜像里没有把prisma目录复制进去容器根本找不到 schema 文件。如果你也在容器里跑 Prisma务必确认镜像包含prisma/schema.prisma和迁移目录且运行时执行路径正确。4.4 时间序列化与 superjson 的边界T3 Stack 使用 superjson 解决了 Date 序列化问题但它也不是万能的。如果你直接绕过 tRPC从 Next.js API Route 返回Date对象前端接收到的依然是一个字符串。只有在 tRPC 管道路径中的传输才能应用 superjson。另外Prisma 在 SQLite 上返回的日期类型是真实的Date对象在 tRPC 链路上能保持类型正确。但如果你用了 PostgreSQL 的 timestamp 类型并启用了timestampWithTimeZone要注意时区转换。我的建议是全文统一存储 UTC展示时再转本地时区别在数据库层做时区转换。提示不要把 tRPC 的类型安全理解为数据校验安全。类型安全处理的是编译期约束运行时校验需要靠 zod。如果服务端 procedure 没有定义 input你传进去任意字段也不会报错因为 TypeScript 类型在编译后被擦除了。所以输入校验一定不要省tRPC 的魅力就在于把类型和校验放在同一个定义里用起来很顺手。4.5 Tailwind 类名失效与样式不生效的排查思路Tailwind 偶尔会遇到样式不生效排查思路非常简单先确认类名有没有拼错再确认 Tailwind 的内容扫描配置有没有把对应文件包含进去。// tailwind.config.js module.exports { content: [./src/**/*.{js,ts,jsx,tsx}], };如果新增页面不在 content 匹配范围内对应的类名就不会被生成。还有一种情况是类名优先级冲突比如 Tailwind 的bg-red-500被普通 CSS 规则覆盖常规解法是检查全局样式文件里有没有对background的高优先级定义。真遇到需要灵活处理的地方可以用 Tailwind 的任意值语法比如bg-[#f0f0f0]、w-[calc(100%-2rem)]但不要滥用太多任意值类名会让可维护性下降。4.6 NextAuth 在 Vercel 上出现回调地址错误使用 NextAuth 部署到 Vercel 之后经常遇到redirect_uri不匹配的报错原因很简单你本地用的是http://localhost:3000Vercel 上是https://xxx.vercel.app而登录服务商那边的回调地址配置的还是本地地址。解决办法是在 OAuth 服务商后台把线上域名加进回调白名单同时把环境变量里的NEXTAUTH_URL设置成线上地址。NEXTAUTH_SECRET必须给一个稳定的长随机字符串不要用默认值否则服务重启后会话会失效用户会被反复踢出登录态。5. 把这套实践搬进团队适应场景与落地步骤5.1 什么时候适合 t3code什么时候不建议硬上这套组合不是万能的我在实际调研和项目体验中总结了一些判断依据。适合的场景内部工具、后台管理系统、中小型 SaaS 产品前后端由同一个小团队甚至一个人维护。快速原型验证和 MVP 阶段需要在两周内把想法变成可用页面。团队对 TypeScript 有基本掌握愿意接受类型即契约的开发理念。不适合的场景已有庞大且稳定的 REST API 体系团队成员分散在不同项目组此时改造为 tRPC 需要付出较高的迁移成本。需要大规模对接外部系统接口五花八门tRPC 主要解决的是自家前后端契约外部系统无法共享这些类型定义。团队对 TypeScript 很抗拒或者前端工程化基础薄弱。t3code 的体验高度依赖类型推断和编辑器支持如果类型语法本身就让你头疼这套方案会放大你的痛苦。注意选技术方案不是选最热门的而是选能在团队当前阶段稳定产生收益的。t3code 的收益本质来自减少沟通成本和编译期兜底这是时间越长越明显的好处不是上线第一天就能量化的指标。5.2 我们团队落地时的四步迁移法如果你决定在现有项目引入 t3code 风格实践不建议一次推倒重来。我后来帮几个团队做迁移时用的是下面这个四步法第一步先选一个非核心模块做试点比如内部工具里的报表模块或者 SaaS 的用户设置页。目标是完整跑通页面 - tRPC - Prisma - 数据库链路同时让团队熟悉新代码组织方式。第二步定义项目内的目录规范和代码风格。我参考 t3code 默认结构定了一份精简规范页面组件放页面目录业务逻辑沉淀到 tRPC router跨模块的共享类型放 shared 目录禁止在页面里直接访问环境变量。第三步逐步替换低频模块。从读多写少的模块开始比如数据字典、配置读取、搜索接口因为这类接口对一致性要求低即使切换过程中有小问题也不影响核心业务。第四步等团队已经形成肌肉记忆后再考虑替换登录、权限这类核心模块。这时候前面积累的经验已经能覆盖大部分坑风险会小很多。5.3 把类型当作唯一契约最后的小技巧最后说一个很多人忽略的细节。t3code 项目的价值重心不在脚手架本身而在类型穿透带来的协作方式改变。实际操作中我发现一个非常有用的小技巧在创建 Procedure 时把返回类型定义为显式的 TypeScript 类型或 Prisma 返回类型的封装而不是依赖隐式推导的凌乱对象。例如import { z } from zod; import type { Todo } from prisma/client; type TodoWithUser Todo { user: { id: string; name: string }; }; export const todoRouter createTRPCRouter({ getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }): PromiseTodoWithUser | null { return ctx.db.todo.findUnique({ where: { id: input.id }, include: { user: { select: { id: true, name: true } } }, }); }), });给关键返回值写显式类型的好处是前端消费方不会因为后端多了一个include字段而导致整体类型形状变得不稳定。你是在定义接口契约而不是在暴露查询细节。这个习惯一旦养成类型就真正成了团队内部的文档。我个人的体会是t3code 用起来最舒服的时刻往往不是初见时的新鲜感而是项目进入迭代期之后改一个后端字段全链路自动提示前端拿到的数据永远符合预期新人看代码只需要看类型就能理解数据流。这套实践的价值会在三到六个月的持续迭代里显得越来越明显。如果你正在犹豫要不要全栈转向类型安全路线我的建议是找一个小的内部模块先试水半个月内你会自己找到答案。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。