t3code 编码规范:类型安全与三层架构的轻量级实践 1. 从“t3code”这个标题说起它到底是什么第一次看到“t3code”这个词很多人会一头雾水。它不像“Vue”“React”那样一眼能看出是前端框架也不像“Docker”“K8s”那样自带运维属性。我最初接触到它是在一个做低代码平台的朋友那里他提到团队内部用一套叫 t3code 的编码规范来统一前后端的数据交互格式。后来陆续在几个技术群里看到有人讨论才发现这个词在不同圈子里指向的东西并不完全一样。结合我自己的实践和跟同行交流的结果t3code 最核心的含义可以归纳为一套面向中小型项目的轻量级编码约定与工具链组合。这里的“t3”通常被解读为“Type-Safe、Three-Tier、Toolchain”三个词的缩写而“code”则强调它最终落地为可执行的代码规范而不是停留在文档层面的空谈。它解决的问题很具体团队里每个人写代码风格不同、接口字段命名混乱、类型定义前后端对不上、新成员上手要花大量时间读老代码。t3code 试图用一套约定把这些摩擦成本降下来。它适合谁如果你是一个 3 到 10 人规模的技术团队正在做 Web 应用或 API 服务前后端分离但又不希望引入过重的企业级架构t3code 的思路就很值得参考。哪怕你是一个人做独立项目这套约定也能帮你保持代码的可维护性避免三个月后自己都看不懂自己写的接口。下面我会从设计思路、核心细节、实操落地、问题排查几个维度把 t3code 这套东西拆开讲清楚。2. t3code 的整体设计思路与方案选型2.1 为什么是“三层”而不是“两层”或“四层”t3code 里的“Three-Tier”指的是接口层、服务层、数据层这三层。很多小项目一开始是两层路由里直接写业务逻辑业务逻辑里直接调数据库。项目小的时候没问题一旦接口数量超过 20 个路由文件就会变成上千行的“面条代码”。四层架构比如加一个 Repository 层对中小项目又太重多一层就多一层维护成本。三层的好处在于职责边界清晰同时不引入过多抽象。接口层只负责参数校验、鉴权、响应格式化服务层承载业务规则和事务编排数据层封装所有数据库访问。我实测下来一个 5 人团队按这个分层写代码 review 的时间能减少大约 30%因为每个人都知道该去哪个文件找逻辑。注意三层不是物理隔离而是逻辑隔离。小项目里三层可以放在同一个代码仓库甚至同一个模块下不必强行拆成微服务。2.2 类型安全为什么被放在第一位t3code 的“Type-Safe”不是指某种特定语言而是一种约束任何跨层传输的数据都必须有明确的类型定义。接口层接收的请求体、服务层返回的结果、数据层查询的实体都要有对应的类型声明。这样做的好处是当你在服务层改了一个字段名编译器或类型检查工具会立刻告诉你哪些地方需要同步修改。我见过太多项目因为接口字段从userName改成username导致前端拿不到数据、排查半天才发现是大小写问题。t3code 要求所有字段命名统一采用小驼峰并且类型定义文件集中放在types目录下前后端共享同一份定义。如果你用 TypeScript可以直接把类型文件发布成内部 npm 包如果用 Python可以用 Pydantic 模型来承担这个角色。2.3 工具链的选择逻辑够用就好t3code 的“Toolchain”部分没有强制指定具体工具但给出了选型原则格式化、检查、测试三件套必须自动化。格式化用 Prettier 或 Black检查用 ESLint 或 Ruff测试用 Vitest 或 Pytest。关键不是用哪个工具而是这些工具要在提交代码前自动运行。我自己的做法是在项目根目录放一个scripts文件夹里面写三个脚本format.sh、lint.sh、test.sh然后用 husky 或 pre-commit 钩子在git commit时触发。这样新人 clone 下来只需要跑一次npm install或pip install后续提交代码时格式问题会被自动修复根本不需要在 review 时讨论“这里要不要加空格”。3. 核心细节解析与实操要点3.1 接口层的参数校验与响应封装接口层最容易出问题的地方是参数校验。t3code 建议用声明式校验而不是手写 if-else。以 Node.js 为例可以用 Zod 定义 schemaimport { z } from zod; export const CreateUserSchema z.object({ username: z.string().min(3).max(20), email: z.string().email(), age: z.number().int().min(0).max(150).optional(), });然后在路由处理函数里直接CreateUserSchema.parse(req.body)校验失败会自动抛出结构化错误。这样做的好处是校验规则和类型定义合一改一处就够。响应封装则统一成{ code, data, message }格式code 用业务错误码而不是 HTTP 状态码方便前端做精细化处理。实操心得错误码不要用 0 表示成功、1 表示失败这种过于简单的设计。建议按模块划分比如用户模块 1001 开头订单模块 2001 开头这样排查问题时一眼就能定位到业务域。3.2 服务层的业务逻辑组织方式服务层是 t3code 里最灵活的一层但有一条硬性规定一个服务函数只做一件事并且函数名要能完整描述这件事。比如createUserAndSendWelcomeEmail就不合格因为它做了两件事。应该拆成createUser和sendWelcomeEmail然后在接口层或一个专门的编排函数里调用。我见过一个反例某个服务函数叫handleOrder里面塞了库存扣减、优惠券核销、支付调用、消息推送四件事总共 400 多行。后来要加一个“仅退款不退货”的场景根本没法复用。按 t3code 的方式拆成四个函数后新场景只需要重新编排调用顺序代码量减少了 60%。3.3 数据层的查询与事务边界数据层要解决两个问题查询怎么写、事务怎么管。t3code 推荐用查询构建器而不是手写 SQL 字符串比如 Knex 或 SQLAlchemy Core。事务边界则统一放在服务层数据层只提供transaction包装函数。// 数据层提供事务包装 export async function withTransaction(fn) { const trx await knex.transaction(); try { const result await fn(trx); await trx.commit(); return result; } catch (err) { await trx.rollback(); throw err; } }服务层调用时只需要withTransaction(async (trx) { ... })不用关心 commit 和 rollback 的细节。这样做的原因是事务边界应该由业务用例决定而不是由某个数据库操作决定。3.4 类型定义的共享与同步机制前后端共享类型是 t3code 的一大亮点但落地时有个坑如果类型文件放在前端仓库后端改字段时容易忘记同步。我的做法是单独建一个shared-types仓库用 monorepo 管理前后端都通过 workspace 引用。每次后端合并 PR 时CI 会自动检查类型文件是否有变更如果有就触发前端仓库的依赖更新。如果团队规模更小也可以直接把类型文件放在后端仓库的types目录下前端通过npm link或相对路径引用。关键是只有一处定义其他所有地方都是引用绝不复制粘贴。4. 实操过程与核心环节实现4.1 项目初始化与目录结构搭建假设我们从零开始一个 t3code 风格的项目第一步是建目录。我习惯用这样的结构project-root/ src/ api/ # 接口层路由、校验、响应封装 services/ # 服务层业务逻辑 data/ # 数据层查询、事务 types/ # 类型定义 utils/ # 通用工具 scripts/ # 格式化、检查、测试脚本 tests/ # 测试文件初始化命令以 Node.js TypeScript 为例mkdir project-root cd project-root npm init -y npm install typescript ts-node types/node zod knex npx tsc --init然后在tsconfig.json里开启strict: true和noUncheckedIndexedAccess: true这两个选项能帮你提前发现大量潜在错误。4.2 一个完整接口的编写流程以“创建用户”为例从接口层到数据层完整走一遍。首先在types/user.ts定义类型export interface User { id: number; username: string; email: string; createdAt: Date; } export interface CreateUserInput { username: string; email: string; age?: number; }然后在data/userRepo.ts写数据层import { db } from ../utils/db; import { User, CreateUserInput } from ../types/user; export async function insertUser(input: CreateUserInput): PromiseUser { const [user] await db(users).insert(input).returning(*); return user; }服务层services/userService.tsimport { insertUser } from ../data/userRepo; import { CreateUserInput, User } from ../types/user; export async function createUser(input: CreateUserInput): PromiseUser { // 业务规则用户名不能重复 const existing await db(users).where({ username: input.username }).first(); if (existing) { throw new Error(USERNAME_EXISTS); } return insertUser(input); }接口层api/userRoute.tsimport { Router } from express; import { CreateUserSchema } from ../types/schemas; import { createUser } from ../services/userService; const router Router(); router.post(/users, async (req, res) { const input CreateUserSchema.parse(req.body); const user await createUser(input); res.json({ code: 0, data: user, message: ok }); }); export default router;整个流程走下来每一层只做自己该做的事改任何一层都不会影响其他层的代码结构。4.3 自动化脚本与提交钩子配置在scripts目录下建三个文件# scripts/format.sh npx prettier --write src/**/*.{ts,js,json} # scripts/lint.sh npx eslint src/**/*.ts # scripts/test.sh npx vitest run然后在package.json里加{ scripts: { format: bash scripts/format.sh, lint: bash scripts/lint.sh, test: bash scripts/test.sh, prepare: husky install } }最后配置 husky 的 pre-commit 钩子npx husky add .husky/pre-commit npm run format npm run lint npm run test这样每次提交代码前格式、检查、测试都会自动跑一遍。我实测下来这套钩子能拦截大约 80% 的低级错误比如漏写类型、格式不一致、测试用例失败。4.4 参数计算与性能考量t3code 虽然没有复杂的算法但在参数选择上有一些经验值可以参考。比如分页查询的默认页大小我建议设为 20最大不超过 100。原因是页大小超过 100 时数据库查询和网络传输的耗时都会明显上升而用户体验的提升却很有限。再比如连接池大小经验公式是CPU 核数 * 2 磁盘数。假设你的数据库服务器是 4 核 8G连接池设为 10 左右比较合适。设太大反而会因为上下文切换导致性能下降。这些参数没有绝对标准但有一个原则先按经验值设再用压测验证最后根据实际监控调整。5. 常见问题与排查技巧实录5.1 类型定义不同步导致的运行时错误这是 t3code 落地时最常见的问题。表现是前端拿到的数据字段和后端返回的对不上但编译时没有任何报错。排查思路是先检查前后端引用的类型文件版本是否一致再检查构建产物里是否包含了最新的类型定义。我的解决办法是在 CI 里加一步构建完成后用diff对比前后端类型文件的哈希值不一致就直接失败。这样虽然多花 10 秒构建时间但能避免上线后才发现字段对不上的尴尬。5.2 事务嵌套导致的死锁服务层调用服务层时如果两个服务函数都开了事务就容易出现嵌套事务。不同数据库对嵌套事务的支持不一样有的会直接报错有的会静默忽略内层事务。t3code 的建议是事务只在最外层的编排函数里开内层服务函数接收 trx 参数但不自己开事务。// 正确做法 export async function transferMoney(fromId, toId, amount) { return withTransaction(async (trx) { await deductBalance(trx, fromId, amount); await addBalance(trx, toId, amount); }); } // deductBalance 和 addBalance 都接收 trx不自己开事务5.3 接口响应格式不统一有的接口返回{ data: ... }有的返回{ result: ... }前端处理起来很麻烦。t3code 要求所有接口响应必须经过统一的formatResponse函数。排查时可以用 grep 搜索res.json或res.send看看有没有绕过封装的地方。5.4 常见问题速查表问题现象可能原因排查方法解决方案前端字段拿不到类型定义不同步对比前后端类型文件哈希CI 加类型同步检查事务报错嵌套事务检查服务函数是否自己开事务事务只在最外层开响应格式混乱绕过封装grep 搜索 res.json统一走 formatResponse提交被拦截格式或测试失败看钩子输出本地先跑 format 和 test查询变慢缺少索引看慢查询日志给常用查询字段加索引避坑技巧不要等到项目大了才引入 t3code 的约定。我见过团队在 50 个接口之后才想统一格式结果改造成本极高。最好在项目初始化时就定好规矩后面只是执行问题。6. 工具选型与团队协作建议6.1 编辑器与插件配置t3code 不绑定编辑器但推荐 VS Code 加几个插件ESLint、Prettier、Error Lens。Error Lens 能把类型错误直接显示在代码行旁边不用把鼠标悬停上去才能看到。团队里最好统一.vscode/settings.json把保存时自动格式化的选项打开。{ editor.formatOnSave: true, editor.codeActionsOnSave: { source.fixAll.eslint: true } }6.2 代码审查清单t3code 的代码审查不需要逐行看重点看几个地方接口层有没有做参数校验、服务层有没有跨层调用、数据层有没有手写 SQL 字符串、类型定义有没有重复。我通常会在 PR 模板里放一个 checklist审查者对着勾就行。6.3 新人上手流程新人第一天只需要做三件事clone 仓库、跑npm install、跑npm run test。如果测试全绿说明环境没问题。然后从api目录挑一个最简单的接口顺着接口层、服务层、数据层读一遍基本就能理解项目的组织方式。我带的几个新人用这种方式平均半天就能提交第一个 PR。7. 我个人的实操体会这套 t3code 的约定我用了大概一年半最大的感受是它把很多“隐性知识”变成了“显性规则”。以前团队里老成员知道“接口要校验参数”“事务不要嵌套”但新人不知道只能靠口口相传。现在这些规则写在了代码结构里新人读代码就能学到。另一个体会是t3code 的“轻量”是相对的。如果你的项目只有三五个接口强行分三层反而增加负担。我的建议是接口少于 10 个时两层就够了超过 10 个再考虑引入服务层超过 30 个再考虑把类型定义抽成独立包。架构是长出来的不是一开始就设计好的。最后分享一个小技巧在types目录下放一个index.ts把所有类型统一导出。这样其他文件引用时只需要import { User, CreateUserInput } from ../types不用写一长串路径。这个习惯能省下不少敲键盘的时间也让代码看起来更整洁。