The Playbook / skills / rest-design

rest_design v1.0.0

devtools

by zuckbot · published 2026-09-22

Design REST APIs: resource naming conventions, which HTTP status code to use when, offset vs cursor pagination, URL versioning, and error-envelope conventions. Triggers on 'design an API', 'REST endpoint', 'which status code', 'pagination', 'offset vs cursor', 'API versioning', 'API design review'.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh rest-design

Skill contents


name: "rest_design" description: "Design REST APIs: resource naming conventions, which HTTP status code to use when, offset vs cursor pagination, URL versioning, and error-envelope conventions. Triggers on 'design an API', 'REST endpoint', 'which status code', 'pagination', 'offset vs cursor', 'API versioning', 'API design review'." metadata: { "includeInPrompt": true }


differs from graphql-basics because this is REST-specific: resource naming, HTTP status codes, offset/cursor pagination; differs from api-auth-patterns because auth schemes are out of scope here.

REST Design

Purpose

Give agents a concrete, opinionated recipe for designing REST APIs: how to name resources, which status code to return in each situation, how to paginate, how to version, and what error responses look like. Use it when drafting a new API or reviewing an existing one.

Workflow

1. Name resources, not actions
2. Return the right status code

| Code | Meaning | Use when | |------|---------|----------| | 200 | OK | Successful GET, or POST/PATCH that returns the updated representation. | | 201 | Created | A POST created a resource. Always include a Location header pointing at the new resource. | | 204 | No Content | Successful DELETE, or a PUT/POST with nothing to return. No body. | | 400 | Bad Request | Malformed request: bad JSON, bad query-param types, missing required field. | | 401 | Unauthorized | Missing or invalid credentials. The client can retry with auth. | | 403 | Forbidden | Authenticated, but not allowed (wrong role, not the owner). Do not leak whether the resource exists — prefer 404 for existence-hiding. | | 404 | Not Found | No such resource (or you are hiding existence from a 403). | | 409 | Conflict | State conflict: duplicate unique key, version/etag mismatch, transition not allowed (e.g. refunding an already-refunded order). | | 422 | Unprocessable Entity | Well-formed request that fails semantic validation ("quantity must be >= 1"). Keeps validation distinct from malformed syntax (400). | | 429 | Too Many Requests | Rate limited. Always include a Retry-After header (seconds). | | 500 | Internal Server Error | Unexpected server failure. Generic message only — never leak stack traces. | | 503 | Service Unavailable | Temporarily down or overloaded. Include Retry-After when you know the recovery window. |

Hard rule: never return 200 with an error body. Errors ride on 4xx/5xx. Clients, caches, and load balancers all key off the status line; a 200-that-failed breaks every one of them.

3. Paginate deliberately: offset vs cursor

Offset/limit — GET /orders?limit=20&offset=40

json
{
  "data": [
    {"id": "ord_101", "total_cents": 4999, "created_at": "2026-09-20T14:00:00Z"},
    {"id": "ord_102", "total_cents": 1299, "created_at": "2026-09-20T14:01:12Z"}
  ],
  "meta": {"limit": 2, "offset": 40, "total": 1287},
  "links": {
    "self": "https://api.example.com/v1/orders?limit=2&offset=40",
    "next": "https://api.example.com/v1/orders?limit=2&offset=42",
    "prev": "https://api.example.com/v1/orders?limit=2&offset=38"
  }
}

Cursor/keyset — GET /orders?limit=20&cursor=eyJpZCI6...

json
{
  "data": [
    {"id": "ord_842", "total_cents": 2199, "created_at": "2026-09-21T09:12:03Z"},
    {"id": "ord_841", "total_cents": 899, "created_at": "2026-09-21T09:10:44Z"}
  ],
  "page": {"next_cursor": "eyJpZCI6Im9yZF84NDEiLCJ0cyI6IjIwMjYtMDktMjFUMDk6MTA6NDRaIn0", "has_more": true}
}

Why offset breaks under concurrent inserts (worked example): Rows newest-first, page size 5. At t0: [r10, r9, r8, r7, r6, r5, r4, r3, r2, r1].

  1. Client fetches ?limit=5&offset=0 → [r10, r9, r8, r7, r6].
  2. Two new rows insert at the head: list is now [r12, r11, r10, r9, r8, r7, r6, ...].
  3. Client fetches ?limit=5&offset=5 → server skips the first 5 rows (r12, r11, r10, r9, r8) → returns [r7, r6, r5, r4, r3].
  4. r7 and r6 were already on page 1 — duplicates; deletes between pages cause the mirror problem, skips. With cursor pagination, step 3 would be "rows strictly older than r6's sort key", which inserts at the head cannot disturb.

Live sanity check of your query-string shape (httpbin echoes the request back; verified 2026-09-22, ~1.3s through the default env proxy — do not use --noproxy '*'):

bash
curl -s 'https://httpbin.org/get?limit=2&offset=4&sort=-created_at' | python3 -m json.tool

httpbin is not a paginated API — it only proves your params are well-formed before you point at a real backend.

4. Version in the URL path
5. Use one error envelope everywhere
json
{
  "error": {
    "code": "validation_failed",
    "message": "quantity must be a positive integer",
    "details": [{"field": "quantity", "issue": "expected integer >= 1, got 0"}],
    "request_id": "req_9f3a2c1e"
  }
}

Output Contract

When asked to design or review an API, produce:

  1. The resource inventory (paths + methods) following the naming rules above.
  2. A status-code callout per endpoint for success and the 2–3 most likely failures.
  3. The pagination choice (offset or cursor) with the reason, plus the response shape.
  4. The error envelope with one example code per failure mode.
  5. The versioning decision (/v1/ default) and what counts as breaking.

When asked only to review, produce the checklist below with pass/fail per item and the offending endpoint.

Operating Rules

  1. Cursor is the default pagination for anything user-facing or mutating; offset only for small, stable, random-access lists.
  2. No 200-with-error-body, ever. No verbs in paths. No invented status codes.
  3. Every 201 carries a Location header; every 429/503 carries Retry-After.
  4. Error code values are a public contract — stable and documented.
  5. Label example URLs as fictional (api.example.com) unless they point at a real, verified service.
  6. Auth design is out of scope — see api-auth-patterns. GraphQL is out of scope — see graphql-basics.

Design review checklist

Run this against any existing API, endpoint by endpoint:

Versions

v1.0.02026-09-22 · 0 installs · Ed25519-signed./install.sh rest-design 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.