skills/code-quality/api-design-assistant/SKILL.md
Reviews and designs API contracts — function signatures, REST endpoints, library interfaces — for usability, evolvability, and the principle of least surprise. Use when designing a new public interface, when reviewing an API PR, when the user asks whether a signature is well-designed, or when planning a breaking change.
npx skillsauth add santosomar/general-secure-coding-agent-skills api-design-assistantInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
An API is a promise to strangers. Every decision you make now constrains every user forever — or forces a breaking change. Design for the caller you'll never talk to.
For every public surface, ask:
| Smell | Why it hurts | Fix |
| --------------------------------- | ------------------------------------------------------- | -------------------------------------------- |
| Boolean parameter | save(true) — true what? The call site is unreadable. | Two methods, or an enum: save(Overwrite.YES) |
| Positional params > 3 | create(a, b, c, d, e) — which is which? | Named/keyword params, or a config object |
| Inconsistent naming | getUser() but fetchOrder() but loadProduct() | Pick one verb per concept. Grep before naming. |
| Stringly-typed | setMode("fast") — typo → silent no-op | Enum, or fail loudly on unknown strings |
| Return null for "not found" | Every caller must check; most won't | Optional/Maybe, or throw, or a sentinel — be consistent |
| Out parameter | parse(input, &result, &error) | Return a struct/tuple. Out params are C legacy. |
| Required call order | Must call init() before use() | Make it impossible to get wrong: use() inits if needed, or the constructor inits |
| Property | Makes future changes… |
| --------------------------------- | ------------------------------------------------------- |
| Accepts an options object/struct | Easy to add fields. Positional args → every addition is breaking. |
| Returns an object, not a tuple | Can add fields. Tuple → new field breaks destructuring. |
| Versioned (REST: /v1/, library: semver) | Possible. Unversioned → every change is scary. |
| Tolerates unknown input fields | Old clients can talk to new servers |
| Output fields nullable/optional by default | Can add a field old clients don't read |
Adding is cheap. Removing is forever. Every parameter, every field, every endpoint: could you live with it for 5 years?
The name IS the documentation most callers read. If behavior doesn't match the name:
getUser(id) that creates a user if missing → surprise. Call it getOrCreateUser.list() that returns the first 100 → surprise. Call it listPage or document loudly.setX(v) that also triggers a network call → surprise. Setters should be cheap.close() that can't be called twice → surprise. Idempotent close is the convention.Proposed:
def send_email(to, subject, body, html, cc, bcc, attachments, retry, async_):
...
Review:
| Issue | Severity | Fix |
| --- | --- | --- |
| 9 positional parameters | High — call sites will be unreadable | Required: to, subject, body. Rest: keyword-only with defaults. |
| html is a boolean | Medium — send_email(..., True, ...) means what? | body_format: Literal["text", "html"] = "text" |
| async_ — name with trailing underscore | Low — Python keyword collision, but ugly | background: bool or defer: bool |
| attachments type unclear | Medium — list of paths? bytes? file objects? | Type annotation + docstring. Or a typed Attachment class. |
| No way to add headers later without a 10th param | High — evolvability | Wrap the optional stuff: send_email(to, subject, body, *, options: EmailOptions = None) |
Suggested:
def send_email(
to: str | list[str],
subject: str,
body: str,
*,
body_format: Literal["text", "html"] = "text",
cc: list[str] = (),
bcc: list[str] = (),
attachments: list[Attachment] = (),
retry: int = 0,
) -> EmailResult:
Keyword-only after *. async_ dropped — make it a separate send_email_async(). Return a result object so you can add message_id, delivered_at, etc. later.
GET /users/42, not GET /getUser?id=42.200 {"error": "..."}.?limit=&cursor= — you'll need it once the table grows.{"error": {"code": "...", "message": "..."}} — clients pattern-match on it.PATCH for partial update, PUT for full replace — don't make PUT mean PATCH.## Usability
| Issue | Severity | Fix |
| ... | ... | ... |
## Evolvability
<what happens when you need to add X — can you, without breaking callers?>
## Surprise
<does behavior match the name? list mismatches>
## Suggested signature / contract
<concrete revision>
development
Extracts human-readable pseudocode from a verified formal artifact (Dafny, Lean, TLA+) while preserving the verified properties as annotations, so the proof-carrying logic can be reimplemented in a production language. Use when porting verified code to an unverified target, when documenting what a formal spec actually does, or when handing a verified algorithm to an implementer.
development
Translates natural-language or pseudocode descriptions of concurrent and distributed systems into TLA+ specifications ready for the TLC model checker. Identifies state variables, actions, type invariants, safety properties, and liveness properties from the description. Use when formalizing a protocol, when the user describes a distributed algorithm to verify, when designing a consensus or locking scheme, or when starting formal verification of a concurrent system.
testing
Reduces a TLA+ model so TLC can actually check it — shrinks constants, adds state constraints, abstracts data, or applies symmetry — when the state space is too large to enumerate. Use when TLC runs out of memory, when checking takes hours, or when a spec works at N=2 and you need confidence at larger scale.
development
TLA+-specific instance of model-guided repair — reads a TLC error trace, identifies the enabling condition that should have been false, strengthens the corresponding action, and maps the fix to source code. Use when TLC reports an invariant violation or deadlock and you have the code-to-TLA+ mapping from extraction.