SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# Patterns — Designing REST APIs

# Contents

  • URL and method matrix
  • Pagination envelope (cursor)
  • problem+json error schemas
  • Idempotency-Key handling
  • OpenAPI 3.1 skeleton
  • Gotchas

# URL and method matrix

text
GET    /v1/orders               list (paginated)          200
POST   /v1/orders               create                    201 + Location
GET    /v1/orders/{id}          read                      200 / 404
PUT    /v1/orders/{id}          full replace (idempotent) 200 / 404
PATCH  /v1/orders/{id}          partial update            200 / 404 / 409
DELETE /v1/orders/{id}          delete (idempotent)       204 / 404
POST   /v1/orders/{id}/refund   non-CRUD action           201 / 409
GET    /v1/customers/{id}/orders  nested read-only view   200

Nest at most one level; deeper relations get top-level resources with filters (/v1/orders?customer_id=7), not /customers/7/orders/3/items/9.

# Pagination envelope (cursor)

json
{
  "data": [{ "id": "ord_01J8", "status": "active" }],
  "pagination": {
    "next_cursor": "eyJpZCI6Im9yZF8wMUo4In0",
    "has_more": true,
    "limit": 20
  }
}

Request: GET /v1/orders?limit=20&cursor=eyJpZCI6.... The cursor is opaque (base64 of the last sort key) — clients must not parse it. limit above the max of 100 → clamp to 100, do not error.

# problem+json error schemas

Header: Content-Type: application/problem+json

json
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "2 fields are invalid.",
  "instance": "/v1/orders",
  "errors": [
    { "field": "email", "message": "must be a valid email address" },
    { "field": "quantity", "message": "must be between 1 and 99" }
  ]
}

Conflict example:

json
{
  "type": "https://api.example.com/errors/state-conflict",
  "title": "Order already shipped",
  "status": 409,
  "detail": "Order ord_01J8 cannot be cancelled after shipment.",
  "instance": "/v1/orders/ord_01J8/cancellation"
}

Define the schema once under components/schemas/Problem and $ref it from every 4xx/5xx response.

# Idempotency-Key handling

python
# Pseudocode for POST endpoints that create resources.
# Keys expire after 24h — long enough for client retry storms, short enough
# to bound storage.
def create_order(request):
    key = request.headers.get("Idempotency-Key")
    if key:
        cached = idempotency_store.get(key)
        if cached:
            if cached.request_hash != hash(request.body):
                return problem(422, "Idempotency-Key reused with different body")
            return cached.response          # replay, no side effect
    response = do_create(request.body)      # single side effect
    if key:
        idempotency_store.put(key, hash(request.body), response, ttl_hours=24)
    return response

# OpenAPI 3.1 skeleton

yaml
openapi: 3.1.0
info: { title: Orders API, version: 1.0.0 }
servers: [{ url: https://api.example.com/v1 }]
paths:
  /orders:
    get:
      parameters:
        - { name: limit, in: query, schema: { type: integer, maximum: 100, default: 20 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/OrderList" }
        "429": { $ref: "#/components/responses/Problem" }
    post:
      parameters:
        - { name: Idempotency-Key, in: header, schema: { type: string } }
      responses:
        "201": { $ref: "#/components/responses/Order" }
        "422": { $ref: "#/components/responses/Problem" }
components:
  schemas:
    Problem:
      type: object
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: string }

# Gotchas

  • 404 vs 403 leaks existence. If callers must not learn a resource exists, return 404 for both missing and forbidden.
  • PUT with partial body silently nulls fields — that is correct replace semantics; if clients expect merge, they need PATCH. Document which you offer.
  • Offset pagination drifts when rows are inserted mid-scan; deep offsets also get slow. Cursors fix both — default to them.
  • Trailing slashes: /orders and /orders/ must not be two resources. Redirect or normalize one to the other.
  • Enum widening is breaking for clients that switch exhaustively. Adding an enum value is only additive if the spec documents "unknown values may appear" from v1.
  • Date-times: always RFC 3339 UTC (2026-08-05T14:30:00Z); epoch integers and local times cause silent client bugs.