ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

t3code:TypeScript+tRPC+Prisma全栈类型安全方案复盘

t3code:TypeScript+tRPC+Prisma全栈类型安全方案复盘

从项目标题“t3code”出发,我做一次完整的技术复盘。先说结论:t3code 不是一个“另一个脚手架”,而是一套把类型安全贯彻到全栈每一层的工程化代码生成方案。项目名字里那个“t3”不是版本号,而是“Three T”的意思——TypeScript、Tailwind CSS、tRPC 三个以 T 开头的技术组合在一起,配合 Next.js 做全栈应用的底座。这套东西解决的是我一直觉得全栈开发里最磨人的问题:前后端类型割裂、CRUD 样板代码重复、工程配置反复折腾。如果你正在做 Next.js 相关的全栈项目,或者对端到端类型安全有兴趣,这篇复盘值得看完。

1. 项目整体定位:t3code 到底解决了什么问题

1.1 全栈开发的两个老大难

我做了这些年项目,最烦的不是业务逻辑本身,而是两件事。

第一件事是类型割裂。前端写fetch('/api/user'),后端单独维护一套User接口,前端一个interface User,后端一个export interface User,两边还经常不同步。改一个字段,前端忘了改类型,运行到某个页面才发现字段不见了。维护两套类型定义的成本,在小项目里不明显,项目一旦超过十几个接口就非常痛苦。

第二件事是样板代码无限重复。做一个简单的帖子列表功能,要写数据库模型、写 API 路由、写页面组件、写表单校验、写错误处理,光把 CRUD 完整跑通就得两三百行代码。如果每个业务模块都这么来一遍,真正留给业务逻辑的时间就没多少了。

t3code 就是把这两个问题一起解决的设计——它不是一个装饰性质的模板仓库,而是一个能持续生成代码、统一类型来源的工作流工具。核心思路是:只要你在一个地方定义好类型,前端、后端、数据库就能共享同一套类型定义,代码能自动生成的就绝不手写。

1.2 为什么是“三 T”:TypeScript、Tailwind CSS、tRPC

选择这三个 T,每个背后都有理由。

TypeScript 是地基,没有争议。现在的全栈项目,不用 TypeScript 我反而不太会写了。泛型、类型推导、type和interface的组合,能让我在写代码的时候就拦住一大部分运行时才会暴露的问题。

Tailwind CSS 解决的是“样式隔离问题”。它看起来是 CSS 的事,其实深层是工程问题。传统写 CSS Modules 或者 styled-components,每个项目都要重新定主题变量、整理样式文件层级、处理样式覆盖。Tailwind 直接把设计约束收拢到配置文件和 class 里,配合 t3code 生成页面时,样式的组织方式天然一致,不用每次从零设计。

tRPC 是这套方案里最妙的一环。它可能不如 REST 或 GraphQL 流行,但它的核心卖点正中 t3code 的靶心:前端调用后端函数,不需要单独定义 API schema,不需要生成 API 文档,类型直接从后端函数定义推导到前端。我写完一个后端 procedure,前端立刻获得免于手写类型的完整调用体验,字段名错了编译期就给你拦下来。

提示:如果你对 tRPC 没概念,就把它理解成“像调用本地函数一样调用远程函数,并且类型自动对齐”。它不是魔术,是基于 TypeScript 类型推导的远程过程调用框架。

这套组合选下来,整个应用从数据库到组件,类型体系是不间断的。t3code 的价值正在于把这套“不间断”变成默认能力,而不是靠开发者自觉。

1.3 项目适用人群和典型场景

谁应该关注 t3code?我认为是这三类人:

  • 用 Next.js 做全栈应用,但不想在 API 类型同步上耗时间和精力的人;
  • 做内部系统、管理后台这类 CRUD 密集场景,希望快速产出可维护模块的人;
  • 刚接触 tRPC、Prisma 这套生态,想找一个“最佳实践”模板直接开跑的人。

对比一下常见的替代方案:Next.js 官方脚手架没有集成 tRPC,需要自己搭;自己从零整合 TypeScript、Prisma、Tailwind、tRPC 至少需要半天时间,还要处理各种版本兼容;而 t3code 把这些全部预置,还把代码生成补上,可以说是“别人帮你踩完了配置的坑”。

2. 核心功能拆解与设计思路

2.1 交互式脚手架:用选择代替配置

第一次使用 t3code 时会进入一个交互式问答,选择需要的模块。这里的设计逻辑很实用:按需加载,而不是一股脑塞给你所有东西。

我记得早期用某些脚手架,装完以后.tsx文件几十个,一半用不上,删又不敢乱删。t3code 的处理方式是把选择权交还给你,并且每个选项前面带说明,我对比过其他工具的交互式 CLI,t3code 这一步的信息提示是最友好的——它会告诉你每个选项目前是否稳定、依赖哪些包、大概多大体积,而不是丢一个“yes/no”让你猜。

一个值得注意的细节是:t3code 初始化时没有大海报一样的 ASCII 艺术图(很多命令行工具喜欢整这个),而是直接进入选项。这种产品取舍我很认同,工具栏里最重要的是效率和可读性,而不是视觉表演。

2.2 代码生成器:把 CRUD 变成一道填空题

这是 t3code 最让我觉得“日常开发体验被升级了”的功能。

要建一个帖子模块,我只跑一句命令:

t3code g resource post --fields "title:string content:text published:boolean"

它会自动完成以下工作:

  • 生成 Prisma 模型定义,并追加到schema.prisma;
  • 生成 tRPC router,包含create/read/update/delete/list五个基础 procedure;
  • 生成 Zod 校验 schema,每个字段的校验规则按类型自动推断;
  • 生成列表页、详情页、创建/编辑表单组件;
  • 生成类型导出入口,前端模块统一从@server/db和@server/api导入类型。

这个设计看起来简单,实现起来最关键的地方是模板的可预测性。命令跑完以后生成的代码质量,决定你能不能信任它、后续改起来顺不顺手。我翻了它生成的代码,发现很多细节是做得好而不是“能运行就行”:

  • 表单错误提示已经接好,不要求你回头再补;
  • create 和 edit 共用同一个表单组件,利用 TypeScript 联合类型做区分;
  • 列表页默认做分页,但不是一次性把分页组件全写上,而是给出注释好的空位,防止过度生成。

这种“给框架但留好扩展点”的生成策略,比那些把代码塞得满满当当的生成器高明。生成器给的是一块像样的毛坯房,不是塞满家具的样板间。

2.3 工程化配置的“开箱即用”是怎么做到的

实际项目里,真正的坑往往不在业务代码,而在工具链配置。t3code 的工程配置部分我拆开看过,里面有几处设计让我印象非常深。

路径别名统一管理:它把@server、@app、@components这些路径别名写到一个tsconfig.paths.json里,然后主tsconfig.json通过extends引用。我见过太多项目在 tsconfig、jest、webpack 里各维护一遍别名,最后改一次要改三个文件。t3code 这种集中管理的方式,改一处就全局生效,而且 ESLint 的import排序规则也是配合别名设计好的。

ESLint 和 Prettier 规则是“内聚”的:很多项目里 ESLint 和 Prettier 打架,格式化以后和 lint 规则冲突。t3code 的做法是给 ESLint 配置直接关闭了所有与 Prettier 可能冲突的 stylistic 规则,然后样式统一交给 Prettier 管。这个“分工明确”的思路看起来简单,但能实际解决那种“一保存就报错”的体验问题。

环境变量类型化:它在初始化时生成一个env.mjs文件,用 Zod 校验环境变量。启动时如果缺少DATABASE_URL之类的必要变量,会直接给出中文般的清晰报错,而不是运行到某个页面才报“undefined is not a function”。这个点很关键,项目换人接手或者部署到新环境时能省下大量排查时间。

3. 实操全过程:跑通一个完整的资源模块

3.1 环境准备

我的建议环境如下:

依赖版本建议说明
Node.js18.x 或 20.x LTS低于 16 会有 ES2022 语法兼容问题
pnpm8.x+t3code 默认包管理器,锁文件更严格
Docker可选本地数据库用容器跑最稳

先安装脚手架:

pnpm create t3code@latest my-app

这个过程会下载模板然后进入交互问答,不需要全局安装任何 CLI 包,这点很干净。

3.2 初始化选择的实际经验

交互问答里有几个选择题,我整理一下我实际选择时的考虑。

第一问选框架,默认是 Next.js。如果做的是纯 API 后端,也可以选 Express 变体,但 t3code 的生成器大部分是围绕 Next.js 设计的,非 Next.js 会砍掉不少功能。我建议保持默认。

第二问是选 tRPC,默认勾选,建议保留。如果你还没用过 tRPC,正好借这个项目体验一把。它是整套类型安全链路里最关键的一环,去掉它 t3code 就只剩一个普通 CRUD 生成器了。

第三问是选 Prisma,建议勾选。ORM 的选择这一步定了,后面数据库模型生成直接由 Prisma 接管。

第四问是 Tailwind CSS,如果项目要做界面就选上,纯 API 项目可以不用,配置会自动精简。

初始化完成后进入项目,先跑一次:

pnpm install

然后启动开发环境:

pnpm dev

这时候 Next.js 和 tRPC 已经能通了,浏览器打开http://localhost:3000,能看到一个基础页面,表示整条链路已经打通。

3.3 创建第一个业务模块

我现在创建一个“文章”模块,包含标题、内容、是否发布三个字段,命令如下:

pnpm t3code g resource post --fields "title:string content:text published:boolean"

生成器的输出会提示你哪些文件被创建、哪些文件被修改。我逐个检查一下:

  • prisma/schema.prisma:新增了Post模型,三个字段类型分别是String、String(content 因为是 text 类型映射成text,实际上 Prisma 里还是String)、Boolean;
  • src/server/api/routers/post.ts:新增了带注释的 tRPC router 骨架,包含所有 CRUD procedure;
  • src/app/(routes)/posts/page.tsx:生成了帖子列表页,使用服务端数据获取;
  • src/components/post-form.tsx:生成了创建/编辑共用的表单组件,包含 Zod 校验。

这几个文件生成完,一个具备前后端完整链条的模块已经可以运行。

3.4 连接数据库并完成迁移

本地我用 Docker 起一个 PostgreSQL:

docker run --name localpg -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:16

然后修改.env里的DATABASE_URL。如果初始化时没成功写入,手动补:

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

接着执行迁移,把Post模型同步到数据库:

pnpm prisma migrate dev --name init_post

这里有个关键操作:每次改动schema.prisma之后,都要执行npx prisma generate刷新 Prisma Client。t3code 的命令会自动触发吗?我实测下来,g resource会提醒你执行迁移,但不会自动跑migrate dev,因为迁移时机应该由开发者控制,避免在生成阶段就动数据库。这个设计是合理的,但也意味着你需要记住“改 schema 后先迁移再开发”。

全部完成后,重新访问http://localhost:3000/posts,应该能看到一个空的帖子列表,通过表单可以创建第一条数据。

3.5 一个小而完整的“发布状态”联动验证

我顺手验证一下类型安全链路。在生成的post-router.ts里,有一个byId的 procedure,我打开页面组件,试着给它传一个不存在的字段:

trpc.post.byId.useQuery({ id: "abc", fakeField: 123 });

保存的瞬间,TypeScript 编辑器立刻报错,提示fakeField在ByIdInput中不存在。这就是 t3code 全套设计的核心验证点:你在后端定义校验 schema 的地方,前端调用的类型已经同步了。这种体验对做全栈的人来说是质的提升——以前是出了问题靠 postman 试,现在是编译期直接告诉你哪里错了。

4. 常见问题与排查技巧实录

4.1 三大高频类型报错场景

我在实际使用中碰到过几个类型层面的高频问题,这里统一说一下解法。

第一个是“Cannot use import statement outside a module”。这通常发生在 Node 环境跑 TS 文件时,直接执行.ts脚本导致 ESModule 语法炸掉。解决办法:要么用tsx来跑脚本,要么用 t3code 提供的t3code run命令(它内部帮你处理 module 解析);不要手动改package.json的 type 字段,那会让你整个项目模块解析方式大乱。

第二个是“Property ‘xxx’ does not exist on type ‘never’”。这个往往是 tRPC 的 procedure 输入类型没推导出来,常见原因是 Router 对象没有用router()包一层,或者.input()没接 Zod schema。t3code 生成的模板一般不会踩这个,但你自己扩展 procedure 时容易犯。检查一下 provider 的这层:

import { router, publicProcedure } from "../trpc";

确保用的是 t3code 生成的trpc实例,而不是从@trpc/server直接裸导入。

第三个是“Type ‘string | undefined’ is not assignable to type ‘string’”。这个是标准的 TS 严格空检查问题,在表单组件里非常常见。原因是 tRPC 输入经过.optional()后,类型变成了string | undefined,但你的组件 props 是按必填定义的。解法是给表单统一加默认值,而不是用非空断言!——我用!修过一两次,后来发现运行时照样报错,因为数据源根本没有那个字段。

4.2 样式与响应式相关的坑

用 Tailwind 最烦的其实是“为什么我刚加的 class 没生效”。常见三种原因我都遇到过:

  1. 字体配置没写:Tailwind v3 默认用系统字体栈,如果你在tailwind.config.ts里设置了自定义字体,但没加到font-family的 CSS 变量,整个界面会出现“样式很怪但是不影响功能”的状态。t3code 初始化时会生成一个globals.css,里面有:root变量,改字体直接调这个文件就行。

  2. 类名拼接被 Purge 误删:如果你用字符串拼接的方式来组合类名,比如`bg-${color}-500`,Tailwind 在构建时扫描不到完整类名,就会把bg-red-500这类动态生成的样式给丢到生产环境外面去。解决方式是列一个完整的类名映射表,或者用沙箱环境跑t3code的构建检查(它天生自带 Tailwind 的安全列表校验)。

  3. 响应式断点被覆盖:很多人调试时屏幕上明明够宽,但是md:前缀不生效。这是 Tailwind v3 的断点逻辑,md以下的默认样式如果不写清楚,样式会被上一级的sm样式覆盖。建议列表页和表单页都用 t3code 生成的框架类名,它已经处理好了断点层级关系,不要自己重新堆叠公共样式。

4.3 依赖升级与版本锁定的经验

我踩过一个比较重的坑:用了一周后,我把 tRPC 从 10.x 升到 11.x,然后一堆类型报错冒出来。createTRPCNext的调用变成了createTRPCClient,返回类型从TRPCClientErrorLike变成了TRPCClientError,各种组件代码全部要适配。

这里面有一个有价值的教训:t3code 模板里的版本是锁定在pnpm-lock.yaml的,升级版本之前要做一次全量pnpm install,然后在可回滚分支上验证类型联通。我自己后来的做法是:每个里程碑单独锁一次依赖,大版本升级前先用npx codemod(如果有)自动做迁移,不要直接用pnpm update全量滚。

另外一个细节:docker 容器里跑 Prisma 时,Postgres 的版本最好锁定 major 版本号。postgres:16和postgres:latest的初始化行为有差异,如果迁移失败先看一下DATABASE_URL是否合规。Prisma 报Can't reach database server的时候,先docker ps看容器起没起,再npx prisma db pull验证连接链路,不要一上来就改 schema。

4.4 我个人的一条黄金排查路径

如果真遇到搞不定的状态,我推荐用这条路径排查:

  1. pnpm t3code doctor检查工程配置是否完整(它会检查环境变量、Prisma Client、Next.js 配置文件);
  2. pnpm prisma generate+pnpm prisma migrate dev重跑迁移;
  3. 停掉pnpm dev,删除.next缓存,重新启动。

这三步执行完,80% 的诡异问题都能解决。很多看似代码的问题,其实只是开发服务器的旧编译产物在捣乱。这个经验对任何 Next.js + Prisma 类项目都适用,不仅仅是 t3code。

5. 延伸思考:t3code 适合带进你的下一个项目吗

我个人判断,t3code 最适合的场景是:你已经确定要用 TypeScript + Next.js + 关系型数据库,想快速搭建内部管理后台或业务原型,又不希望在类型维护上花太多时间。它和“手写一切”相比省下的时间,主要体现在每一次新模块的增删改查搭建上,而不是体现在初始化那一次命令的两分钟里。

它也有边界。项目复杂度高到一定程度,比如你需要非常多自定义 API 聚合逻辑、复杂的中间件、多租户权限矩阵,生成器给你的只能作为起点,你需要改建 router 结构或者写自己的代码生成器。但好的地方在于,t3code 生成的代码并没有把你锁死在特定框架里——模块之间是解耦的,你不用它继续生成也没问题。

最后分享一个小技巧:t3code g resource生成的表单里,所有输入组件都保留了name属性。这意味着你可以在替用户排错时,用浏览器开发者工具快速定位字段名,验证 Zod 报错到底挂在哪个字段上。这类“方便续链而不是方便断链”的细节,是我愿意持续把它用在下一个项目里的重要原因。

返回列表