Agent Skills: API Documentation

Document REST APIs with OpenAPI/Swagger specifications, endpoint references, authentication flows, error handling, and SDK guides. Use for API reference docs, Swagger specs, interactive explorers, and client library documentation.

UncategorizedID: cosmix/claude-loom/loom-api-documentation

Install this agent skill to your local

pnpm dlx add-skill https://github.com/cosmix/loom/tree/HEAD/skills/loom-api-documentation

Skill Files

Browse the full folder contents for loom-api-documentation.

Download Skill

Loading file tree…

skills/loom-api-documentation/SKILL.md

Skill Metadata

Name
loom-api-documentation
Description
Document REST APIs with OpenAPI/Swagger specifications, endpoint references, authentication flows, error handling, and SDK guides. Use for API reference docs, Swagger specs, interactive explorers, and client library documentation.

API Documentation

Overview

Produce API docs developers can actually use: an accurate OpenAPI spec as the source of truth, plus reference/auth/error/versioning guides generated or kept in sync with it. Correctness and drift-prevention matter more than prose.

What every API must document

Auth · base URLs per environment · every endpoint + operation · request/response schemas · all response codes (incl. errors) · rate limits (with headers) · pagination · versioning/deprecation policy.

Spec-first vs code-first (choose deliberately)

| Approach | How | Drift risk | Use when | | -------- | --- | ---------- | -------- | | Spec-first | Hand-write OpenAPI, generate server stubs + clients + mocks | Runtime can diverge from spec unless validated | New APIs, contract negotiated across teams, mock-driven frontend | | Code-first | Annotate handlers; framework emits spec (FastAPI, springdoc, drf-spectacular, tsoa) | Spec stays close to code, but annotations can lie | Existing codebase, small team, code is the truth |

Either way, enforce the contract in CI (lint + validate examples + breaking-change diff). Docs that aren't tested against the running API are fiction.

OpenAPI 3.1 — what changed from 3.0 (get these right)

  • Fully aligned with JSON Schema 2020-12. A schema is now a valid JSON Schema; you can set jsonSchemaDialect and use $schema per-schema.
  • nullable: true is GONE. Use a type array: type: [string, "null"].
  • Type can be an array: type: [string, integer].
  • exclusiveMinimum/exclusiveMaximum are numbers, not booleans (draft-4 behavior removed).
  • Top-level webhooks describe events the API sends (see below).
  • Examples split by object: Schema Objects use JSON Schema's examples (an array); Media Type / Parameter Objects use example (singular) or examples (a map of named Example Objects with summary/value). Don't confuse the two.
  • info.license.identifier accepts an SPDX id (e.g., MIT) instead of a URL.
  • $ref may now sit alongside sibling keywords (e.g., description).

⚠ Tooling lag: Swagger UI / some generators still have partial 3.1 support. Verify your renderer and codegen handle 3.1 before committing to type: [..., "null"] everywhere.

Documentation quality rules

  • Write for competent developers: skip patronizing basics; lead with a working example, then explain.
  • Keep schemas DRY with $ref; reuse parameters, responses, securitySchemes from components.
  • Every operation needs a unique operationId — it becomes the generated client's method name. Renaming it is a breaking change for SDK users.
  • Tag endpoints for navigation; realistic example data (not foo/bar); document rate limits with concrete numbers and the exact headers.
  • Validate every example against its schema (Redocly/Spectral catch this).

Examples

OpenAPI 3.1 spec (trimmed to the load-bearing shapes)

openapi: 3.1.0
info:
  title: User Management API
  version: 2.0.0
  license: { name: MIT, identifier: MIT }   # 3.1 SPDX identifier
servers:
  - { url: https://api.example.com/v2, description: Production }
  - { url: https://api.staging.example.com/v2, description: Staging }
security:
  - BearerAuth: []
tags:
  - { name: Users, description: User management }

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers          # stable → SDK method name
      tags: [Users]
      parameters:
        - $ref: "#/components/parameters/LimitParam"
        - name: status
          in: query
          schema: { type: string, enum: [active, inactive, pending] }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserListResponse" }
              examples:              # media-type: MAP of named Example Objects
                page1:
                  summary: First page
                  value:
                    data: [{ id: usr_123, email: john@example.com, status: active }]
                    pagination: { limit: 20, next_cursor: "eyJpZCI6MTIzfQ", has_more: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      summary: Create user
      operationId: createUser
      tags: [Users]
      parameters:
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateUserRequest" }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/User" } } } }
        "409":
          description: Email already exists
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Problem" }

webhooks:                            # 3.1: events the API SENDS
  userCreated:
    post:
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/User" }
      responses:
        "200": { description: Receiver acknowledged }

components:
  securitySchemes:
    BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
    ApiKeyAuth: { type: apiKey, in: header, name: X-API-Key }
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/authorize
          tokenUrl: https://auth.example.com/token
          scopes: { "users:read": Read users, "users:write": Manage users }
  parameters:
    LimitParam:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
  schemas:
    User:
      type: object
      required: [id, email, status]
      properties:
        id: { type: string, example: usr_123 }
        email: { type: string, format: email }
        status: { type: string, enum: [active, inactive, pending] }
        deletedAt: { type: [string, "null"], format: date-time }   # 3.1 nullable
    CreateUserRequest:
      type: object
      required: [email, name, password]
      properties:
        email: { type: string, format: email }
        name: { type: string, minLength: 2, maxLength: 100 }
        password: { type: string, format: password, minLength: 8 }
    UserListResponse:
      type: object
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/User" } }
        pagination:
          type: object
          properties:
            limit: { type: integer }
            next_cursor: { type: [string, "null"] }
            has_more: { type: boolean }
    Problem:                          # RFC 9457 application/problem+json
      type: object
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code: { type: string }
              message: { type: string }
  responses:
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Problem" }
    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit: { schema: { type: integer }, description: Requests per window }
        X-RateLimit-Remaining: { schema: { type: integer } }
        X-RateLimit-Reset: { schema: { type: integer }, description: Unix epoch when window resets }
        Retry-After: { schema: { type: integer }, description: Seconds to wait }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Problem" }

Endpoint reference (Markdown template)

## Create User — `POST /users`

Auth: Bearer token. Idempotent via `Idempotency-Key` header.

**Body**

| Field    | Type   | Req | Notes                                             |
| -------- | ------ | --- | ------------------------------------------------- |
| email    | string | yes | Valid email                                       |
| name     | string | yes | 2–100 chars                                        |
| password | string | yes | ≥8 chars; upper+lower+digit+symbol                |

```bash
curl -X POST https://api.example.com/v2/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c...-once" \
  -d '{"email":"jane@example.com","name":"Jane Smith","password":"SecureP@ss123"}'
```

**201 Created** → `{ "id": "usr_abc123", "email": "jane@example.com", "status": "pending" }`

| Status | Code             | Meaning                  |
| ------ | ---------------- | ------------------------ |
| 401    | UNAUTHORIZED     | Missing/invalid token    |
| 409    | USER_EXISTS      | Email already registered |
| 422    | VALIDATION_ERROR | See `errors[]` per field |
| 429    | RATE_LIMITED     | Honor `Retry-After`      |

Auth flows (document the full lifecycle, not just the header)

## Bearer (JWT)

`POST /auth/login` → `{ "accessToken": "...", "refreshToken": "...", "expiresIn": 3600 }`
Send `Authorization: Bearer <accessToken>`. Access token 1h, refresh 30d; `POST /auth/refresh` to renew.

## API key (server-to-server)

`X-API-Key: sk_live_...`. Never ship keys client-side; scope minimally; rotate ≤90 days; one key per environment.

Documenting errors, rate limits, versioning consistently

Reference the RFC 9457 Problem schema for every error response; maintain one canonical error-code table:

| Code | HTTP | Meaning | Client action | | ---- | ---- | ------- | ------------- | | UNAUTHORIZED | 401 | No/invalid token | Re-auth | | INSUFFICIENT_SCOPE | 403 | Token lacks scope | Request scopes | | VALIDATION_ERROR | 422 | Field validation failed | Inspect errors[] | | NOT_FOUND | 404 | Missing resource | Verify id | | ALREADY_EXISTS | 409 | Duplicate/conflict | Use unique key | | RATE_LIMITED | 429 | Throttled | Wait Retry-After |

Rate limits: always document the window, the limit, and the X-RateLimit-* + Retry-After headers. Versioning: publish supported versions + sunset dates, classify breaking vs non-breaking, and give side-by-side migration examples plus Deprecation/Sunset response headers.

Tooling

| Job | Tool | Command | | --- | ---- | ------- | | Render (3-panel) | Redoc | npx @redocly/cli build-docs openapi.yaml -o docs.html | | Render (try-it) | Swagger UI | docker run -p 80:8080 -e SWAGGER_JSON=/api/openapi.yaml -v $(pwd):/api swaggerapi/swagger-ui | | Embed | Stoplight Elements | <elements-api apiDescriptionUrl="./openapi.yaml" router="hash" /> | | Lint | Spectral / Redocly | spectral lint openapi.yaml · npx @redocly/cli lint openapi.yaml | | Bundle | Redocly | npx @redocly/cli bundle openapi.yaml -o bundled.yaml | | Client gen | OpenAPI Generator | openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o ./client | | Postman | openapi-to-postmanv2 | openapi2postmanv2 -s openapi.yaml -o collection.json | | Mock | Prism | prism mock openapi.yaml | | Contract test | Dredd / Schemathesis | dredd openapi.yaml http://localhost:3000 · schemathesis run openapi.yaml | | Breaking-change diff | oasdiff | oasdiff breaking old.yaml new.yaml |

Keeping docs in sync with code (the real problem)

Docs rot the moment they're decoupled from the running service. Enforce sync mechanically:

  • Runtime validation — proxy requests/responses through the spec (express-openapi-validator, Prism proxy) in dev/staging; a mismatch fails the build.
  • Contract tests in CI — Dredd (example-driven) or Schemathesis (property-based fuzzing derived from the schema) run against the real API; Schemathesis catches undocumented 500s and schema violations you'd never write by hand.
  • Breaking-change gateoasdiff breaking (or openapi-diff) on every PR blocks silent contract breaks.
  • Lint gate — Spectral ruleset enforces house style (descriptions present, operationId unique, examples valid, error responses documented).
  • Single source — generate SDKs and mocks from the spec so they can't disagree; never hand-maintain a second copy of the contract.

Anti-patterns

  • Documenting only happy-path 200s — clients need the 4xx/5xx bodies and codes to handle failure.
  • Prose clients must parse (switching on message text). Give stable machine-readable code/type.
  • example vs examples mixups (schema=array, media-type=map) — renders empty or errors in tooling.
  • Reusing/renaming operationId — silently breaks generated SDKs.
  • Fake data (foo/bar) and fragment-only snippets — show complete, copy-pasteable, realistic requests.
  • Screenshots of JSON instead of copyable code blocks.

Checklists

Spec quality — before publish:

  • [ ] openapi: 3.1.x; nullable via type: [..., "null"] (no nullable:)
  • [ ] Every operation has a unique, stable operationId and is tagged
  • [ ] All response codes documented incl. every 4xx/5xx, each referencing the shared Problem (RFC 9457) schema
  • [ ] Rate-limit responses document X-RateLimit-* + Retry-After; pagination shape documented consistently
  • [ ] Security schemes defined and applied (global security + per-op overrides); OAuth2 flows/scopes listed
  • [ ] example/examples used correctly per object type; every example validates against its schema
  • [ ] Schemas DRY via $ref from components

Sync & release — in CI:

  • [ ] Spectral/Redocly lint passes
  • [ ] Contract test (Dredd/Schemathesis) runs against the real API and passes
  • [ ] oasdiff breaking shows no unintended breaking changes (or version bumped + migration guide written)
  • [ ] SDKs/mocks regenerated from the spec; Deprecation/Sunset headers set for retiring versions