agentsclimarketplace

Api design

Skill the-open-agent/oss-skills/skills/api-design

Design public interfaces for libraries and tools that other people depend on. Use when designing or reviewing a public API surface, deciding what to export, naming functions and CLI flags, adding configuration options, planning deprecations, or when the user asks "is this a good API" or "how do I change this without breaking users". Covers minimal surface area, error design, async and extension patterns, CLI ergonomics, config precedence, and stability guarantees. Also use before a 1.0 release to freeze the interface.From its SKILL.md

Install
npx -y skills add the-open-agent/oss-skills --skill api-design

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.7 KB, as published. Nobody here has run it

API Design

The difference between an internal API and a public one is that you can fix an internal one on a Tuesday. Every public symbol is a promise you will be asked to keep for years.

The core discipline: export less

Everything exported is a maintenance obligation. Everything private is free.

  • Start with the smallest surface that solves the problem. Adding an export later is a minor release; removing one is a major release plus a migration guide.
  • Explicit export lists. export { a, b } from './x' — never export *, which silently promotes every future internal symbol into your public API.
  • Mark internals unmistakably. _private, internal/ directories, #[doc(hidden)], @internal. Users will still reach in; the marking is what makes it their problem when it breaks.
  • Audit before 1.0. List every exported symbol and justify each one out loud. In most codebases this deletes 30–50% of the surface, and none of the deletions are ever missed.
# TypeScript: see the real public surface
npx api-extractor run --local
# Python: what does `from pkg import *` actually give?
python -c "import pkg; print([s for s in dir(pkg) if not s.startswith('_')])"
# Rust
cargo public-api
# Go
go list -f '{{.Name}}: {{.GoFiles}}' ./...

Wire a public-API snapshot into CI. A diff in that snapshot forces a conscious decision at review time instead of an accidental breaking change at release time.

Function signatures

Arguments. More than three positional parameters, or any boolean parameter, means switch to an options object / keyword arguments. connect(host, 5432, true, false) is unreadable at the call site and impossible to extend without breaking.

// Bad — unextendable, unreadable at call site
connect('db.local', 5432, true, false, 30)
// Good — self-documenting, additive
connect({ host: 'db.local', port: 5432, tls: true, timeoutMs: 30_000 })

Defaults. Every option gets a sensible default. The zero-config path must work for the common case; configuration is for the exceptions. If your quickstart needs six options set, the defaults are wrong.

Return types. Consistent shapes. A function that returns T | undefined | T[] depending on input forces every caller to write a type guard. Pick one.

Naming. Boring and predictable beats clever:

  • Match ecosystem convention (camelCase in JS, snake_case in Python/Rust, MixedCaps in Go)
  • Verb-first for actions (parseConfig), noun for accessors (config)
  • Say what it returns, not how it works: getUserById not queryUserTable
  • Same concept, same word, everywhere. Not remove here and delete there.
  • No abbreviations except universal ones (id, url, db). cfgMgr costs every reader a lookup, forever.

Errors are API

Error behavior is part of your contract, and it is the part users hit under stress.

  • Typed, distinguishable errors. Users must branch on failure mode without regex on message strings — because if you leave them no choice, they will regex on message strings, and then your messages are frozen too.
  • Actionable messages. State what failed, what was expected, and what to do: Config error at config.yml:12 — 'timeout' must be a number, got "30s". Use 30000.
  • Never swallow errors. No empty catch. No returning null for "something broke".
  • Distinguish programmer errors from operational errors. A bad argument is a bug to be thrown loudly; a network timeout is a condition to be returned and handled.
  • Include a cause chain. throw new Error('msg', { cause }), raise ... from e, fmt.Errorf("...: %w", err). Stripping the cause destroys debuggability.

Extension points

Users will need behavior you did not anticipate. Decide deliberately how they get it, because if you provide no seam they will monkey-patch your internals and you will break them accidentally.

MechanismGood forCost
Callbacks / hooksSingle well-known extension pointOrdering and error semantics get subtle
Plugin interfaceMany independent extensionsVersioning the plugin contract
Middleware chainComposable request/response pipelinesDebuggability suffers
EventsObservation, logging, metricsNot suitable for control flow
SubclassingRarely the right answerEvery internal method becomes public API

Prefer composition over inheritance in public APIs. Inheritance makes every protected member a compatibility obligation.

Async and concurrency

  • Do not mix paradigms. Callback and promise variants of the same function double the surface and the bug surface. Pick one; if you must support both, generate one from the other.
  • Make cancellation possible from day one — AbortSignal, context.Context, CancellationToken. Retrofitting cancellation is a breaking change.
  • Never block the main thread in a library. Users cannot work around it.
  • Do not create global state or start background work at import time. A library that opens a connection pool on import is unusable in a serverless function and untestable everywhere.

CLI design

A CLI is an API where the users are humans and shell scripts, simultaneously.

  • <tool> <noun> <verb> for multi-command tools (git remote add, docker image ls). Consistent, discoverable, extensible.
  • Long flags always; short flags for the top five only. --verbose with -v.
  • --help at every level, with examples. Most users never open the docs site.
  • Respect --json. Machine-readable output turns your tool into a building block. Also: detect whether stdout is a TTY and disable colors/spinners when piped.
  • Exit codes matter. 0 success, 1 general failure, 2 usage error. Scripts depend on these.
  • stdout is data, stderr is chatter. Progress bars and logs on stderr, always.
  • Confirm destructive actions, with --yes/--force to skip in automation.
  • Never require interactivity in a non-TTY. A prompt in CI is a hang.

Config precedence, highest to lowest — deviate from this and users will be confused forever: CLI flag → environment variable → project config file → user config file → built-in default. Document it, and provide a way to show the resolved config (tool config show --explain).

Stability guarantees

State the contract explicitly in the README or docs. Users cannot infer it.

## Stability
- Everything exported from the package root is public API under semver.
- Anything under `internal/` or prefixed `_` may change in any release.
- `experimental_*` exports may change in minor releases.
- Node 20+ and Python 3.10+ supported; dropping a runtime is a major release.

Decide and document whether these count as breaking: adding a required config field (yes), changing an error message (no) vs an error type (yes), tightening input validation (usually yes in practice), changing output ordering (yes if unsorted output was documented as stable), and raising the minimum runtime version (yes).

Deprecation

Never delete without a deprecation cycle. The full sequence:

  1. Mark it — @deprecated / #[deprecated] / warnings.warn(DeprecationWarning), with the replacement named in the message.
  2. Warn at runtime, once per process, not per call. A warning in a hot loop gets silenced globally and then nobody sees any warnings.
  3. Document the removal version in the message and the changelog.
  4. Provide the migration path — ideally a codemod (jscodeshift, libcst, comby).
  5. Wait at least one minor release, ideally two, and honor the stated timeline.
  6. Remove in a major release, listed in the migration guide.
/** @deprecated Use `createClient()` instead. Removed in v4.0.0. */
export function makeClient(opts: Options): Client {
  warnOnce('makeClient() is deprecated; use createClient(). Removed in v4.0.0.');
  return createClient(opts);
}

Reviewing an API proposal

Questions worth asking on every new public symbol:

  1. What is the smallest version of this that solves the reported problem?
  2. Can a user already do this by composing existing pieces? (If yes, document that.)
  3. What does the call site look like? Write it before writing the implementation.
  4. How does this fail, and what does the user see?
  5. What is the migration path when we get this wrong?
  6. Does it match the naming and shape of everything nearby?
  7. Does it force a dependency on users who will never call it?

The best answer to a feature request is often a documentation page. The second best is an extension point. A new public function is third.

Anti-patterns

  • export * from the package root.
  • Boolean parameters. render(true) — nobody knows what that does.
  • Config objects with 40 options. Symptom of unmade decisions; split the API.
  • Stringly-typed inputs. mode: 'fast' | 'safe' beats mode: string.
  • Leaking dependency types in your public signatures. Now their major version is your major version.
  • Different names for the same concept across the surface.
  • Silent behavior change in a patch release. This is the trust-destroying one.
  • Designing for hypothetical users. Build for the requests you actually have.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.