# API And Interface Design

> Use when designing module boundaries or public interfaces, when defining error semantics, when adding fields to shared config, or when changing a public function with downstream users.

- Skill: `just-silver/api-and-interface-design` (Agent Skill)
- Install (CLI): `npx skillmds@latest add just-silver/api-and-interface-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/just-silver/api-and-interface-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Just-Silver (https://skillmd.com/u/just-silver)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/just-silver/api-and-interface-design

---


# API and Interface Design（接口与边界设计）

## Overview

好接口让对的事容易、错的事难。每个可观察行为都是潜在承诺：用户够多时，连 bug、报错文本、时序都会有人依赖。设计时就要决定暴露什么、怎么错、怎么变。

## When to Use

- 定模块边界、公开函数、团队间契约时
- 新增 API endpoint、配置字段、事件格式时
- 统一错误语义（抛/返回/null 三选一）时
- 改公开签名、加必填项、做破坏性变更前

**When NOT to use:**

- 纯内部函数重构（无下游、无公开承诺）
- 一次性脚本
- 已有契约且本次不碰边界，只是内部实现

## Hyrum's Law（设计前提）

用户够多时，一切可观察行为都会被依赖——包括未文档化的怪癖、报错文本、时序、顺序。所以：

- **有意暴露**：每个公开项都是承诺，问一句"3 年后还愿意维护它吗"
- **不漏实现细节**：能被观察到就会被依赖；日志格式、内部错误文本不进契约
- **设计时定废弃**：怎么拆、迁移期多长，先写好（见 `deprecation-and-migration`）
- **测试不够**：契约测试全绿也拦不住依赖未文档行为的用户，"安全"改动也可能破下游

## Contract First（先契约后实现）

```
// 先定契约：参数、返回值、错误码，冻结后再实现
// 例：main(argv) -> exitCode，内部归一，不直接退出进程
```

- 契约即 spec，实现跟着走；扩展只加可选，不改已有签名
- 扩展用对象可选字段，不用位置参数加参
- IO 与纯逻辑分开：纯函数保持纯输入输出，可测部分返回结果对象而非只打日志

## 边界划分

- **公开面最小**：下游只调编排入口，拼装细节（解析/渲染/写盘/交互）全内部不公开
- **副作用隔离**：混了日志/写盘的函数不公开；公开即把日志格式变成 API
- **生成物封装**：自动生成文件由拥有者模块封装，下游不直引
- **构建契约同等**：靠特殊机制（拼接/截断/生成）的行视同公开契约，改即 breaking

## 错误语义统一（三选一，全仓一致）

- **内部只抛，不返回 null/{error}**：null 分不清失败与缺席，`{error}` 让每个调用方写 if 必然漏接
- **顶层一个出口归一**：唯一入口 `try/catch`，返 exit code，内部不 `process.exit`、不各自打印
- **预期失败 vs Bug**：预期失败抛具名错误（code + 用户可行动提示 + hint）；非预期让原始异常上抛 + 堆栈，不包装吞掉
- **禁混用**：有的抛、有的返 null、有的返 `{error}` 即违规，下游无法预测

## 边界校验（信内不信外）

- 校验在系统边：CLI 参数、API 路由、外部响应、环境变量加载处
- 校验完内部信任类型，不层层复验
- **第三方返回视为不可信**：先验 shape 再进逻辑/渲染
- 内部函数之间、刚出自家 DB 的数据不重复校验

## 只加不改（兼容演进）

- 新字段一律可选 + 合理默认值，缺失自动填，不中断老用户
- 读取层 `?? default`，缺失不报错；非法值才报错
- 老配置回归测试：不带新字段跑全流程，全绿才合
- 转必填等下个 major，提前文档 + CHANGELOG 声明废弃期
- One-Version：不逼用户二选一，扩展不分叉

## 命名可预测

| 场景 | 约定 | 例 |
|------|------|----|
| 公开函数 | 动词 + 对象 | `runInstall`, `createTask` |
| 布尔 | is/has/can 前缀 | `isForced`, `hasConfig` |
| 错误码 | UPPER_SNAKE | `TARGET_NOT_EMPTY`, `NOT_FOUND` |
| 配置字段 | camelCase（或本仓约定） | `targetDir`, `dryRun` |

## Quick Reference

| 场景 | 动作 |
|------|------|
| 定边界 | 公开面最小，副作用不公开，生成物有人封装 |
| 定错误 | 内部抛具名错误，顶层归一 exit code，不混 null/{error} |
| 加字段 | 可选 + 默认 + 老配置回归绿，major 才收紧 |
| 改签名 | 先废弃别名 + 迁移期（见 deprecation），不直改 |
| 接外部数据 | 先验 shape，第三方返回当不可信 |

## Common Rationalizations

| Excuse | Reality |
|--------|---------|
| "多公开几个方便测试" | 测试直引内部函数即把实现锁成契约；可测性靠纯函数 + 结果对象，不是公开一切 |
| "返回 null 简单" | null 语义模糊，调用方必漏接；具名错误 + 顶层归一才是简单 |
| "加个必填字段而已" | 必填即 breaking；可选 + 默认 + 迁移期，否则等 major |
| "报错文本随便写" | Hyrum's Law：文本会被依赖；code 稳定，message 可读，细节放 hint |
| "内部校验一层更保险" | 层层复验即噪音；边界验完内部信类型，保险靠测试不是复验 |

## Red Flags — STOP

- 公开面含拼装细节/副作用函数/生成物直引
- 错误语义三混（抛 + null + `{error}` 并存）
- 新必填字段无迁移期
- 公开改名无别名直接改
- 第三方返回未验即用
- 报错文本当契约依赖（下游正则匹配 message）

**以上任一出现 → 停手，回契约/边界修正后再合。**

## Verification

- [ ] 公开面最小且有契约（签名/返回值/错误码）
- [ ] 错误语义全仓统一，顶层归一
- [ ] 边界有校验，内部无复验，第三方返回已验
- [ ] 新增全可选 + 默认，老配置回归绿
- [ ] 命名符合约定，错误码 UPPER_SNAKE

