news 2026/10/28 23:03:02

t3code:构建端到端类型安全的现代全栈种子工程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
t3code:构建端到端类型安全的现代全栈种子工程

我最早用 t3code 这个名字,其实只是给一套全栈种子仓库随便起的代号,没想到它后来成了我所有参考工程里最常用的起点。在这之前,我其实对"端到端类型安全"这种说法不太当回事,觉得 TypeScript 都普及到这个程度了,前后端类型还能对不上吗?直到把一个项目从接口定义一路做到前端页面,才发现问题根本不在"有没有用 TS",而在"类型到底有没有被当成一等公民来管理"。

简单说一下 t3code 是什么:这是一套现代全栈应用的种子工程,把"类型优先、按需定制、默认可测试"三个原则固化进项目配置里,基于 Next.js、tRPC、Prisma、Zod、Tailwind 这套组合,让你从第一行代码开始,前后端共享同一套类型系统。接口、校验、数据库模型之间不再脱节,后端删了一个返回字段,前端编译期直接报红。

这篇文章会把 t3code 从设计思路、选型逻辑、完整搭建过程,到我在真实业务里踩过的坑,一条条梳理清楚。适合想把自己的全栈工程做得更规范、对类型安全有执念的前后端开发者,也适合准备从脚手架转型到完整工程体系的朋友参考。

1. t3code 的设计思路与整体拆解

1.1 先搞清楚它到底要解决什么问题

很多项目表面上没毛病:用了 TypeScript、写了 API 文档、建了代码规范,但开发效率就是上不去。我在维护 t3code 之前,被这几个问题反复折磨了很多次。

第一个是类型割裂。后端接口返回的对象,前端往往要再另写一个 interface 或 type 去描述。一次两次没问题,字段一多就开始失控。后端改了一个字段名,前端还按老名字取数据,页面上一行字都渲染不出来,但编译期完全无感知,只能靠运行时 console 排查。

第二个是脚手架重复劳动。每开一个新项目,都要重新配 ESLint、Prettier、tsconfig、路径别名、环境变量模板、日志规范。这些配置我从 GitHub 上一个项目复制到另一个项目,每次都要修半天,修完发现版本还不一致。

第三个问题更隐蔽,就是"约定只存在于文档里"。团队规定 API 参数必须校验、数据库访问必须走统一封装,但规范文档放在 Wiki 里吃灰,代码评审靠人盯,只要有一个成员图省事,规则就破了。

t3code 的设计目标,就是要让这些约定从"人自觉"变成"系统强制"。类型不对编译不过,参数不校验写不进 router,格式不统一连提交都不让提。把规矩融入工具链,而不是印在文档里。

1.2 三个 T 的含义:Type-first、Tailored、Testable

t3code 里的 t3,不是我随便凑的三个字母,它代表三条设计原则。

第一条是 Type-first,类型优先。数据从数据库模型、API 入参出参、前端组件状态,整条链路只有一个类型源头。数据库 Schema 改了,Prisma Client 类型自动变;Router 接口改了,前端 useQuery 的返回值类型自动跟着变。你不需要在多个地方反复声明同一份类型,改一处,全局生效。

第二条是 Tailored,按需定制。t3code 不是一个冗余的全家桶,它的依赖是"按场景拼装"的。项目要用数据库就引入 Prisma,要用认证就引入 NextAuth,不用就不选。这样新项目起步时不需要背着几十个用不到的依赖跑,维护成本低,升级也不需要等所有包对齐。

第三条是 Testable,默认可测试。类型安全解决的是"能编译",但业务逻辑对不对,还得靠测试兜底。t3code 里把单元测试、组件测试、端到端测试的脚本和目录结构都预留好了,还接入了提交前的质量闸门。测试不是"以后有空再补",而是从第一天起就是工程的一部分。

1.3 哪些场景适合,哪些场景别硬上

t3code 不是一个万能框架,它有自己的边界。我实际用下来的感受是,下面几类场景非常适合:

  • 中小型全栈应用:比如后台管理、内容平台、工具类产品,CRUD 多,业务实体之间关系复杂。
  • 快速验证的 MVP,但又预感到会长久维护:前期靠脚手架快,后期靠类型安全稳。
  • 希望建立团队开发规范的场景:与其靠 leader 一遍遍提醒,不如让编译器和提交钩子来卡。

但有些场景,我不建议强行套它。如果你所在的组织已经是微服务架构,各个服务用不同语言,前后端完全分离部署,tRPC 的"同仓共享类型"优势就发挥不出来,这时候上 t3code 反而别扭。还有一个更现实的问题:如果团队里大部分人 TypeScript 基础薄弱,连泛型都还没吃透,端到端类型安全带来的维护优势会被学习成本抵消掉。技术选型不是追新,是看团队现状能不能消化。

2. 技术选型:这条类型安全链路是怎么串起来的

2.1 为什么是 Next.js,而不是 Vite + Express 两套服务

全栈项目最常见的方案是前端 Vite + 后端 Express 或 NestJS,两套服务、两个端口、两套部署流程。我早年也是这么干的,后来发现一个问题:前后端类型共享这件事,在两套独立服务里特别难做。

你当然可以通过 OpenAPI 生成前端类型,但前提是后端必须严格维护好 schema 定义,一旦手写接口和 schema 不同步,生成出来的类型就是错的。每次接口变更,都要重新跑一遍代码生成器,跑完还要处理类型映射的边界情况。

Next.js 的价值在于,把渲染层和服务层放进同一个 TypeScript 项目里。App Router 的 Server Component 可以直接读取数据库,也可以直接调用 tRPC 的 service 层,不需要跨域、不需要网络请求封装、不需要额外的 RPC 协议。部署时也简单,Vercel、Docker、Node 环境都能跑,一个应用就是一个整体。

当然,也有 trade-off。如果公司已经有很成熟的后端服务,微服务加起来几十个,前端只是其中一个消费者,那你应该继续走传统前后端分离,而不是为了类型安全把后端搬进前端项目。t3code 的全栈模式,更适合"一个团队负责一个完整业务模块"的形态。

2.2 tRPC 把 API 从文档变成了函数调用

在没有 tRPC 之前,常见的 API 对接方式有两种。REST 风格要维护 OpenAPI 规范,前端根据规范生成客户端,虽然也算自动化,但中间多了一层"规范和代码必须保持一致"的约束。GraphQL 更麻烦一点,你需要在 schema 定义语言里声明类型,再在 resolver 里写业务逻辑,类型声明和业务实现天然就分了两层。

tRPC 的思路很直接:把后端暴露的函数当成一个普通对象,前端直接调用这个对象的方法。没有额外的 IDL 语言,没有生成步骤,服务端定义 router,前端通过类型推导就能拿到完整的入参和返回类型。

这套机制用生活化的类比讲:传统 API 对接像是两个不同公司的员工交流,得通过翻译、邮件、合同来对齐信息;tRPC 像是同一个团队里的两个同事,直接面对面沟通,对方递过来的东西是什么结构,你一眼就能看懂,而且有 TypeScript 编译器帮你盯着。

有一点必须说清楚:tRPC 的优势建立在"前后端同在一个 TS 项目"这个大前提上。如果你以后要开放 API 给第三方客户端,或者要做移动端原生应用,tRPC 就不是最优解了,那时还是老老实实上 REST 或 GraphQL。t3code 默认把 tRPC 放在核心位置,所以在选它之前,先想清楚你的 API 消费者到底是谁。

2.3 Prisma:让数据库模型成为整条链路的起点

数据库是几乎所有业务的数据终点,也是类型漂移最容易发端的地方。表结构里加了一个字段,后端查询里漏了,前端页面里没展示,整个链路可能只有数据库自己知道。t3code 选择 Prisma,看中的就是它"Schema 驱动"的能力。

Prisma 用一个 schema.prisma 文件描述数据模型,你可以用 migrate 命令生成可追溯的 SQL 迁移文件,也可以直接用 prisma generate 生成类型安全的客户端。关键在于,这个客户端是直接从 Schema 编译出来的,表里有什么字段,查询结果就有什么字段,类型完全对应,没有手写映射层。

我在实际使用中觉得,Prisma 对开发效率的提升主要来自两件事:一是 migration 文件可以放进版本控制,数据结构的演进历史完整可查,同事拉代码后跑一遍 prisma migrate dev 就能同步最新结构;二是查询 API 的返回类型天然嵌套了外键关系,不需要写一堆 join 和类型断言,前端拿到的对象本身就是结构化好的。

代价也有:Prisma 在复杂查询上的灵活性不如手写 SQL,特别是一些账单统计、报表汇总的场景,你可能会被迫用 raw query。我的建议是常规 CRUD 交给 Prisma,极端复杂查询单独写 SQL,然后在类型层做好封装,别让裸 SQL 穿透到业务代码里。

2.4 Zod:把编译期的安全感延续到运行时

TypeScript 类型有一个众所周知的短板:它是编译期的,运行时会消失。你可以在编译期保证函数的入参是 string,但永远无法保证 HTTP 请求发过来的 body 真的是 string。防御性校验还是得回到运行时。

Zod 在这里的价值,是让运行时校验和 TS 类型合体。你定义一个 zod schema,它既能做运行时校验,又能通过 z.infer 推导出 TS 类型,一份定义,两处使用。更进一步,tRPC 的 input 可以直接接收 zod schema,省掉了一堆手写类型校验的样板代码。

比如一个创建文章的接口,你要求 title 不能为空,且长度不能超过 100。在传统方案里,你得写一个校验函数,再写一个 TS 类型,再保证前端传入的格式和后端一致。在 t3code 里,只要写一个 zod schema,tRPC 自动完成校验和类型推导,前端调用时传错类型直接编译报错。

运行时校验这件事,新手容易觉得多余,但我在生产环境里被坑过太多次了。客户端被攻击、第三方回调数据格式异常、数据库里混入手工修正的脏数据,这些场景编译期完全看不见,Zod 是最后一道闸门。

2.5 外围质量闸门:lint、typecheck、prettier 的配合

t3code 里配置了一整套质量检查工具,它们各管一段,缺一不可。我用表梳理一下:

工具检查内容在 t3code 中承担的角色
tsc --noEmitTypeScript 类型检查保证类型正确性,这是类型安全的地基
ESLint代码规范与错误模式检查未使用变量、反模式、React Hooks 依赖等
Prettier代码格式统一缩进、引号、分号,消灭格式化争论
Vitest单元测试覆盖工具函数与核心业务逻辑
Playwright端到端测试模拟真实用户操作,验证关键流程
Husky + lint-staged提交前自动执行只在暂存文件上跑检查,避免全量检查太慢

这条链路的思路是:本地提交前先挡住低级错误,CI 里再跑全量检查兜底。如果把质量检查都压到 CI 才做,开发者的反馈回路太长,改一次代码等一次流水线,效率损失很大。放在提交前,每个提交都是经过类型检查、代码规范、单元测试三重过滤的产物,代码评审只需要关注业务逻辑本身,省下大量精力。

3. 实操过程:从 create-t3-app 到第一个类型安全的 CRUD

3.1 初始化项目

t3code 的基座来自 create-t3-app,它会让每一步都通过交互式命令行选择,避免新手一次性接触太多概念。执行:

pnpm create t3-app@latest

随后会有一系列选项,我对每个选项的取舍逻辑是:核心栈固定选前的,外围组件按需选。一个典型的初次配置是勾选 Next.js、Tailwind CSS、tRPC、Prisma、ESLint,认证模块看你项目是否需要再决定,日志库和打包分析工具可以先不开。

这里有个新手容易犯的错误:一上来把能勾的都勾上。心智负担先不提,光是 NextAuth、Drizzle、Tailwind 这些模块互相之间还有版本联动问题,报错时根本分不清是哪个包出的问题。我建议第一次接触 t3code 时,只保留自己马上会用的模块,跑通之后再补装。

初始化完成后,项目自带一套登录示例和一课一练的注释结构。你可以直接删掉 demo 代码,或者保留看陈设。我的习惯是先把整体目录读一遍,搞明白每一层是干什么的,再动手改。

3.2 理解目录结构

create-t3-app 生成的目录结构非常讲究,核心就是划分客户端和服务端的边界:

prisma/ schema.prisma # 数据模型定义 src/ app/ # Next.js App Router 页面 api/trpc/[trpc]/route.ts layout.tsx page.tsx server/ api/ root.ts # tRPC 根 router routers/ post.ts # 业务 router 示例 trpc.ts # tRPC 实例、上下文、鉴权封装 db.ts # Prisma Client 单例 trpc/ server.ts # 服务端 tRPC 调用器封装 react.tsx # 前端 React Query hooks

最核心的规则是:客户端代码不能直接访问src/server目录里的东西。src/server/db.ts只能在服务端使用,前端页面要拿数据,必须经过 tRPC router 或者 Server Component 的封装层。这个边界在文件系统层面就被 tsconfig 的路径区分加上代码评审守住了,不会出现前端一股脑把数据库连接引进去的灾难。

src/trpc/react.tsx是前端独有的封装,它把 tRPC 的 query、mutation 转换成了 React Query 的 hooks。你写业务时不需要关心底层是 React Query 怎么缓存,只需要调用api.post.list.useQuery()就好了。

3.3 定义数据模型并迁移

我以经典的"文章+作者"模型为例,展示 t3code 里数据建模的完整流程。打开prisma/schema.prisma,添加:

model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt author User? @relation(fields: [authorId], references: [id]) authorId String? @@index([authorId]) } model User { id String @id @default(cuid()) name String? email String @unique posts Post[] }

这里有几个细节值得展开说。@default(cuid())比自增数字 ID 更适合分布式和多环境部署,不容易撞号。@updatedAt会在每次更新时自动写入时间,省了手动维护更新时间戳的麻烦。@@index([authorId])是为外键字段加索引,Post 表里按 author 查文章是很高频的操作,没有索引表数据一多查询就变慢。

定义好模型后执行迁移命令,让数据库结构和 Schema 对齐:

pnpm prisma migrate dev --name add_post

这条命令会生成一个prisma/migrations/下的迁移文件,同时重新生成 Prisma Client。我强烈建议把这个迁移文件提交到 Git,团队成员拉下来后跑pnpm prisma migrate dev就能同步结构,整条 Schema 演进历史可回溯,出问题还能看当时改了什么。

3.4 写第一个 tRPC router

数据模型有了,接下来写 API。t3code 里不会出现"路由层+控制器层+服务层"三层设计,tRPC router 本身就是接口层,业务逻辑可以直接写,也可以拆到单独 service 文件里。以文章接口为例:

import { z } from "zod"; import { createTRPCRouter, publicProcedure, protectedProcedure } from "~/server/api/trpc"; export const postRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.post.findMany({ orderBy: { createdAt: "desc" }, }); }), byId: publicProcedure .input(z.object({ id: z.string().min(1) })) .query(async ({ ctx, input }) => { return ctx.db.post.findUnique({ where: { id: input.id } }); }), create: protectedProcedure .input( z.object({ title: z.string().min(1).max(100), content: z.string().optional(), }) ) .mutation(async ({ ctx, input }) => { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), });

publicProcedure表示无需登录即可访问,protectedProcedure会先走 NextAuth 会话检查。ctx上下文里已经挂好了db和session,你不用在业务里反复处理鉴权逻辑。Zod schema 直接作为 input 校验器的作用也体现出来了:前端调用时传入的 title 如果为空或者超过 100 字,会在运行时被拦截,而类型层面前端拿到的错误提示也极其明确。

写完后,要把postRouter挂到根 router 上:

export const appRouter = createTRPCRouter({ post: postRouter, user: userRouter, });

有个小细节容易踩坑:只新建 router 文件、忘记挂到 root 上,前端调用api.post.list时会一直提示类型不存在。我见过不止一个同事被这个问题卡住,排查半天发现只是挂载遗漏。

3.5 前端页面接入,类型自动推导

后端接口定义完,前端调用页面不需要手写任何请求类型。一个最简单的文章列表页面长这样:

"use client"; import { api } from "~/trpc/react"; export function PostList() { const { data: posts, isLoading } = api.post.list.useQuery(); const utils = api.useUtils(); const createPost = api.post.create.useMutation({ onSuccess: async () => { await utils.post.list.invalidate(); }, }); if (isLoading) return <div>加载中...</div>; return ( <div> {posts?.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> </div> ))} </div> ); }

注意看:useQuery返回的posts类型,是 tRPC 后端 router 自动推导出来的。如果你在后端给 Post 模型加了字段,前端这里不用动任何代码,类型就自动带上新字段;如果后端删了字段,这里直接编译报错。这就是端到端类型安全的直接体现。

onSuccess里调用utils.post.list.invalidate()的作用是让 React Query 重新拉取列表数据,这样新增文章后页面自动刷新,不用手动 reload。React Query 的缓存机制和 tRPC 深度集成后,写前端数据流的感觉接近"本地状态管理",不需要手动维护请求状态和 loading 状态。

3.6 把质量闸门接进提交流程

真正的工程化不能靠自觉,要把它接到流程里。t3code 里我用 husky 加 lint-staged 实现提交前自动检查。先保证 package.json 里的脚本齐全:

{ "scripts": { "lint": "eslint .", "typecheck": "tsc --noEmit", "format": "prettier --check .", "test": "vitest run" } }

然后配置 husky 的 pre-commit 钩子:

pnpm exec husky add .husky/pre-commit "pnpm lint-staged"

lint-staged 的配置可以这样写:

{ "lint-staged": { "*.{ts,tsx}": ["eslint --fix", "prettier --write"], "*": ["prettier --write --ignore-unknown"] } }

这套配置的意义在于:每次执行git commit,只会检查暂存区里准备提交的文件,而不是全项目扫一遍。改动多时全量检查会很慢,很多人为了省时间会绕过钩子,lint-staged 把检查范围缩小后,几秒钟就完成,开发者就不抵触了。

typecheck 要不要放进 pre-commit,我曾经犹豫过。项目大了以后全量类型检查也要十几秒,有人会觉得烦。但我坚持保留,因为类型错误是影响面最大的一类问题,与其等到 CI 跑完再反馈,不如在本地提交那一刻就堵住。

3.7 部署时最容易忽视的几件事

部署环节我踩过不少坑,这里挑三个最常见的说。第一个是环境变量。Next.js 构建时,DATABASE_URL、AUTH_SECRET、NEXTAUTH_URL这类变量必须在部署平台配置好,本地.env里的值不会自动同步到线上。第二个是 Prisma Client 的生成时机。很多 CI 环境在安装依赖后会执行postinstall脚本,但如果没有,需要手动在构建前加一步pnpm prisma generate,否则线上构建会报找不到 Prisma Client。

第三个是服务器端数据库连接的连接数问题。开发环境用 SQLite 无所谓,生产环境用 PostgreSQL 时,默认连接池配置需要根据并发量调整。如果某个 Serverless 平台会为每个请求创建新实例,要特别小心连接数耗尽,必要时配置连接池或限制实例数。t3code 默认用 SQLite 起步适合本地演示,真正上线前一定要切换成 PostgreSQL 并验证连接行为。

4. 踩过的坑:t3code 在真实开发中的排查记录

4.1 Prisma 迁移报错,表结构变更那点事

有一次我给 Post 表加一个枚举字段,执行prisma migrate dev时直接报错了。原因是枚举类型和已有数据之间产生了冲突:迁移脚本尝试给已有行添加新字段时,需要用默认值或者 nullable,但我定义成了必填且没有默认值。类似的问题多得很,比如给已有表加唯一约束但数据里有重复值,比如删字段时外键关系还挂着。

我的处理习惯是:改 Schema 之前先想"存量数据怎么办"。能设置默认值就设默认值,不能设默认值就先允许 nullable,跑一个数据补齐脚本后再收紧约束。如果迁移文件已经生成但执行失败,先去prisma/migrations里看下一个迁移文件的内容,确认 SQL 是否符合预期,再谨慎执行migrate reset。reset 会清空数据,只适合开发环境,生产环境千万不要手滑。

4.2 tRPC 类型不更新,害我折腾一下午

我遇到过一件很烦的事:后端 router 明明加了新方法,前端调用时编辑器里一直提示类型不存在。重启开发服务器也没用。后来发现是 dev server 的增量编译缓存问题,tRPC 的新类型没有触发 React Query 的类型声明重新加载。还有一次,新 router 文件创建了但忘了挂到 root,前端api.newModule一直不存在,这类问题单看报错信息根本看不出来。

经验是:遇到 tRPC 类型不更新的情况,先检查三件事——router 是否挂到 root、tsc --noEmit是否能通过、dev server 是否重启过。另外,把src/trpc/react.tsx里的类型导出做一个显式暴露,也能减少怪异问题。如果实在没头绪,直接重启 VSCode 的 TS Server:插件或者缓存版本不一致时,这个操作能解决 80% 的"明明没错但报错"。

4.3 在 Server Component 里硬套 useQuery,直接报错

App Router 刚出来时,我很自然地想在 Server Component 里调用api.post.list.useQuery(),结果浏览器控制台直接报错,提示 hooks 只能在 Client Component 里使用。这是"use client"边界不清导致的。每个使用到 hooks、浏览器 API、React Context 的组件,都需要显式声明"use client"。

Server Component 里要获取数据,t3code 提供了createCaller这种方式:

import { createCaller } from "~/server/api/root"; export default async function Page() { const caller = createCaller(); const posts = await caller.post.list(); return <PostList initialPosts={posts} />; }

这个思路是把数据在服务端先查好,作为 props 传给客户端组件,客户端再基于初始数据渲染。好处是首屏渲染更快,SSR 的 Markup 里直接包含内容,不会闪加载态。坏处是必须处理好"服务端数据"和"客户端 React Query 缓存的同步",否则刷新交互后状态对不上。

4.4 水合错误:页面突然白屏,控制台提示 hydration failed

水合错误是 Next.js 全栈开发绕不开的坑。它发生的原因是:服务端渲染出来的 HTML,和客户端第一次渲染的结果不一致,React 在"接管"页面时发现两边的结构对不上,只能报错。常见触发场景有三个:在组件顶层直接调用Math.random()、直接读取Date.now()生成渲染内容、依赖 window 对象做判断但服务端没有 window。

t3code 的默认模板基本不会触发这个问题,但业务代码里经常踩到。我的规避方法是:把随机数、当前时间、浏览器本地状态这类"环境相关值"全部放到useEffect里延迟生成,服务端和客户端初次渲染都用稳定数据。比如倒计时组件,服务端先渲染一个通用的占位时间,客户端挂载后再开始走秒,两边渲染的 HTML 完全一致,水合就过了。

4.5 常见问题速查表

问题现象可能原因解决思路
前端调用 api 报类型不存在router 未挂载到 root 或 dev 缓存检查 root 挂载,重启 dev server
Prisma migrate dev 报错Schema 与存量数据冲突加默认值或设为 nullable,先备份数据再 reset
Server Component 里 useQuery 报错把 hooks 用在了服务端组件改用 createCaller 或移入 "use client" 组件
控制台报 hydration failed服务端和客户端渲染不一致把随机数等放到 useEffect 中生成
线上 TypeScript 编译失败环境缺少 prisma generate在构建命令前补pnpm prisma generate

5. 从 t3code 继续生长:扩展路径与个人经验

5.1 从种子项目到完整业务的推进顺序

t3code 不是终态,它只是一个高质量的起点。从它往完整业务系统走,我建议按这样推进:先把认证闭环彻底跑通,确保用户能注册、登录、退出,并能通过 protectedProcedure 保护需要登录的接口;再把第一组领域模型建好,比如电商里的商品和订单,把它们的 CRUD 和业务规则用 tRPC router 完整实现。

之后再做权限模型、文件上传、后台管理、支付回调这些外围能力。每一步都坚持一条铁律:数据和接口的类型必须保持共享,不能让某个模块绕过 tRPC 裸写 fetch。某次如果实在需要调用第三方 HTTP API,也要在外围封装一层能推导类型的方法,再暴露给业务代码。

从什么时候开始需要 monorepo?我发现当项目拆成管理后台、客户端前台、独立定时任务三个以上应用,且它们共享大量数据库模型和工具函数时,就该把它拆成 pnpm workspace 了。t3code 的单仓结构这时会成为瓶颈,但过渡并不痛苦,因为核心的类型模型和 router 文件几乎可以原样搬进 monorepo 的 packages 里。

5.2 在团队里推行类型安全的小技巧

推行任何工程规范,最难的不是技术,而是让团队成员相信"这事值得做"。我这几年带团队的经验是:不要上来就要求全员理解泛型、infer、类型体操,而是定几条简单的硬性规则,靠工具强制落地。

第一条,code review 时必须确认"前端没有手写接口返回类型"。如果看到责任人有声明的interface PostDTO和后端模型对应,就可以直接退回,要求改成从 tRPC 的调用结果去 inference。只要坚持一个迭代,团队就尝到了改字段不用全局搜索的甜头。

第二条,禁止绕过 tRPC 的前端裸 fetch。新增的接口都必须走 router 暴露,特殊场景需要第三方接口时,包一层可推导类型的 service。规则简单,执行成本低,效果立竿见影。

第三条,在 CI 里加一个类型检查任务,任何 PR 如果 typecheck 失败就不能合并。技术债能还就还,团队执行力是逼出来的,不是靠自觉。

5.3 维护 t3code 几年后,我最想强调的三句话

第一句:类型不是限制自由,而是把人的脑力从机械的对齐接口工作中解放出来。你省下的时间,会实实在在地变成对业务逻辑的思考。

第二句:种子项目要做的是"省掉重复配置",而不是"把原理藏起来"。哪怕 t3code 把一切都配好了,我也建议新手把 tsconfig、Prisma Schema、tRPC 上下文这三块源码反复读懂。工具会升级,原理不会过时。

第三句:质量建设要放在门槛最低的位置。提交前自动检查比 CI 人工审核有效,类型推导比文档约定有效,运行时校验比口头承诺有效。把最容易跳过的地方堵上,制度的牢靠程度完全取决于最低门槛那一个环节。

我自己维护 t3code 时有一个固定的习惯:每次新建业务页面,先让自己接受一个拷问——如果后端接口在某个夜里悄悄改了字段,我能不能在第二天的编译期就发现?能,是因为类型链路还通着;不能,那就别急着写业务,先回去把缺口堵上。这套工程化体系没有给我带来任何炫技的快感,它只是把我从无穷无尽的"接口又对不上了、字段又找不到了、线上又出错了"里一点点捞了出来。如果你也在经历同样的拉扯,不妨从一份种子工程开始,把类型安全这条护城河挖起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 23:02:12

t3code实战:配置驱动与模板渲染的代码生成工具解析

写代码这几年&#xff0c;我最怕的不是需求复杂&#xff0c;而是同一套初始化动作重复几十遍&#xff1a;建目录、配路由、写类型定义、复制模型模板、改配置文件。版本一升级&#xff0c;模板还要跟着同步改&#xff0c;稍微漏掉一个地方&#xff0c;就是运行时才能暴露的问题…

作者头像 李华
网站建设 2026/10/28 23:00:30

Docker部署PostgreSQL全指南:从入门到生产实践

1. 为什么选择Docker部署PostgreSQL&#xff1f;在数据库部署领域&#xff0c;Docker已经成为现代开发者的首选方案。我最初接触PostgreSQL容器化部署是在2016年&#xff0c;当时为了快速搭建开发测试环境尝试了这个方案&#xff0c;没想到这个临时解决方案最终成为了我们团队的…

作者头像 李华
网站建设 2026/10/28 22:59:37

t3code:面向Web/Electron/iOS三端开发者的CLI工具链

1. 项目概述&#xff1a;t3code 是什么&#xff0c;它解决哪类开发者的实际痛点&#xff1f;t3code 这个名字乍看像一个缩写或代号&#xff0c;但结合当前高频热搜词——CLI、Electron、web app、iOS——以及大量围绕codex cli、electron 技术栈、iOS 开发者模式、iOS 模拟、CL…

作者头像 李华
网站建设 2026/10/28 22:59:24

鸿蒙物联网开发入门:HarmonyOS核心优势与实践

1. 鸿蒙物联网开发入门&#xff1a;为什么选择HarmonyOS&#xff1f;第一次接触鸿蒙&#xff08;HarmonyOS&#xff09;的开发者往往会有这样的疑问&#xff1a;在Android和iOS主导的移动生态中&#xff0c;为什么还要学习这个新系统&#xff1f;我在2021年接触鸿蒙2.0时也带着…

作者头像 李华
网站建设 2026/10/28 22:58:00

GSAP ScrollTrigger 三层滚动视差实现与性能调优

1. 滚动视差到底在做什么&#xff0c;为什么我不用 CSS 硬写滚动视差&#xff08;Parallax Scroll&#xff09;这个词听着挺玄&#xff0c;说白了就一句话&#xff1a;不同的元素在页面滚动时以不同速度移动&#xff0c;让人眼产生"这些东西离我远近不一"的错觉。我用…

作者头像 李华
网站建设 2026/10/28 22:57:37

CFileDialog 模板嵌入自定义控件:机制、实现与排查

1. 先搞清楚 CFileDialog 到底封装了什么1.1 这个需求为什么会反复出现做桌面工具类软件的同行大概都遇到过类似的场景&#xff1a;用户点了“打开文件”&#xff0c;弹出来的标准对话框里只有文件名、文件类型、几个常用按钮&#xff0c;但你偏偏还想在这个窗口里塞一个“以只…

作者头像 李华