interface-design — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited interface-design (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
An interface is a promise to its callers. Design for the people (and services) calling it — make the easy thing the correct thing, and treat every published contract as expensive to change. Once shipped, unknown consumers depend on shape, semantics, and error behavior; breaking them is a [[migration-path]], not a quick edit.
Good interfaces are small, explicit, consistent, and evolvable. Bad interfaces leak internals, overload with options, or fail silently — every caller handles errors differently forever.
Pairs with [[spec-first]] for requirements before contracts, [[migration-path]] when changing live surfaces, [[hardening]] at trust boundaries, [[resilience]] for timeouts/idempotency on network contracts, [[data-modeling]] when the API exposes persistence shape, and [[test-first]] / contract tests to lock the promise.
Skip for purely internal helpers with no external callers — still apply if the helper is likely to become public. Skip redesigning published APIs without a migration plan.
Work in order. Know callers and constraints before naming fields.
Before designing the shape:
| Question | Why |
|---|---|
| Who calls this? (frontend, other service, script, model tool) | Different needs — sync vs async, human vs machine |
| What's the common case? | Optimize the default path |
| What's rare but required? | Don't distort common case for edge |
| Sync or async? | Timeouts, polling, webhooks, events |
| Idempotency needed? | Retries, duplicate delivery ([[resilience]]) |
| Auth / tenancy? | Scoping in contract ([[hardening]]) |
| Existing conventions? | Match pagination, errors, naming in codebase |
Write the call site you wish existed before implementing the provider.
// Design target — common case is obvious
const order = await orders.create({ cartId, idempotencyKey });
// Not — caller must know magic
await api.post('/order', { c: cartId, type: 1, flags: 0 });Caller-first beats implementer-first:
The common case should be short, with sensible defaults. Rare options go in optional objects — not positional boolean explosion.
// Prefer optional config object
searchProducts({ query, filters: { inStock: true } });
// Not boolean positional
searchProducts(query, true, false, null, 20);If the simplest use case needs five arguments and a comment — redesign.
Names are part of the contract:
Order, Cart, PaymentIntentcreateOrder, cancelSubscription — or HTTP verbs on REST pathscreatedAt not timestamp1; emailAddress not email if ambiguous in contextamountCents, durationMs, weightKg — not bare amountisActive, hasVerifiedEmail — not active (ambiguous type)Match existing codebase conventions — don't invent a parallel error or pagination style.
Avoid leaking implementation — RedisCacheKey, DbRow, InternalId in public API.
Types and schemas beat documentation warnings:
status: 'pending' | 'paid' not stringid; update may partialOrderId vs UserId — prevent cross-entity mistakes at compile time// Create — no id, required cartId
{ "cartId": "...", "couponCode": "SAVE10" }
// Not one blob where everything is optional
{ "id": null, "cartId": null, "data": { ... } }Validation at the boundary ([[hardening]]) — schema, OpenAPI, zod, protobuf — enforces the contract.
Ambiguous failure (null, false, empty 200) forces every caller to guess.
Define:
429 rate limit, 5xx server
CART_EMPTY, PAYMENT_DECLINED — not only English prose{
"error": {
"code": "CART_EMPTY",
"message": "Add items before checkout",
"requestId": "req_abc",
"retryable": false
}
}Don't leak stack traces, SQL, or internal ids to clients ([[hardening]]). Log details server-side ([[observability]]).
For libraries: Result<T, E>, typed exceptions, or error codes — not mixed null and throw without documented rules.
Expose the minimum that callers need:
| Public | Internal |
|---|---|
| Stable functions/types exported from package entry | Helpers, adapters, DB row mappers |
| OpenAPI-documented routes | Handler private methods |
| Event payload schema | Full domain entity with 40 fields |
Every public symbol is a compatibility commitment. Prefer:
internal / package-private / non-exported by defaultOrderSummary, not raw OrderRowIf callers need internals, the interface is wrong — fix the surface, don't make everything public.
Interfaces feel "designed" when patterns repeat:
| Pattern | Pick one per API |
|---|---|
| Pagination | cursor + nextCursor vs page + total |
| Timestamps | ISO-8601 UTC strings vs epoch ms — document |
| IDs | string uuid vs int — consistent |
| Sorting/filtering | sort=-createdAt, filter query syntax |
| Bulk operations | batch endpoint shape, partial failure reporting |
| Versioning | URL prefix /v2, header, or additive-only |
New endpoints copy the existing pattern — not a third pagination style.
REST-ish conventions (adapt to your stack):
POST /orders, GET /orders/{id}PUT/DELETE where appropriate; POST createsGraphQL / RPC: same discipline — typed inputs, explicit errors, don't expose entire DB.
Query design:
?include=lineItemsVersioning when you must break ([[migration-path]]):
Messages are interfaces too — often harder to change:
eventType, schemaVersion, payloadDon't publish full internal entity as event — publish what downstream needs, stable and small.
Every public contract needs a change story:
| Change type | Approach |
|---|---|
| New optional response field | Safe — add |
| New required response field | Breaking for strict clients — version or optional first |
| Rename field | Dual-field period ([[migration-path]]) |
| Remove field | Deprecate → metric zero → remove |
| Behavior change | Feature flag, version bump, or new endpoint |
Document in OpenAPI/README/changelog. Contract tests catch accidental breaks ([[test-first]]).
Record significant boundary choices in [[decision-docs]] when teams will relitigate.
Interface design overlaps [[hardening]] and [[resilience]]:
Retry-After on 429Server implements validation; client docs say what's allowed — both sides align.
If docs drift from code, callers trust wrong shapes — verify generated docs from types where possible ([[source-first]]).
New REST endpoint
Caller sketch → schema → errors → authz → pagination if list → OpenAPI → contract test → implement.
New npm/internal package export
Facade types → minimal exports → error model → semver policy → document breaking change process.
Split monolith module to service
Define API that replaces in-process calls → same shapes initially → migrate callers → trim to service boundary ([[migration-path]]).
Event for downstream analytics
Minimal payload → version field → sample events in doc → idempotent consumer guide.
LLM tool definition
JSON schema strict → validate args server-side → allowlist actions → same authz as HTTP ([[llm-feature-engineering]]).
Breaking rename
Add new field/endpoint → dual-read/write period → deprecate old → metric → remove.
PR review of API change
Diff OpenAPI → identify breaking → migration plan → error codes consistent → examples updated.
200 OK with { "success": false } or ambiguous null~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.