api-design — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-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.
Use when designing or reviewing HTTP API surfaces: consumer tasks, audience class, protocol/paradigm fit, resources/actions, route taxonomy, request and response schemas, status codes in context, pagination, filtering, sorting, field selection, idempotency, auth and tenant boundaries, error envelopes, rate-limit signals, versioning, deprecation, discovery, and contract artifacts.
Design clear, durable HTTP API surfaces — the contract another program depends on. This skill covers:
Idempotency-Key for unsafe writes (request fingerprinting, replay, and conflict rules); ETag / If-Match optimistic concurrency for updates.202 Accepted + operation-resource polling.429 + the RateLimit / RateLimit-Policy fields / Retry-After; Deprecation + Sunset headers.This skill owns the product-facing contract of an HTTP API. It does not own the broader interface contract between systems, async event envelopes, stored data design, inbound provider webhook mechanics, framework-specific route handler implementation, or diagnosis of an already failing endpoint.
An API is a product surface for another program. Its main job is stable meaning under change: a consumer that integrated last year should keep working, and a client should be able to tell what happened, what it can do next, and whether retrying is safe without reading server code. Internal convenience should not leak into routes, schemas, or errors unless consumers actually need it.
Prefer boring consistency. A small set of predictable patterns — one error shape, one pagination style, one idempotency mechanism — beats clever endpoint-specific behavior that every client has to rediscover. Consistency is itself a feature: it is what lets a client author one HTTP layer instead of one per endpoint.
Assume observable behavior becomes part of the contract. Clients can come to depend on response shape, ordering, error codes, null behavior, latency class, and even undocumented fields once those details are visible. Decide deliberately what is public and what may change.
Do not expose the database, framework, or internal service layout by accident. A resource is the representation the client needs, not necessarily a table. A route is the public language of the product, not the shape of the implementation. A storage table can split, merge, denormalize, or migrate without changing the API if the representation is stable.
Design the contract first, then implement to it. A contract written as an OpenAPI document (or even a reviewed Markdown spec) before code exists is cheap to change; the same change after three teams have coded against it is expensive. The contract is the artifact frontend, backend, and test authors agree on in parallel — that parallelism is the whole point of writing it down.
Evolve additively; break loudly. Adding an optional field, a new endpoint, or a new enum value a client can ignore is safe. Removing a field, renaming it, tightening validation, or changing a status code is breaking — it requires a new version and an announced deprecation, never a silent in-place edit.
Standards are defaults, not ceremony. Start from HTTP semantics, Problem Details, OpenAPI, documented deprecation headers, and explicit compatibility rules. Deviate only when the consumers and the migration cost justify it.
/orders, /orders/{id}). When a verb has no clean resource (/orders/{id}/cancel, /payments/{id}/refunds), model it as a state transition or a sub-resource, not a free-floating RPC.ETag/If-Match), async job behavior, and retry windows for unsafe operations.Deprecation/Sunset headers.Use this skill when the task is the shape and behavior of an HTTP API endpoint or documented JSON-over-HTTP contract.
| If the task is mainly... | Use |
|---|---|
| REST/resource route taxonomy, request/response shape, pagination, idempotency, versioning, errors | api-design |
| HTTP method/status/header semantics independent of one API surface | http-semantics |
Next.js route.ts, runtime choice, raw body parsing, CORS mechanics, framework defaults | route-handler-design |
| A contract across modules, jobs, services, agents, teams, or multiple transport types | system-interface-contracts |
| Async topics, event envelopes, replay, dead-letter behavior, CloudEvents, AsyncAPI | event-contract-design |
| Tables, keys, constraints, indexes, normalization, stored-data lifecycle | entity-relationship-modeling |
| Inbound third-party webhook signatures, provider retry contracts, raw payload persistence | webhook-integration |
| Verifying consumer/provider compatibility from the API contract | contract-testing |
| A behavior is already broken and needs root-cause isolation | debugging |
This skill may help choose the initial API style, but it should not take over full ownership of non-HTTP or specialized schema work.
| Paradigm | Prefer when | Boundary |
|---|---|---|
| Resource-oriented JSON-over-HTTP | Consumers need broad tooling, cacheable/linkable resources, simple public or partner compatibility, and OpenAPI-style documentation. | api-design owns the endpoint contract; verify method/header details with http-semantics. |
| GraphQL-over-HTTP | Consumers need query-shaped reads, selective nested data, or frontend-driven composition, and the organization can govern schema evolution and resolver cost. | Use api-design for the selection tradeoff only; schema/resolver ownership belongs to a GraphQL-specific or broader interface-contract skill when available. |
| gRPC or typed IDL | Internal service-to-service calls need strict generated clients, streaming, low-latency binary transport, or strongly versioned RPC methods. | Route detailed ownership to system-interface-contracts and transport-specific skills; do not force gRPC design into this HTTP API skill. |
| Async events or webhooks | Producers push state changes, consumers subscribe, replay matters, or delivery/retry contracts dominate. | Use event-contract-design for event streams and webhook-integration for inbound provider webhooks. |
Protocol choice depends on consumers, tooling, compatibility, cache behavior, operations, contract ownership, and migration cost. Avoid categorical rules like "public means REST" or "internal means gRPC."
Before drawing routes, name:
If the API has multiple independent consumers, design for the least coordinated one. A public or partner API needs stronger compatibility and deprecation discipline than an internal route called only by one UI bundle. A co-deployed internal surface may relax the version label — but never the additive-vs-breaking discipline.
Start with resources and standard operations:
| Operation | Typical HTTP shape | Notes |
|---|---|---|
| List | GET /orders | Define filters, sort, pagination, and stable ordering. |
| Read one | GET /orders/{orderId} | Use an opaque stable identifier. Decide 404 vs hidden-by-policy 404. |
| Create | POST /orders | Usually returns 201 Created, Location, and the created representation. |
| Replace | PUT /orders/{orderId} | Full replacement; idempotent if the same representation is sent repeatedly. |
| Partial update | PATCH /orders/{orderId} | Define patch format and concurrency behavior explicitly. |
| Delete | DELETE /orders/{orderId} | Idempotent post-condition; repeated calls may return different responses. |
Use an action endpoint when the operation is not a natural resource state transfer:
POST /orders/{orderId}/cancelPOST /exportsPOST /orders/{orderId}/refundPOST /imports/{importId}/retryAction endpoints still need resource discipline: request schema, response schema, auth, idempotency, retry behavior, error types, and compatibility rules. Avoid inventing custom HTTP verbs. Pick one action style and use it consistently. Reach for an action endpoint only when the operation genuinely is not a resource lifecycle change.
API design does not replace http-semantics, but every API contract must respect HTTP semantics. Pick the code by what you want the client to do, not just by category.
| Decision | API design rule |
|---|---|
| Method | Choose by operation meaning, safety, idempotency, payload semantics, and client retry behavior. |
| Status family | The first digit is the contract: 2xx fulfilled, 3xx further action, 4xx client-actionable problem, 5xx server or upstream failure. |
| Retryability | State whether the client may retry, when, and whether it must reuse an idempotency key or conditional header. |
| Client action | Error responses must tell the client whether to fix input, authenticate, ask for permission, retry later, or stop using the endpoint. |
| Representation metadata | Define Content-Type, content negotiation, caching, ETag, Vary, and Location when they matter. |
| Code | Meaning | Client should |
|---|---|---|
200 OK | Success with a body | Use the body |
201 Created | Resource created | Read Location / returned resource |
202 Accepted | Accepted, processing async | Poll the operation/status URL |
204 No Content | Success, no body (e.g. delete) | Proceed; expect no body |
400 Bad Request | Malformed / un-parseable request | Fix the request; do not retry unchanged |
401 Unauthorized | Missing/invalid credentials | Authenticate, then retry |
403 Forbidden | Authenticated but not allowed | Do not retry; lacks permission/scope |
404 Not Found | Resource absent (or hidden for authz) | Do not retry as-is |
409 Conflict | State conflict (duplicate, version clash) | Resolve conflict, then retry |
410 Gone | Resource permanently removed | Stop calling; update integration |
412 Precondition Failed | If-Match/If-Unmodified-Since failed | Re-fetch, reconcile, retry |
415 Unsupported Media Type | Request body media type unsupported | Send a supported Content-Type |
422 Unprocessable Content | Syntactically valid, semantically invalid | Fix field-level errors |
428 Precondition Required | Server requires a conditional request | Retry with If-Match |
429 Too Many Requests | Rate limited | Back off per Retry-After or the RateLimit field's reset parameter |
500 Internal Server Error | Unexpected server fault | Retry with backoff (idempotent ops) |
503 Service Unavailable | Temporarily down/overloaded | Retry per Retry-After |
422 is the right code for "well-formed JSON, business rule violated" (RFC 9110 §15.5.21); reserve 400 for requests the server cannot parse. Both are common — pick one convention and apply it everywhere. Never return 200 OK with an error body for a failed operation — that breaks clients, caches, observability, and retry logic.
Treat schemas as public representations:
x-extensible-enum, use it as documentation or generator guidance — but do not depend on one extension unless the supported tooling is named.include, expand, fields) if clients can request related data.Validation should happen at the API boundary. Do not bind arbitrary request JSON directly to internal entities — that causes mass-assignment and property-authorization failures. Allowlist writable input fields and reject or ignore unknown fields deliberately, with a documented policy.
A collection endpoint is incomplete until it defines ordering, pagination, filtering, sorting, and empty-result behavior.
| Strategy | Best for | Cost / risk |
|---|---|---|
Cursor (keyset) — opaque cursor/next token over a stable sort key | Large or actively changing collections; infinite scroll | Cannot jump to an arbitrary page; cursor must encode the sort |
Offset — ?limit=&offset= (or page=) | Small, bounded, slow-changing sets needing page numbers | At deep offsets the DB reads and discards all skipped rows; inserts cause skipped or duplicated items between pages |
Default to cursor-based pagination for anything that grows; offset is acceptable only for small admin lists. Always define a stable total ordering (e.g. created_at, id) — pagination over a non-deterministic order silently drops and repeats rows.
Cursor rules:
next/prev cursors and/or link relations).200 response, not an error.Offset rules:
Filtering, sorting & field selection — filters and sorts are part of the contract, not pass-through query language:
?status=open&sort=-created_at).?fields=id,status) only when payload size is a real problem; an extra option is a permanent contract. Document default fields, allowed fields, dependency fields, and auth behavior.For every mutating endpoint, decide whether duplicate requests are safe. GET/PUT/DELETE are idempotent by HTTP definition (RFC 9110); POST and PATCH are not.
| Mutation type | Contract decision |
|---|---|
| Natural idempotent update | Use PUT, DELETE, conditional PATCH, or a state transition with a deterministic post-condition. |
| Non-idempotent create/action | Support client-generated idempotency keys when duplicate side effects would harm users. |
| Long-running work | Return 202 Accepted with an operation resource, cancellation semantics, completion states, and retry behavior. |
| Concurrency-sensitive update | Use ETag plus If-Match, a version field, or another explicit precondition. |
Idempotency keys. For create/charge-style POSTs, accept an `Idempotency-Key` request header — the de-facto industry pattern (Stripe, PayPal, Square). It was specified in the IETF httpapi working-group draft draft-ietf-httpapi-idempotency-key-header, but that draft has lapsed (the -07 revision expired 2026-04-18), so treat the convention as the reference, not a ratified standard, and pair it with the provider contracts your clients actually depend on. When using idempotency keys, define:
Idempotency-Key) and which methods accept it — do not require it for naturally idempotent GET/DELETE.409/422) — never silently apply the new body under an old key.409 (or waits on the original) rather than executing twice concurrently.Optimistic concurrency. Return an ETag on reads; require If-Match: <etag> on updates. A stale If-Match fails with 412 Precondition Failed instead of clobbering a change the client never saw; a domain state conflict returns 409 Conflict. Do not collapse both into a generic 400. Use 428 Precondition Required to force conditional writes.
Do not hold a connection open for work that takes seconds-to-minutes. Return 202 Accepted with a link to an operation resource (/operations/{id} or the eventual resource's status URL); the client polls it and reads status: pending|succeeded|failed plus a result or error, with cancellation semantics defined. This keeps timeouts, retries, and progress observable. (If the consumer needs push notification of completion, that is a webhook/event-contract concern — route to webhook-integration / event-contract-design.)
Authorization is part of API design, not only middleware. For each operation, specify:
401 vs 403 mean here.403, 404, or a redacted representation?Property-level authorization matters in both directions (OWASP API3:2023, broken object property-level authorization). Output schemas can leak fields that should be hidden; input schemas can allow writes to fields that should be server-controlled. Bind writable fields to an allowlist so a client cannot set role, owner_id, or is_admin via mass assignment; bind readable fields so internal columns do not leak in responses.
Use one error shape across the whole API. The portable default is RFC 9457 Problem Details for HTTP APIs (published July 2023; obsoletes RFC 7807, same wire format), served as Content-Type: application/problem+json. Migrate old RFC 7807 references to RFC 9457 while preserving the same client-facing problem-type identities where possible — the upgrade is additive, so there is no urgency to migrate the wire format if you already standardized on 7807.
{
"type": "https://api.example.com/problems/insufficient-funds",
"title": "Insufficient funds",
"status": 403,
"detail": "Charge of $50.00 exceeds the available balance of $12.40.",
"instance": "/accounts/12345/charges/67890",
"errors": [
{ "detail": "amount exceeds balance", "pointer": "/amount" }
]
}type is a stable URI identifying the problem class — the field clients branch on. Treat changing a `type` URI or its meaning as a breaking change. RFC 9457 added an IANA "HTTP Problem Types" registry for common type values.title is human-readable and constant per type; detail is instance-specific. Never make clients parse detail prose for logic.type, title, status, detail, and instance. Validation arrays such as errors, violations, or invalid_params are RFC 9457 extension members (§3.2), not standardized base fields — so the errors array shown above is part of your contract to document. Define its member name, item schema, JSON Pointer/field-path convention, ordering, localization, and compatibility rules.200 OK with an error body — that defeats every generic HTTP client and is a classic semantic failure.Validation errors should distinguish: malformed JSON / wrong content type; missing required field; unknown-field policy violation; field type/format violation; domain-rule violation; authorization failure; state conflict.
A rate-limited API must tell clients how to behave. Define:
429 Too Many Requests.Retry-After and a RateLimit reset are present, they should point to the same instant.RateLimit / RateLimit-Policy structured-field headers (draft-ietf-httpapi-ratelimit-headers) carry remaining quota and the window reset as parameters of the single RateLimit field (not the older separate RateLimit-Limit/-Remaining/-Reset headers). It is still an active Internet-Draft, not an RFC — if you adopt it, document the convention explicitly and keep clients tolerant of provider-specific alternatives (e.g. x-ratelimit-*).Define compatibility rules before the first breaking change. For any externally-consumed API, do not ship an unversioned "default" surface — it creates silent breakage the day a change lands. (A tightly-controlled internal API with a known, co-deployed set of clients may defer an exposed version, but it still owes the same additive-vs-breaking discipline.)
Usually backward-compatible (do not bump the version): adding optional response fields; adding new enum values only if clients are required to ignore unknown values; adding optional request fields; adding new endpoints; adding new filters/sorts when old behavior is unchanged.
Usually breaking (need a new version + migration path): removing or renaming fields; making optional request fields required; changing field meaning, unit, precision, enum semantics, or identifier format; changing default sort/order in a client-visible way; changing error type/code semantics; changing pagination cursor format without preserving old cursors during a transition; tightening authorization clients previously depended on.
Pick one versioning scheme and use it consistently:
| Scheme | Example | Trade-off |
|---|---|---|
| URI path | /v1/orders | Most explicit and cache-friendly; coarse — a new version is a whole new tree. Best for public APIs needing parallel versions. |
| Header / media type | Accept: application/vnd.example.v2+json | Keeps URLs stable; harder to test from a browser/curl. |
| Date-based | Stripe-Version: 2026-01-15 | Fine-grained, one pinned version per account; large internal transformation cost (Stripe's model). |
Avoid minor/patch versions in routes. Additive changes land in place; incompatible changes need a major version or a negotiated migration path. Stripe's decade of backward compatibility is built on absorbing this complexity internally via a per-version transformation layer rather than forcing every client to migrate at once.
Deprecation & sunset are distinct stages:
Deprecation response header (RFC 9745) — "no longer recommended." Use Link: <...>; rel="deprecation" to point to migration documentation.Sunset header (RFC 8594) with the removal date — its timestamp must not precede the Deprecation date.Every nontrivial API should have a machine-readable contract artifact. For HTTP APIs, prefer OpenAPI:
For public API portfolios, publish an API catalog (/.well-known/api-catalog, RFC 9727) so consumers can discover available surfaces, documentation, usage policies, versions, and OpenAPI definitions.
For agent/tool consumers, make the same public contract easy to ingest without inventing a second source of truth:
/llms.txt or an equivalent docs index that links to the canonical contract. Treat such files as public documentation support, not a required verification gate, and never a place for secrets, customer data, private paths, or unreleased internals.Contract artifacts do not replace contract tests. OpenAPI describes the provider surface; consumer-driven contract tests capture what a specific consumer relies on and verify the provider against that expectation.
| Anti-pattern | Why it fails | Replace with |
|---|---|---|
200 OK with an error body | Clients, caches, retries, and observability see success. | Real 4xx/5xx status plus Problem Details or stable equivalent. |
| Database-shaped routes and fields | Storage refactors become breaking API changes. | Consumer-facing resource representations with opaque stable IDs. |
| Custom verbs or action sprawl | Every endpoint needs custom client behavior. | Standard methods first; action endpoints only for real non-resource transitions. |
| Offset pagination over large mutable collections | Inserts/deletes cause duplicates, gaps, and expensive deep pages. | Cursor/keyset pagination with deterministic ordering and a unique tiebreaker. |
| Non-idempotent retries | Network ambiguity can duplicate side effects. | Idempotency keys, conditional requests, or explicit "do not retry" rules. |
| Public or partner breaking changes without a migration path | Independent consumers cannot coordinate instantly. | Compatibility rules, major-version or negotiated migration, Deprecation, and Sunset. |
| Closed enum additions without unknown handling | Generated clients crash or reject future values. | Open enum policy, fallback behavior, or version-gated additions. |
| Implicit null, missing, or unknown-field behavior | Clients guess whether absence means unset, hidden, unauthorized, or unchanged. | Document nullable vs optional, patch semantics, and unknown-field policy. |
| Over-broad input binding | Mass assignment and property-level authorization bugs leak through the API. | Separate request models, allowlisted writable fields, and property-level auth. |
| Chatty task flow | Clients stitch many calls together, creating latency and consistency problems. | Expansion, includes, compound resources, batch endpoints, or async jobs when the task requires them. |
Contract decisions for GET /orders:
orders:read; tenant is derived from auth context, not query string.status, created_at[gte], created_at[lt], customer_id.created_at, updated_at; server always appends id as a tiebreaker.limit=50, max limit=200, opaque page[after].{ "data": [...], "page": { "next": "..." } }.200 with data: [].400 problem; invalid cursor → 400 problem; unauthorized tenant access → 404 or 403 according to policy.Contract decisions for POST /orders:
Idempotency-Key because duplicate order creation can harm users.201 Created, Location: /orders/{orderId}, and the order representation.422 Problem Details with field-level errors.409 with a stable problem type.This skill ships a comprehension-eval artifact at examples/evals/api-design.json. The checklist below is the authoring gate for API surface decisions; the eval file is the grader surface.
400 vs 422, 401 vs 403, 409/412 for conflicts).type/code, never 200 + error body.Idempotency-Key with request fingerprinting + replay), concurrency (ETag/If-Match), and duplicate-request behavior, or explicitly reject retries.202 + a pollable operation resource, not a held connection.Deprecation/Sunset), and migration window are stated.| Use instead | When |
|---|---|
http-semantics | The question is purely about HTTP method, status, header, caching, conditional-request, or content-negotiation semantics, rather than whole-surface API design. |
route-handler-design | You are implementing the handler itself — framework-specific request parsing, middleware order, runtime choice, raw body parsing, CORS mechanics — rather than defining the externally-visible contract. |
system-interface-contracts | The boundary is broader than an HTTP API endpoint, such as jobs, modules, events, services, or agent interfaces. |
event-contract-design | You need asynchronous event schema, envelope, topic/channel naming, replay, dead-letter, or compatibility rules. |
entity-relationship-modeling | You need persistence structure, keys, constraints, indexes, normalization, or lifecycle. |
webhook-integration | You are implementing inbound third-party webhook handling, signatures, provider retry semantics, or raw payload persistence. |
contract-testing | The API contract exists and the task is writing the provider/consumer tests that pin compatibility, rather than designing the contract those tests verify. |
semantics | You are naming a single field, status code, or error code for truthfulness, rather than designing the surface. |
debugging | An API already fails and needs root-cause diagnosis. |
api-catalog well-known URI for API discovery: https://www.rfc-editor.org/rfc/rfc9727<!-- skill-graph-context:start (generated — do not edit by hand) -->
Classification
backend-engineeringtrueengineering/api-designhttp-semantics), framework-specific route handler mechanics (use route-handler-design), non-HTTP system contracts (use system-interface-contracts), async event contracts (use event-contract-design), database design (use entity-relationship-modeling), inbound provider webhook mechanics (use webhook-integration), or post-failure diagnosis (use debugging).When to use
Not for
Related skills
contract-testing, testing-strategy, code-review, http-semanticsevent-contract-design, entity-relationship-modeling, debugging, system-interface-contracts, testing-strategy, webhook-integration, semanticsKeywords
API design, REST API, endpoint design, request response schema, status codes, pagination, filtering, idempotency, API versioning, error envelope<!-- skill-graph-context:end -->
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.