Node.js Code Style
定位
写像老练工程师写的 Node.js/TypeScript:代码短,但不是压缩;抽象少,但边界准确;主流程顺,但错误和状态不糊;类型不是装饰,而是把业务约束钉在代码里。
这个 Skill 管代码级设计和语法纪律。架构选择、模块边界、DDD、ports/adapters、任务队列和运行生命周期,使用 nodejs-backend-engineering。
先后顺序
- 正确性和业务语义
- 数据安全、权限、幂等、额度、租户隔离
- 类型表达和运行时校验
- 错误上下文和可观测性
- 主流程可读
- 紧凑和少抽象
简洁不是少写几行,而是让读者少猜。任何“省事写法”如果隐藏失败、状态、权限或副作用,都不算优雅。
工作方式
- 先读相邻代码:命名、错误类型、schema、logger、测试风格以项目为准。
- 找边界:API body、query、env、DB JSON、queue message、cache、第三方响应都不可信。
- 先建模再写流程:有限状态用 union;成功/失败分支显式;ID 和业务值别全用
string。 - 写主流程:入口做校验,业务函数拿干净输入,早返回减少嵌套。
- 收紧抽象:只保留能隐藏复杂度、保护不变量、减少真实重复的函数或模块。
- 自查验证:type-check/test/lint/build 按项目最小必要命令跑;不能跑就说明原因。
代码规则
1. 让非法状态尽量不可表示
不要靠注释解释“这个字段只在 completed 时有”。用 discriminated union 表达状态,让调用方必须处理分支。
type TaskResult =
| { status: 'pending'; taskId: TaskId }
| { status: 'running'; taskId: TaskId; startedAt: Date }
| { status: 'completed'; taskId: TaskId; collectionId: CollectionId }
| { status: 'failed'; taskId: TaskId; errorCode: string; message: string };
状态、错误码、provider、任务类型、权限类型都优先用有限集合。字符串散落判断是后端腐烂的早期信号。
2. 类型边界要窄,运行时边界要硬
TypeScript 不能校验线上输入。外部数据先 unknown,在边界用项目已有 schema/parser 校验,之后再进入 service/domain。
const input = CreateTaskSchema.parse(await req.json());
return taskService.createTask(input);
不要让业务函数到处接 Request、Context、process.env、第三方原始 response 或未解析 JSON blob。
3. 主流程连续,复杂度下沉
主流程应读得出业务动作顺序;复杂度放到有名字的深模块里,而不是拆成一堆浅 helper。
好函数边界:
- 隐藏外部协议、重试、分页、签名、响应清洗。
- 保护业务不变量,如配额预留、状态流转、金额计算、权限判定。
- 独立表达算法、规则、排序、合并、去重。
- 消除真实重复,且名字比代码更清楚。
坏函数边界:
- 只调用一次,名字只是复述实现。
- 把连续业务流程拆碎,读者必须来回跳。
- 为“以后可能复用”提前抽象。
- 为单测制造生产接口。
4. 命名必须带业务重量
- ID 写全:
userId、taskId、collectionId,不要跨函数传裸id。 - 布尔值用
is/has/can/should,避免反向布尔名。 - 数组用复数,map/set 带 key 语义,如
tasksByCollectionId。 - 函数动词准确:
fetch远程取,load本地/DB 取,create新建,reserve预留,consume扣减,release回退,enqueue投递。 - 少用
data/info/result/item/temp/manager/helper/common/core,除非局部语境极小。
5. 错误是接口的一部分
catch 只能做四件事:补上下文、分类、补偿、重新抛出。禁止空 catch,禁止失败后返回空数组/空对象装作成功,除非业务明确允许且有日志/指标。
try {
await quotaRepository.consume(reservationId);
} catch (cause) {
throw new AppError('Failed to consume reserved quota', {
cause,
code: 'QUOTA_CONSUME_FAILED',
context: { reservationId, userId },
});
}
对外返回稳定错误码;日志保留内部 cause 和定位字段;响应不要泄露 secret、token、完整敏感 payload。
6. async 要有超时、取消和并发语义
- 无依赖才
Promise.all。 - 允许部分失败才
Promise.allSettled,且逐项保留上下文。 - 第三方调用、批处理、流式处理要考虑 timeout、AbortSignal、限流和 backpressure。
- 有顺序、事务、锁、额度、幂等或短路语义时,不要机械并发。
- 后台任务必须能解释 retry、补偿、重复投递的结果。
7. 条件表达要显性
- 业务分支优先早返回或
switch。 - 有限状态分支用 exhaustive check,不要默认分支吞掉未来状态。
- 三元只用于短小纯表达式;嵌套三元和有副作用三元要拆。
- 可选链只用于确实可选的数据。已校验必传对象缺失,应暴露错误。
8. 注释记录约束,不复述代码
应该注释:
- 业务规则背后的原因。
- 历史数据、第三方怪癖、协议兼容。
- 幂等、事务、缓存、锁、重试的不变量。
- 不直观算法、正则、时间窗口。
不应该注释:
// get user by id// loop items// return result
9. 依赖和文件改动要克制
新增依赖前先查标准库和项目已有依赖。只为几行代码引入一个包,通常不是好交易。
只动任务相关文件。不要顺手重排、批量格式化、重命名、删除无关注释。发现旁边问题,记录,不混入当前改动。
按需读取
references/type-design.md:状态、错误、ID、Result、exhaustive check 的 TypeScript 建模。references/refactoring-and-smells.md:基于 Fowler、Ousterhout、Google 工程实践的重构判断和坏味道。references/real-code-patterns.md:Node.js 后端常用代码片段,包含边界校验、错误映射、并发、超时和日志。
交付自查
- 外部输入已校验或窄化。
- 状态和失败分支不是散落字符串。
- 没有无意义
any、空catch、生产console.log。 - 没有用可选链掩盖必传对象缺失。
- 没有无业务语义兜底。
- 没有把连续主流程拆成浅 helper。
- 错误有 cause/context,日志有定位字段且不泄露敏感数据。
- 改动跑过最小必要验证,或明确说明不能跑的原因。