api-design-assistant
API Design Assistant
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.
The three questions
For every public surface, ask:
- Can a stranger call this correctly on the first try? (Usability)
- Can you add to it without breaking existing callers? (Evolvability)
- Does it do what the name says — and nothing else? (Surprise)
Usability smells
| 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 |
Evolvability — can you change it later?
| 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?
Surprise — the hidden contract
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 itgetOrCreateUser.list()that returns the first 100 → surprise. Call itlistPageor 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.
Worked example — function review
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.
REST-specific checklist
- Nouns in paths, verbs in methods:
GET /users/42, notGET /getUser?id=42. - Status codes carry meaning: 404 for not-found, 400 for bad input, 422 for valid-but-rejected. Don't
200 {"error": "..."}. - Pagination from day one.
?limit=&cursor=— you'll need it once the table grows. - Error body has a stable shape.
{"error": {"code": "...", "message": "..."}}— clients pattern-match on it. PATCHfor partial update,PUTfor full replace — don't make PUT mean PATCH.
Do not
- Do not add a parameter "just in case." Every parameter is a promise to keep working.
- Do not overload one endpoint/function to do five things controlled by a flag. Five things = five names.
- Do not return different shapes from one endpoint based on a query param. One endpoint, one shape.
- Do not break a public API in a minor version. If the change is breaking, the version bump is major — that's what major means.
- Do not ship an undocumented API and call it "internal." If it's reachable, someone will use it, and now it's public whether you meant it or not.
Output format
## 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>
More from santosomar/general-secure-coding-agent-skills
code-review-assistant
Performs structured code review on a diff or file set, producing inline comments with severity levels and a summary. Checks correctness, error handling, security, and maintainability — in that priority order. Use when reviewing a pull request, when the user asks for a code review, when preparing code for merge, or when a second opinion is needed on a change.
15cd-pipeline-generator
Generates deployment pipelines with environment promotion, approval gates, and rollback triggers based on target infrastructure. Use when wiring automated deployments from CI to staging/production, when the user asks for a release pipeline, or when adding promotion gates to an existing deploy workflow.
1code-pattern-extractor
Identifies recurring structural patterns in a codebase — idioms, copy-paste clones, homegrown abstractions — and characterizes each as a reusable template. Use when learning a codebase's conventions, when hunting for copy-paste that should be a function, or when documenting how this team does things.
1code-comment-generator
Generates code comments that explain non-obvious intent, constraints, and tradeoffs — not what the code already says. Use when code is correct but opaque, when documenting for future maintainers, or when a function's why is harder to see than its what.
1test-guided-bug-detector
Uses failing test results as signals to guide bug search and narrow down candidate fault locations. Use when one or more tests are failing and the user wants to understand what's broken, when CI reports failures, or when triaging a batch of test failures after a change.
1coverage-enhancer
Raises test coverage by identifying uncovered code regions, ranking them by risk, and generating targeted tests that hit them — prioritizing branches and conditions over raw line count. Use when coverage is below target, when untested code is blocking a release, or when deciding which tests to write next.
1