agentleFS
svngokusvngoku/coding-agents-skills/skills/api-design/SKILL.md

api-design

Skill12 starsChanged 23 days agoMITView on GitHub

Design and review intuitive, scalable, maintainable HTTP APIs. Use this skill whenever the user wants to design a new REST API, review an existing API or spec, write OpenAPI 3.x definitions, or work with HTTP semantics (GET/POST/PUT/PATCH/DELETE), status codes, idempotency (Idempotency-Key), error envelopes (RFC 7807 problem+json), pagination, filtering, versioning, or API auth (API keys, OAuth2 client credentials, rate limits). Also trigger for "API design", "RESTful", "endpoints", "OpenAPI", "Swagger", "ReDoc", "contract testing", or GraphQL and gRPC design questions.

191 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use db-design, everything-claude-code and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • yuexueyuSkill

    db-design

    数据库工程师技能 - Schema设计、索引优化、并发安全、数据库审计。当你涉及数据库表/字段/索引/SQL/迁移/MCP数据库操作、建表改表、写查询时必须使用此技能。即使用户只是说"加个字段"或"查下数据",也应触发。

    Also in 4 projects that use this

  • InfopibeSkill

    everything-claude-code

    Also in 9 projects that use this

  • mryeastcodeSkill

    everything-claude-code-conventions

    Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

    Also in 8 projects that use this

  • tpavanipradeepSkill

    nextjs-turbopack

    Next.js 16+ and Turbopack — incremental bundling, FS caching, dev speed, and when to use Turbopack vs webpack.

    Also in 15 projects that use this

  • Indium-AI-LabsSkill

    data-pipeline-design

    Design batch or incremental ETL and ELT pipelines and warehouse models: grain, partitioning, idempotent loads, late-arriving data, data quality tests, PII controls, and cost monitoring, before building them. Use when adding or changing a dbt model, warehouse table, scheduled transformation, or ingestion job. Not for transactional schema design (use the database-design skill) or event-sourced stores (use the event-sourcing-architecture skill).

    Also in 3 projects that use this

  • thejordanleopoldSkill

    third-party-integration

    Use when integrating with external APIs, payment processors (Stripe), webhooks, or third-party services. Use when implementing API clients with authentication and rate limiting, handling webhooks with signature verification and idempotency, writing contract tests for external services, or isolating third-party dependencies behind adapters. Triggers: \"Stripe\", \"payment integration\", \"webhook\", \"third-party API\", \"external service\", \"API client\", \"integrate with\", \"signature verification\", \"idempotency key\".

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

8 rules. 0 are common, 2 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 165 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use sub-resources only for genuinely nested, exclusive ownership (/users/{id}/orders makes sense.

    line 57
  2. Very specific:

    When a client operation is not CRUD — cancel, refund, publish, archive — model it as a POST to a named action sub-resource (POST …

    line 62
  3. Very specific:

    Use a consistent envelope everywhere, preferably RFC 7807 application/problem+json:

    line 119
  4. Very specific:

    Add a code field: a stable, documented string clients can branch on (OUT_OF_STOCK, INVALID_ARGUMENT, RATE_LIMITED).

    line 134
  5. Very specific:

    Add retryable (or derive it): 5xx and 429 are retryable.

    line 135
  6. Distinctive:

    Default to cursor for public/live collections, offset for internal/admin.

    line 151
  7. Very specific:

    Use scopes for least privilege: read:orders, write:orders, admin.

    line 205
  8. Distinctive:

    Run this on any existing API before approving changes or declaring it done:

    line 264

“Common” is measured: kept by at least 6 in 10 of the 165 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

API Design

HTTP APIs are the contract between your service and every client that consumes it. A well-designed API is consistent and predictable: the URL says which resource you are touching, the HTTP method says what happens to it, and the status code plus body say whether it worked and why not. This skill covers designing and reviewing such APIs — resource modeling, HTTP semantics, status codes, error envelopes, pagination, versioning, spec-first OpenAPI 3.x, and authentication basics — REST first, with GraphQL and gRPC covered in passing.

Quick Reference

TaskReference
Error envelopes, RFC 7807, idempotency keys, retry policyerror-handling.md
Cursor vs offset pagination, filtering, sorting, field selectionpagination-filtering.md
Versioning strategies, backward compatibility, deprecationversioning-evolution.md

Core Workflow

Design an endpoint set in five steps:

  1. Model resources and map HTTP semantics — identify the nouns (users, orders, products) and their relationships; name them as plural collections; one method per operation: read → GET, create → POST, full replace → PUT, partial update → PATCH, delete → DELETE, domain action → POST to an action sub-resource.
  2. Design the responses — success shapes, the error envelope (problem+json), and the exact status code per failure mode.
  3. Paginate and filter collections — choose cursor or offset, set defaults, define filter/sort/field-selection syntax.
  4. Write the OpenAPI spec first — the spec is the contract; generate docs and clients from it.
  5. Lock down auth and limits — authentication scheme, least-privilege scopes, rate limits with standard headers.

Resource Modeling

Nouns, not verbs

Resources are nouns; HTTP methods are the verbs. URLs must never contain verbs.

Anti-patternGood
GET /getUser?id=1GET /users/{id}
POST /createOrderPOST /orders
POST /deleteOrder?id=1DELETE /orders/{id}

Collections, sub-resources, actions

PatternExampleMeaning
CollectionGET /orderslist (paginated)
Sub-resourceGET /users/{id}/ordersorders owned by that user
ActionPOST /orders/{id}/canceldomain action with side effects

Relationships

  • Use sub-resources only for genuinely nested, exclusive ownership (/users/{id}/orders makes sense; /orders/{id}/customer does not — the customer is not owned by the order).
  • For non-ownership links, keep the related resource at top level: GET /orders?customer_id=.... Keep nesting shallow — never deeper than one level; embed related IDs in payloads, not whole objects.

Actions are POSTs

When a client operation is not CRUD — cancel, refund, publish, archive — model it as a POST to a named action sub-resource (POST /orders/{id}/cancel). POST is the only method with no idempotency guarantee, which fits side-effecting actions — but pair it with an Idempotency-Key (below).

HTTP Semantics

MethodSemanticsSafeIdempotentNotes
GETread a collection or instance✅✅never changes state; cacheable
POSTcreate a resource, or trigger an action❌❌returns 201 Created + Location
PUTfull replacement of an existing resource❌✅client sends the entire representation
PATCHpartial update (changed fields only)❌❌ by specsend only what changes
DELETEremove a resource❌✅repeat DELETE → 204 or 404, both fine

Safe = no side effects (safe to retry, cache, prefetch). Idempotent = N identical requests produce the same result as one.

Idempotency (PUT vs POST, Idempotency-Key)

  • PUT is inherently idempotent — use it when the client can supply the full representation. Same payload twice → same state.
  • POST and PATCH are not idempotent. A retried POST /orders creates two orders. Fix this with the Idempotency-Key request header: the client sends a UUID, the server stores key → response, and replays the stored response instead of re-executing.
POST /orders
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

{ "items": [ { "sku": "SKU-42", "qty": 2 } ] }
  • Require Idempotency-Key on any POST/PATCH that creates or mutates (payments, orders, subscriptions).
  • A replay with the same key but a different body → 409 Conflict.
  • Expire keys after a bounded window (Stripe uses 24h); document the window.
  • On replay, return the original stored response — that is what makes client retries safe.

Status Codes

Pick the narrowest correct code; do not invent meanings. Full 2xx–5xx table (304, 405, 502, 504 and friends): error-handling.md.

CodeUseExample
200 OKsuccessful read, update, or actionGET /orders/{id}
201 Createdresource created (set Location)POST /orders
400 Bad Requestmalformed syntax or missing required fieldinvalid JSON, unknown enum value
401 Unauthorizedmissing or invalid credentialsno/expired token
403 Forbiddenauthenticated but not allowedvalid token, wrong scope
404 Not Foundno such resource or routeGET /orders/9999
409 Conflictstate conflict; request cannot applyduplicate Idempotency-Key body
422 Unprocessable Entitywell-formed but semantically invalidorder with 0 items, negative price
429 Too Many Requestsrate limited (send Retry-After)burst over limit
500 Internal Server Errorunhandled server failurebug, DB outage
503 Service Unavailabledown / overloaded (send Retry-After)maintenance

Common mistakes

  • 200 with an error body — breaks every client's status-code handling. Errors live in 4xx/5xx.
  • 500 for client errors — validation failures are 4xx; 500 triggers alerts and pointless retries for something the client did.
  • 401 vs 403 — 401: who are you (no/expired credentials); 403: you are not allowed (authenticated, lacking permission or scope).

Error Responses

Use a consistent envelope everywhere, preferably RFC 7807 application/problem+json:

{
  "type": "https://api.example.com/problems/out-of-stock",
  "title": "Order cannot be placed",
  "status": 422,
  "detail": "SKU-42 has only 3 units in stock",
  "code": "OUT_OF_STOCK",
  "retryable": false,
  "errors": [{ "field": "items[0].qty", "code": "OUT_OF_STOCK", "message": "Only 3 units available" }]
}
  • type — stable machine-readable problem URI; title — short human summary; status — HTTP status; detail — human explanation; instance — URI of the request/resource.
  • Add a code field: a stable, documented string clients can branch on (OUT_OF_STOCK, INVALID_ARGUMENT, RATE_LIMITED). Never branch on the human-readable title/detail.
  • Add retryable (or derive it): 5xx and 429 are retryable; 4xx are not (except 408/425/429).
  • Field-level validation failures → an errors[] array with field, code, message.
  • Serve as Content-Type: application/problem+json; document every code in your API reference.

Pagination, Filtering, Sorting

Cursor vs offset

AspectOffset (?page=2&page_size=50)Cursor (?cursor=...)
Stabilitydrifts when rows are inserted/deleted mid-pagingstable — pages are anchored
UXsupports jump-to-page Nnext/prev only (no jump)
ImplementationOFFSET/LIMIT (trivial)keyset: WHERE (id > :cursor) LIMIT n
Best foradmin tables, small datasetslive, high-churn feeds
Orderinganyrequires a stable sort key

Default to cursor for public/live collections, offset for internal/admin. Set a default page size (25–50) and a hard max (100–1000); clamp or return 400 on overflow — and document it.

{ "data": [{ "id": "ord_8f2", "total": "129.00" }],
  "pagination": { "next_cursor": "eyJpZCI6Im9yZF84ZjIifQ", "has_more": true } }

Filtering, sorting, field selection

ConcernConventionExample
Exact match?key=value on known fields?status=shipped
Ranges*_gte / *_lte suffix (or [gte])?created_at_gte=2024-01-01
Sortingsort param; - prefix = descending?sort=-created_at,sku
Field selectionsparse fieldsets?fields=id,name,price

Validate every filter and sort field against a whitelist — unknown keys → 400, never silently ignored.

Versioning

StrategyExampleProsCons
URL path/api/v2/ordersvisible, cacheable per version, easy routingmultiple code paths over time
Media typeAccept: application/vnd.api+json; version=2clean URLshidden from casual inspection, harder to debug
Query param/orders?v=2trivial to addpollutes cache keys and logs; widely discouraged

URL path is the default for public APIs. Never version with query parameters for a public API.

Backward compatibility

Additive (safe, no version bump)Breaking (requires a new version)
new optional fields or query parametersremove or rename a field
new endpoint; widen enums; relax validationchange a field's type or meaning; make an optional field required

Evolve additively as long as you can; version only when a breaking change is unavoidable. When deprecating: announce it, keep the old version alive on a documented sunset schedule, and emit Deprecation / Sunset (RFC 8594) response headers.

Spec-First with OpenAPI 3.x

  1. Write the OpenAPI spec before the implementation — it is the single source of truth for shape, examples, and behavior.
  2. Lint it in CI — Spectral rules catch missing responses, undocumented status codes, and naming drift.
  3. Generate docs and clients from the spec — ReDoc / Redocly, Swagger UI, openapi-generator, openapi-typescript, orval.
  4. Contract-test it and treat spec and implementation as one — Schemathesis fuzzes the spec against the running service; Pact handles consumer-driven contracts; when a review finds a mismatch, fix the spec first, then the code.

Authentication & Rate Limiting

Schemes

SchemeUse forHowNotes
API keysimple machine clients, first-party toolsAuthorization: Bearer <key> or X-API-Key: <key>long-lived; treat as a secret; support rotation
OAuth2 client credentialsmachine-to-machinePOST /oauth/token (grant_type=client_credentials&scope=read:orders) → access tokenshort-lived tokens
OAuth2 authorization codeuser-facing apps acting for a userbrowser flow → access tokendelegate to an IdP (Auth0, Clerk, …)

Use scopes for least privilege: read:orders, write:orders, admin. A token with only read:orders calling a write endpoint → 403.

Rate limiting

On exceeding the limit, return 429 with Retry-After (seconds) plus the standard headers: X-RateLimit-Limit (window quota), X-RateLimit-Remaining, X-RateLimit-Reset (epoch seconds when the window resets). Document your window ("100 requests/minute per API key") and whether limits are per-key, per-IP, or per-user.

Worked Example: Orders API

An orders resource with a lifecycle — the endpoint set, and why each choice:

MethodPathSemantics
GET/orderslist, cursor-paginated, filterable by status, sortable by created_at
POST/orderscreate; requires Idempotency-Key; returns 201 + Location
GET/orders/{id}fetch one; 404 if unknown
PATCH/orders/{id}partial update (e.g. shipping address)
POST/orders/{id}/canceldomain action (triggers refunds); idempotency-keyed; 409 if already shipped
GET/orders/{id}/itemssub-resource — items belong exclusively to the order

Notes: no DELETE /orders — physical deletion is not part of the lifecycle; cancellation is a named action. No PUT — partial updates fit PATCH. Errors use the RFC 7807 envelope; collections use cursor pagination.

OpenAPI 3.1 excerpt (paths, security, components — trimmed):

openapi: 3.1.0
info: { title: Orders API, version: 2.0.0 }
servers: [{ url: https://api.example.com/v2 }]
security: [{ oauth2: [read:orders, write:orders] }]
paths:
  /orders:
    get:
      summary: List orders
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [created, confirmed, shipped, cancelled] } }
        - { name: cursor, in: query, schema: { type: string, description: "Opaque pagination token" } }
        - { name: limit, in: query, schema: { type: integer, maximum: 100, default: 25 } }
      responses:
        "200": { description: A page of orders, content: { application/json: { schema: { $ref: "#/components/schemas/OrderPage" } } } }
        "429": { description: Too many requests, headers: { Retry-After: { schema: { type: integer } } }, content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
    post:
      summary: Create an order
      parameters:
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string, format: uuid } }
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/NewOrder" } } } }
      responses:
        "201": { description: Order created, headers: { Location: { schema: { type: string } } }, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        "422": { description: Unprocessable entity, content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } } }
components:
  securitySchemes:
    oauth2: { type: oauth2, flows: { clientCredentials: { tokenUrl: https://auth.example.com/oauth/token, scopes: { read:orders: Read orders, write:orders: Create and update orders } } } }
  schemas:
    Problem: { type: object, required: [type, title, status, code], properties: { type: { type: string, format: uri }, title: { type: string }, status: { type: integer }, detail: { type: string }, instance: { type: string }, code: { type: string }, retryable: { type: boolean } } }
    Order: { type: object, required: [id, status, total, currency], properties: { id: { type: string, example: ord_8f2 }, status: { type: string, enum: [created, confirmed, shipped, cancelled] }, total: { type: string, example: "129.00" }, currency: { type: string, example: USD } } }
    NewOrder: { type: object, required: [items], properties: { items: { type: array, minItems: 1, items: { type: object, required: [sku, qty], properties: { sku: { type: string }, qty: { type: integer, minimum: 1 } } } } } }
    OrderPage: { type: object, required: [data, pagination], properties: { data: { type: array, items: { $ref: "#/components/schemas/Order" } }, pagination: { type: object, required: [has_more], properties: { next_cursor: { type: string }, has_more: { type: boolean } } } } }

Pre-Review Checklist

Run this on any existing API before approving changes or declaring it done:

#AreaCheck
1Naming & URLsPlural nouns, consistent casing, no verbs in URLs
2Methods & idempotencyGET never mutates; PUT full replace, PATCH partial; creating/mutating POST/PATCH accept Idempotency-Key
3Status codesNo 200-with-error-body; 4xx for client errors; narrowest correct code
4ErrorsOne envelope everywhere (RFC 7807); stable machine-readable codes documented
5PaginationCursor or offset chosen and documented; default + max page size; has_more / next token
6Filtering/sortingParams whitelisted and validated; unknown keys → 400
7VersioningStrategy documented; no silent breaking changes; deprecation has a sunset plan
8Spec fidelityOpenAPI exists, linted, matches behavior (contract tests in CI)
9Auth & rate limitsLeast-privilege scopes; X-RateLimit-* headers; Retry-After on 429
10SecurityTLS everywhere; no secrets in URLs/logs; no stack traces in responses

Anti-Patterns to Avoid

  • Verbs in URLs (/getUser, /createOrder) — the method is the verb.
  • 200 with an error body — clients branch on status codes; you have just broken all of them.
  • 500 for client mistakes — mis-validated input is a 4xx; 500 means a server bug.
  • Deep nesting (/users/{id}/orders/{oid}/items/{iid}) — flat resources + query params scale better.
  • Bare POST without idempotency on creates and money-adjacent operations — double-submits are real.
  • Unbounded collections — every list endpoint paginates or dies.

When to Use / Not Use

Use this skill when:

  • Designing a new REST API, or a new set of endpoints for an existing service
  • Reviewing an API design doc, an OpenAPI spec, or a PR that changes endpoint behavior
  • Establishing API design standards for a team or platform

Do not use when:

  • The "API" is a library or function interface (Python SDK internals, method signatures) — different concerns
  • Designing event payloads or message-broker contracts (Kafka, SQS) — no status codes or idempotency keys in the same sense
  • The user wants a deep GraphQL- or gRPC-only treatment — this skill covers them in passing only; use dedicated GraphQL/gRPC guides

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.