Merge branch 'feat/v0.7-api-design' — 4 blocks + /api-design
This commit is contained in:
commit
6d382ee939
11 changed files with 1000 additions and 0 deletions
33
_blocks/api-graphql.md
Normal file
33
_blocks/api-graphql.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# API — GraphQL (schema-first, DataLoader, subscriptions, persisted queries)
|
||||
|
||||
Single-endpoint, client-driven query language. Pairs with `auth-sessions.md` / `auth-authorization.md` (identity + field-level authz) and `api-versioning-pagination-ratelimit.md` (Relay cursors + cost-based rate limits).
|
||||
|
||||
## When to include
|
||||
|
||||
- Client needs shape each response themselves (mobile bandwidth, SPA over-fetch, UI-driven demand).
|
||||
- Graph-shaped domain (social, sharing, org charts, document tree) where REST nesting explodes.
|
||||
- Multiple teams own different resolvers behind one gateway (federation / subgraphs).
|
||||
|
||||
## What it declares
|
||||
|
||||
- **Schema-first, not code-first:** `schema.graphql` is the SSoT, committed to the repo. Resolvers are generated types (TS `graphql-codegen`, Rust `async-graphql` derive, Go `gqlgen`) that must implement the schema. Schema-first beats code-first for reviewability, federation, and client codegen.
|
||||
- **SDL only, no custom DSL:** use standard GraphQL SDL — `type`, `input`, `interface`, `union`, `enum`, `scalar`, directives. Custom scalars (`DateTime`, `UUID`, `JSON`) declared once; keep the list short.
|
||||
- **Resolver structure (Apollo / urql / Relay agnostic):** one resolver per field; resolvers return values OR a loader handle, never hit the DB directly in a loop — that's the N+1 trap.
|
||||
- **DataLoader for every 1-to-many or many-to-many field:** Facebook's `dataloader` pattern (batch + per-request cache). Without it, a query `users { posts { comments { author { name } } } }` issues O(N³) queries; with it, exactly 4. Implementations: `dataloader` (JS, reference), `async-graphql` built-in (Rust), `graphql-dataloader` (Go), `aiodataloader` (Python).
|
||||
- **Pagination: Relay cursor spec** — `type FooConnection { edges: [FooEdge!]! pageInfo: PageInfo! totalCount: Int } type FooEdge { node: Foo! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }`. See `api-versioning-pagination-ratelimit.md`.
|
||||
- **Errors:** don't throw — return the GraphQL error envelope. Expected errors (not-found, unauthorized, validation) go in `errors[]` with `extensions.code` taxonomy (`NOT_FOUND`, `FORBIDDEN`, `BAD_USER_INPUT`, `RATE_LIMITED`). Unexpected errors → generic `INTERNAL_SERVER_ERROR`, server-side logged with correlation id.
|
||||
- **Subscriptions — pick transport explicitly:** **graphql-ws** (RFC-like WebSocket sub-protocol, Apollo-server + urql default; replaces the deprecated `subscriptions-transport-ws`) OR **graphql-sse** (HTTP Server-Sent Events, no WS infra). WebSocket needs auth on `connectionInit` (token in payload), reconnect strategy, and a resumable cursor — SSE is simpler where you don't need client→server push.
|
||||
- **Persisted queries (APQ / PQ):** hash the query at build time, send only the hash at runtime. Stops query-bombing attacks, cuts bandwidth, and enables CDN caching of `GET /graphql?hash=...`. Apollo Automatic Persisted Queries, Relay persisted queries, Hasura allow-list all implement this. PRODUCTION-ONLY allow-list the hashes — reject unknown queries.
|
||||
- **Depth + cost limiting:** every query runs through a cost analyser (e.g. `graphql-cost-analysis`, `graphql-armor`) and rejects when depth > N (typically 10) or cost > budget. Without this, a 20-line query can DoS the DB.
|
||||
- **Introspection:** ON in dev and staging (the whole tooling assumes it). OFF on the public-facing prod endpoint unless you operate a public API — combine with persisted-query allow-list.
|
||||
- **Field-level authz:** directive-based (`@auth(role: ADMIN)`) OR middleware in the resolver. Either way — check permission INSIDE the resolver, NOT only at the HTTP layer; a single GraphQL POST hits dozens of resolvers.
|
||||
- **Libraries:** **TS server**: GraphQL Yoga, Apollo Server 4, Mercurius (Fastify). **TS client**: Apollo Client, urql, Relay. **Rust**: async-graphql (schema-first via derive). **Go**: gqlgen. **Python**: Strawberry, Ariadne. **Federation**: Apollo Federation 2 (`@key`, `@extends`, `@external`), Cosmo, Hive — only if you truly have multiple subgraphs.
|
||||
|
||||
## References
|
||||
|
||||
- GraphQL spec (https://spec.graphql.org/October2021/) [E1 — normative, October 2021 revision current].
|
||||
- GraphQL over HTTP + GraphQL over WebSocket (graphql-ws) + graphql-sse [E1 — working group specs].
|
||||
- Relay Cursor Connections (https://relay.dev/graphql/connections.htm) [E1].
|
||||
- DataLoader — Facebook OSS (https://github.com/graphql/dataloader) [E2].
|
||||
- Apollo Federation v2 docs, Hasura docs, gqlgen docs, async-graphql docs [E2 — production-deployed].
|
||||
- Evidence grade [E2] — GitHub v4 API, Shopify Admin, Facebook, Netflix all production GraphQL.
|
||||
39
_blocks/api-openapi-first.md
Normal file
39
_blocks/api-openapi-first.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# API — OpenAPI-First (3.1 as single source of truth)
|
||||
|
||||
Machine-readable contract that drives server stubs, client SDKs, docs, mocks, and contract tests from ONE file. Pairs with `api-rest-conventions.md` (the HTTP rules the spec encodes) and `api-versioning-pagination-ratelimit.md` (versioning + pagination schemas).
|
||||
|
||||
## When to include
|
||||
|
||||
- Any REST API with ≥2 consumers (web + mobile, public + partner, multiple internal services).
|
||||
- API that must publish SDKs in >1 language — spec-driven codegen beats hand-written clients per language.
|
||||
- Regulated API (finance / health) where the contract must be reviewable and diff-able as a single artefact.
|
||||
|
||||
## What it declares
|
||||
|
||||
- **OpenAPI 3.1.0** — the 2021+ version that is a strict superset of JSON Schema 2020-12. Use 3.1 unless a specific tool pins you to 3.0.x; 2.0 (Swagger) is legacy and missing `oneOf/anyOf/nullable` nuances.
|
||||
- **Single file, single source of truth:** `openapi.yaml` (or `.json`) committed at repo root or under `api/`. ALL of the following are GENERATED, never hand-written:
|
||||
- Server routing stubs / request validators (codegen for your stack).
|
||||
- Typed client SDKs (TS, Swift, Kotlin, Python, Rust, Go).
|
||||
- Human docs site (Swagger UI / Redoc / Scalar / Stoplight Elements).
|
||||
- Mock server (Prism, mswjs, Stoplight) for consumer tests before the backend exists.
|
||||
- Contract tests (Schemathesis, Dredd, Pact broker feed).
|
||||
- **Structure:** `info`, `servers` (per environment — prod, staging, sandbox), `paths` (one entry per resource/action pair), `components.schemas` (reusable types), `components.securitySchemes` (bearer / OAuth2 / API-key), `components.parameters` (shared query params like `page`, `cursor`, `limit`), `components.responses` (problem+json 400 / 401 / 403 / 404 / 409 / 422 / 429 / 500 reused by `$ref`), `tags` (grouping for docs).
|
||||
- **Schemas ARE types:** every `$ref` resolves to `components/schemas/*`; no anonymous objects inline inside responses. This makes the codegen output readable and re-usable.
|
||||
- **Error model is shared:** define `Problem` schema once (RFC 9457 shape) and `$ref` it from every 4xx/5xx response. Keeps the error contract identical across 120 endpoints.
|
||||
- **Examples are typed:** every operation has ≥1 request example + ≥1 response example. Examples flow into Redoc docs, mock server responses, and SDK fixtures. Invalid examples break CI — treat them as test data.
|
||||
- **Tooling pick — ONE per job:**
|
||||
- Lint: **Spectral** (`.spectral.yaml` with a ruleset — Google/Microsoft API guidelines ship starter rulesets).
|
||||
- Diff / breaking-change gate: **oasdiff** or **openapi-diff** in CI — PR fails on a breaking change unless `breaking: approved` label.
|
||||
- Codegen: **openapi-generator** (multi-language, mature; prefer `*-axios`, `*-nullable` templates for TS); **orval** for TS + React Query / SWR first-class; **oapi-codegen** for Go; **progenitor** for Rust.
|
||||
- Docs: **Redoc** (read-only, pretty), **Swagger UI** (interactive), **Scalar** (modern, fast), **Stoplight Elements** (embeddable React component). Pick one — documented decision in repo.
|
||||
- **Governance:** `openapi.yaml` change = PR review like code. No drift between spec and server: CI runs the generated server stubs AND contract tests against the running app.
|
||||
- **[UNVERIFIED] claims — forbidden:** never quote an OpenAPI feature without checking the 3.1 spec. `discriminator`, `oneOf`, `nullable` (removed — use `type: [string, "null"]`) are easy to get wrong; cite spec link on debate.
|
||||
|
||||
## References
|
||||
|
||||
- OpenAPI 3.1.0 spec (https://spec.openapis.org/oas/v3.1.0) [E1 — normative].
|
||||
- JSON Schema 2020-12 (https://json-schema.org/specification.html) [E1].
|
||||
- RFC 9457 Problem Details + `api-rest-conventions.md` for the HTTP semantics the spec encodes.
|
||||
- Swagger UI / Redoc / Scalar / Stoplight Elements — all actively maintained as of 2026 [E2].
|
||||
- openapi-generator (https://openapi-generator.tech/), orval (https://orval.dev/), oapi-codegen (https://github.com/oapi-codegen/oapi-codegen) [E2 — production-deployed].
|
||||
- Evidence grade [E2] — pattern is the Stripe / GitHub / Twilio / Shopify default.
|
||||
28
_blocks/api-rest-conventions.md
Normal file
28
_blocks/api-rest-conventions.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# API — REST Conventions (verbs, status codes, resources, idempotency, ETag)
|
||||
|
||||
HTTP-level contract for resource-oriented APIs. Pairs with `api-openapi-first.md` (spec as SSoT), `api-versioning-pagination-ratelimit.md` (list + version policy), and `auth-oauth2-oidc.md` / `auth-sessions.md` (principal + scopes).
|
||||
|
||||
## When to include
|
||||
|
||||
- Public or partner JSON-over-HTTP API where clients are heterogeneous (mobile, SPA, third-party integrations, curl).
|
||||
- Internal service boundary that you want reviewable by humans without generated tooling.
|
||||
- Any API that must degrade gracefully through an HTTP cache / proxy / API gateway.
|
||||
|
||||
## What it declares
|
||||
|
||||
- **Resource naming:** plural nouns, lowercase, kebab-case (`/invoices`, `/invoice-items/{id}`), no verbs in path. Nested resources ≤2 levels deep (`/invoices/{id}/items`); beyond that flatten with query filters. One canonical URL per resource — never two paths for the same entity.
|
||||
- **Verbs (RFC 9110):** `GET` safe + idempotent, `HEAD` metadata only, `PUT` full replace + idempotent, `PATCH` partial (JSON Merge Patch RFC 7396 OR JSON Patch RFC 6902, pick one per API), `POST` create / non-idempotent action, `DELETE` idempotent. Non-CRUD actions → `POST /resource/{id}:action` (Google AIP-136) or a child resource — never `GET /do-thing`.
|
||||
- **Status codes — pick from this set, no creativity:** `200 OK`, `201 Created` (+ `Location` header), `202 Accepted` (async), `204 No Content`, `301/308` (moved), `400 Bad Request` (validation), `401 Unauthorized` (no/invalid credential), `403 Forbidden` (authenticated but not allowed), `404 Not Found`, `409 Conflict` (optimistic-lock / duplicate), `410 Gone`, `412 Precondition Failed` (If-Match mismatch), `415 Unsupported Media Type`, `422 Unprocessable Entity` (semantic validation), `429 Too Many Requests`, `500 Internal Server Error`, `502/503/504` (upstream). `418` is a joke, not a status.
|
||||
- **Error body: RFC 9457 Problem Details** — `{ "type": "https://api.example.com/errors/invoice-not-found", "title": "...", "status": 404, "detail": "...", "instance": "/invoices/42", "errors": [{"field":"amount","code":"negative"}] }`. Content-Type `application/problem+json`. Stable `type` URI = machine key; `title` = human; `detail` = this instance.
|
||||
- **Idempotency-Key header (Stripe / IETF draft-ietf-httpapi-idempotency-key-header):** required on `POST` that creates/charges. Server stores `(key, route, response)` for ≥24 h and replays on retry. Different body with same key → `422`. Missing key on mutating `POST` → `400` for strict APIs, accept + warn for lenient.
|
||||
- **Conditional requests (RFC 9110 §13):** `ETag` on every resource representation (strong `"abc123"` unless you truly serve byte-equivalent variants). Clients send `If-Match: "abc123"` on `PUT` / `PATCH` / `DELETE` — server replies `412` on mismatch. `If-None-Match` + `304 Not Modified` on `GET` for cache revalidation. `Last-Modified` as a weaker fallback only.
|
||||
- **Content negotiation:** `Accept`, `Accept-Language`, `Accept-Encoding` honoured. Default `application/json; charset=utf-8`. Version media types (`application/vnd.example.v2+json`) ONLY if you commit to header-based versioning — see `api-versioning-pagination-ratelimit.md`.
|
||||
- **HATEOAS / hypermedia:** OPTIONAL. Include a `_links` / `links` object per resource when the API is explicitly browsable (HAL, JSON:API, Siren) — it's not required for typed SDKs. Document the choice in `openapi.yaml` and stay consistent.
|
||||
- **Safe-by-default surface:** `GET` never mutates. `DELETE` is idempotent — repeated calls return `204` even if the row is already gone. `PUT` requires the FULL representation; partial field on `PUT` = `400`.
|
||||
|
||||
## References
|
||||
|
||||
- RFC 9110 (HTTP Semantics), RFC 9111 (HTTP Caching), RFC 9457 (Problem Details, 2023), RFC 7396 / 6902 (Merge Patch / JSON Patch), RFC 5988 + 8288 (Web Linking) [E1 — IETF standards-track].
|
||||
- Google AIP (https://google.aip.dev/) and Microsoft REST API Guidelines (https://github.com/microsoft/api-guidelines) — production-grade conventions [E2].
|
||||
- `api-openapi-first.md` — encode this block as the machine-readable SSoT; `api-versioning-pagination-ratelimit.md` — list, cursor, and version policy.
|
||||
- Evidence grade [E2] — every rule here is deployed across Stripe, GitHub, Google, Microsoft production APIs.
|
||||
53
_blocks/api-versioning-pagination-ratelimit.md
Normal file
53
_blocks/api-versioning-pagination-ratelimit.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# API — Versioning, Pagination, Rate Limiting
|
||||
|
||||
Three cross-cutting concerns that every production API hits within the first month. Pairs with `api-rest-conventions.md` (HTTP semantics), `api-openapi-first.md` (where the policy is encoded), and `api-graphql.md` (Relay cursors + cost-based limits).
|
||||
|
||||
## When to include
|
||||
|
||||
- Any API expected to outlive one client release — versioning decided BEFORE launch, not during the first breaking change.
|
||||
- Any endpoint returning a collection — pagination decided BEFORE the dataset grows past 10k rows.
|
||||
- Any API on the public internet or behind a partner quota — rate limits decided BEFORE the first abusive client.
|
||||
|
||||
## What it declares
|
||||
|
||||
### Versioning — pick one strategy, document it
|
||||
|
||||
| Strategy | Example | Pros | Cons | Use when |
|
||||
|---|---|---|---|---|
|
||||
| **URL path** | `/v1/invoices` → `/v2/invoices` | Most visible, curl-friendly, easy CDN routing | Pollutes every path; "v2" is vague | Public API, coarse versions, infrequent bumps. GitHub v3/v4, Stripe-compatible mirrors. |
|
||||
| **Header (media type)** | `Accept: application/vnd.example.v2+json` | Clean URLs, content negotiation native | Invisible in logs/curl; needs client support | Internal APIs with typed SDKs, GitHub v4 hybrid. |
|
||||
| **Date-based** | `Stripe-Version: 2025-11-01` | Fine-grained, every breaking change pinnable | Complex rollout matrix; server must keep N-1 versions live | Pay-for-stability APIs (Stripe, Shopify); regulated domains. |
|
||||
| **GraphQL evolution** | Never break the schema; mark fields `@deprecated(reason: "use X")` and remove after telemetry shows 0 usage | No versions to maintain | Schema grows forever; deprecation discipline required | Any GraphQL API — see `api-graphql.md`. |
|
||||
| **No versioning (additive-only)** | Promise: additions never break clients; removals need a new endpoint | Simplest | Only works with disciplined teams + strong typing | Small internal APIs with ≤3 consumers. |
|
||||
|
||||
Rules that apply to ALL strategies: (a) deprecate with `Deprecation` + `Sunset` headers (RFC 8594, RFC 9745) + 6-month minimum runway, (b) publish a changelog, (c) run the old + new in parallel until telemetry shows the old is unused.
|
||||
|
||||
### Pagination — three patterns, one rule
|
||||
|
||||
- **Offset / page (LIMIT N OFFSET M):** `?page=3&limit=50`. OK for admin UIs over small tables. BROKEN for real data — rows drift during paging, `OFFSET 10000` scans 10k rows on every call. Returns `X-Total-Count` or a `meta.total` field; clients assume random access.
|
||||
- **Cursor (opaque token, keyset/seek):** `?cursor=eyJpZCI6MTIzfQ&limit=50`. Cursor = base64 of `(id, created_at, …)` — opaque to client, ordered by the server's index. Handles drift, O(log n) lookups. Response envelope: `{ data: [...], meta: { next_cursor, prev_cursor, has_more } }`. REQUIRED for any list that can exceed 1k rows or where concurrent writes happen.
|
||||
- **Relay (GraphQL spec):** `first: 50, after: "cursor"` + `Connection { edges, pageInfo { endCursor, hasNextPage } }`. Standardised cursor pattern for GraphQL — see `api-graphql.md`.
|
||||
|
||||
Rule: **default cursor, offer offset only when the UI genuinely needs page numbers**. Never return >1000 items per page; clamp `limit` server-side.
|
||||
|
||||
### Rate limiting — headers + strategy
|
||||
|
||||
- **Token bucket or sliding-window**, per authenticated principal (user / API key / IP). Redis-backed, atomic via Lua. Policy tiers: anon < authenticated < partner < internal.
|
||||
- **Response headers — IETF `RateLimit` (draft-ietf-httpapi-ratelimit-headers, shipped in Cloudflare / GitHub as of 2024):**
|
||||
- `RateLimit-Limit: 1000` — quota in the current window.
|
||||
- `RateLimit-Remaining: 947` — left in the current window.
|
||||
- `RateLimit-Reset: 47` — seconds until reset.
|
||||
- Also accept legacy `X-RateLimit-*` for GitHub/Stripe parity during migration.
|
||||
- **On block: `429 Too Many Requests` + `Retry-After: <seconds>`** (RFC 9110 §10.2.3) + Problem+json body describing the limit that was hit. Always include `Retry-After`; idempotent clients retry cleanly.
|
||||
- **Cost-based for GraphQL:** each field has a cost (e.g. `user: 1, user.posts: 5 per item, search: 50`); query total checked against per-principal budget. See `api-graphql.md`.
|
||||
- **Fail-open on metering outage** is a bug, not a feature — fail-closed with a clear error code (`RATE_LIMITER_UNAVAILABLE`) so clients can alert. Silent "no limit" costs more than a short outage.
|
||||
- **Defence-in-depth:** per-IP (anti-bot), per-principal (anti-abuse), per-endpoint (protect expensive routes), global (protect the cluster). Document all four layers in the repo — hidden layers surprise on-call.
|
||||
|
||||
## References
|
||||
|
||||
- RFC 8594 (Sunset header), RFC 9745 (Deprecation header, 2024), RFC 9110 §10.2.3 (`Retry-After`) [E1 — IETF].
|
||||
- draft-ietf-httpapi-ratelimit-headers (https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/) [E1 — active working group draft, deployed by Cloudflare + GitHub].
|
||||
- Relay Cursor Connections (https://relay.dev/graphql/connections.htm) [E1].
|
||||
- Stripe API versioning post (https://stripe.com/blog/api-versioning) [E2 — production-documented 2017 onward].
|
||||
- GitHub v3 → v4 migration notes, Shopify API versioning [E2].
|
||||
- Evidence grade [E2] — all three policies are production-deployed at Stripe, GitHub, Shopify, Cloudflare.
|
||||
114
skills/api-design/SKILL.md
Normal file
114
skills/api-design/SKILL.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
name: api-design
|
||||
description: Hub-and-spoke pipeline that produces a production-grade API design plan for a new or evolving service — style (REST / GraphQL / tRPC / gRPC), resource model, machine-readable contract (OpenAPI 3.1 or GraphQL SDL), versioning strategy, rate-limit + auth handoff, and codegen toolchain — via pure-click decisions across six phases. Emits a spec skeleton, SDK pick list, and a per-surface checklist; never writes secrets.
|
||||
argument-hint: <one-line API description, e.g. "B2C SaaS, public REST API, 3 clients, cursor pagination, needs Google SSO">
|
||||
---
|
||||
|
||||
# API-Design — Style, Contract & Lifecycle Pipeline (index)
|
||||
|
||||
You are converting "I need an API for X" into a concrete, reviewable plan:
|
||||
which style to ship, what resources exist, what the machine-readable
|
||||
contract looks like, how versions evolve, how rate limits + auth integrate,
|
||||
and which codegen toolchain produces the server stubs + SDKs + docs. Every
|
||||
decision is a click; the only typed input is the one-line description in
|
||||
Phase 1 and the resource list in Phase 2.
|
||||
|
||||
This skill does NOT write production server code. It emits a PLAN plus a
|
||||
contract SKELETON (OpenAPI 3.1 YAML scaffold OR GraphQL SDL scaffold), a
|
||||
versioning decision row, a rate-limit policy row, and a codegen pick list.
|
||||
Server scaffolding is a separate task owned by `new-agent` or the project's
|
||||
code-implementer; auth wiring is delegated to `/auth-setup`.
|
||||
|
||||
The skill reads the four companion blocks heavily — every phase references
|
||||
at least one of them:
|
||||
|
||||
- `_blocks/api-rest-conventions.md` — verbs, status codes, resources, ETag, idempotency.
|
||||
- `_blocks/api-openapi-first.md` — OpenAPI 3.1 SSoT + codegen tooling.
|
||||
- `_blocks/api-graphql.md` — SDL, resolvers, DataLoader, subscriptions, persisted queries.
|
||||
- `_blocks/api-versioning-pagination-ratelimit.md` — strategies matrix.
|
||||
|
||||
---
|
||||
|
||||
## Pipeline overview (6 phases, ≥6 AskUserQuestion calls)
|
||||
|
||||
| Phase | File | Purpose | AskUserQuestion |
|
||||
|---|---|---|---|
|
||||
| 1 | [phase-1-intake.md](phase-1-intake.md) | Style, audience, scale, clients | 3× |
|
||||
| 2 | [phase-2-resource-model.md](phase-2-resource-model.md) | Entities → REST resources / GraphQL types | 1× |
|
||||
| 3 | [phase-3-contract.md](phase-3-contract.md) | Generate OpenAPI spec OR GraphQL SDL skeleton | 1× |
|
||||
| 4 | [phase-4-versioning.md](phase-4-versioning.md) | URL / header / date-based decision | 1× |
|
||||
| 5 | [phase-5-limits-auth.md](phase-5-limits-auth.md) | Pagination + rate limit + auth-setup handoff | 1× |
|
||||
| 6 | [phase-6-codegen.md](phase-6-codegen.md) | openapi-generator / orval / graphql-codegen | 1× |
|
||||
|
||||
Minimum AskUserQuestion count across a full session: **8** (3 in Phase 1 +
|
||||
1 each in Phases 2–6). Exceeds the ≥6 hub-and-spoke contract.
|
||||
|
||||
---
|
||||
|
||||
## Variables the pipeline produces
|
||||
|
||||
| Name | Set in | Meaning |
|
||||
|---|---|---|
|
||||
| `STYLE` | Phase 1 | REST / GraphQL / tRPC / gRPC / hybrid |
|
||||
| `AUDIENCE` | Phase 1 | public / partner / internal |
|
||||
| `SCALE` | Phase 1 | small (<100 rps) / mid (100–10k) / large (>10k rps) |
|
||||
| `CLIENTS` | Phase 1 | subset of {web-spa, mobile-native, server-to-server, cli, browser-form} |
|
||||
| `RESOURCES` | Phase 2 | ordered list of entities + relationships (one-to-many, many-to-many) |
|
||||
| `CONTRACT` | Phase 3 | path to generated `openapi.yaml` OR `schema.graphql` skeleton |
|
||||
| `VERSIONING` | Phase 4 | url-path / header-media / date / additive-only / graphql-deprecate |
|
||||
| `PAGINATION` | Phase 5 | cursor / offset / relay-connection |
|
||||
| `RATELIMIT` | Phase 5 | per-principal bucket + per-endpoint policy row |
|
||||
| `AUTH_HANDOFF` | Phase 5 | recorded decision to run `/auth-setup` next (or skipped + why) |
|
||||
| `CODEGEN` | Phase 6 | generator(s) + target languages |
|
||||
|
||||
---
|
||||
|
||||
## Final report (emit after Phase 6)
|
||||
|
||||
```
|
||||
=== API-DESIGN REPORT ===
|
||||
Description: <first 80 chars of intake>...
|
||||
Style: <STYLE>
|
||||
Audience: <AUDIENCE> Scale: <SCALE> Clients: <CLIENTS list>
|
||||
Resources: <N entities, M relationships>
|
||||
Contract: <CONTRACT path> — <lines LOC> skeleton
|
||||
Versioning: <VERSIONING> + deprecation runway <N months>
|
||||
Pagination: <PAGINATION> RateLimit: <policy summary>
|
||||
Auth handoff: <AUTH_HANDOFF> (run /auth-setup next? yes/no)
|
||||
Codegen: <CODEGEN list — one line per target>
|
||||
Env vars: <count> new entries (none if managed-only)
|
||||
Next: run `compose-solution` or hand off to project code-implementer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules (apply throughout)
|
||||
|
||||
- **Pure-click contract.** Only the Phase 1 intake paragraph and the Phase 2
|
||||
resource list are typed. Every other decision is `AskUserQuestion`.
|
||||
- **RULE 0.4 NO HALLUCINATION.** Never claim an OpenAPI feature, RFC number,
|
||||
or library capability without citing the spec link. If unsure, mark
|
||||
`[UNVERIFIED]` in the report and flag a follow-up. No fabricated version
|
||||
numbers, no invented library features, no made-up SDK names.
|
||||
- **RULE 0.8 Secrets SSoT.** The skill emits env VARIABLE NAMES only
|
||||
(`STRIPE_API_KEY_NAME`, `RATE_LIMIT_REDIS_URL`, ...). It NEVER echoes a
|
||||
token value, never writes to `secrets/*.env`, never suggests hard-coding.
|
||||
- **NO DOWNGRADE.** If the chosen combination is unsafe or contradictory
|
||||
(e.g. "public API + additive-only versioning + no deprecation runway")
|
||||
the skill returns 2–3 constructive alternatives, never "not supported".
|
||||
- **Fail-closed default.** Rate limiter, auth check, and contract-drift
|
||||
gate all default to the safer option when the user is unsure.
|
||||
- **Surgical scope.** Reads the four API blocks; writes a contract
|
||||
skeleton file (Phase 3) and nothing else. Production scaffolding is
|
||||
delegated. Auth wiring is delegated to `/auth-setup`.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- `_blocks/api-rest-conventions.md`, `_blocks/api-openapi-first.md`,
|
||||
`_blocks/api-graphql.md`, `_blocks/api-versioning-pagination-ratelimit.md`.
|
||||
- `_blocks/rule-pre-dev-gate.md` — analogue check before inventing resources.
|
||||
- `skills/auth-setup/SKILL.md` — Phase 5 handoff target.
|
||||
- Evidence grade [E2] — pipeline mirrors Stripe, GitHub, Shopify, Twilio
|
||||
production API lifecycles.
|
||||
115
skills/api-design/phase-1-intake.md
Normal file
115
skills/api-design/phase-1-intake.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Phase 1 — Intake (style, audience, scale, clients)
|
||||
|
||||
One free-text paragraph, then three click batches. This is one of only two
|
||||
phases that accepts typed input (the other is Phase 2's resource list).
|
||||
|
||||
## 1a — Ask for the API description
|
||||
|
||||
Emit a regular message (NOT AskUserQuestion):
|
||||
|
||||
> Describe the API in one paragraph: what problem does it solve, who calls
|
||||
> it, and any constraint I should know (regulated, existing schema,
|
||||
> mobile-first, partner network, realtime). Reply in one message.
|
||||
|
||||
Store the reply verbatim as `INTAKE`.
|
||||
|
||||
## 1b — Style click (AskUserQuestion, single-select)
|
||||
|
||||
Reference: `_blocks/api-rest-conventions.md`, `_blocks/api-graphql.md`.
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Which API style fits the problem?",
|
||||
"header": "Style",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "REST + JSON",
|
||||
"description": "Resource-oriented, HTTP-native, CDN-cacheable. Default for public APIs with heterogeneous clients. See _blocks/api-rest-conventions.md"},
|
||||
{"label": "GraphQL",
|
||||
"description": "Client-shaped queries, graph-shaped domain, federation-ready. Apollo / Yoga / async-graphql. See _blocks/api-graphql.md"},
|
||||
{"label": "tRPC (TypeScript end-to-end)",
|
||||
"description": "TS-only monorepo; server exports types, client imports. Zero codegen. NOT suitable for multi-language consumers."},
|
||||
{"label": "gRPC / Protobuf",
|
||||
"description": "Service-to-service, strong typing, streaming. Over HTTP/2. Browsers need grpc-web. Choose for internal backbones."},
|
||||
{"label": "Hybrid (REST public + GraphQL internal, or gRPC internal + REST edge)",
|
||||
"description": "Common at scale — record BOTH surfaces, run this skill twice if needed."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `STYLE`. If `Hybrid` → warn user that this skill will design the
|
||||
PRIMARY surface first; they can re-run for the secondary.
|
||||
|
||||
## 1c — Audience + scale click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Audience + traffic class?",
|
||||
"header": "Audience+Scale",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "Public internet, small (<100 rps)",
|
||||
"description": "Landing-site API, indie SaaS MVP. Rate limit per IP + per key."},
|
||||
{"label": "Public internet, mid (100–10k rps)",
|
||||
"description": "Growth-stage product. Needs proper quotas, partner tiers, SDK story."},
|
||||
{"label": "Public internet, large (>10k rps)",
|
||||
"description": "Stripe / GitHub tier. Date-based versioning, cost-based limits, federation."},
|
||||
{"label": "Partner / B2B only",
|
||||
"description": "Known callers, NDA, mTLS or signed requests possible. Simpler abuse surface."},
|
||||
{"label": "Internal service boundary",
|
||||
"description": "Inside the VPC / mesh. Skip public rate limits; keep contract tests."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store label components as `AUDIENCE` (public / partner / internal) and
|
||||
`SCALE` (small / mid / large — internal defaults to `small` unless noted).
|
||||
|
||||
## 1d — Clients click (AskUserQuestion, multi-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Which clients will consume the API?",
|
||||
"header": "Clients",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "Web SPA (React / Svelte / Vue)",
|
||||
"description": "CORS, HttpOnly session cookie or Bearer; SDK in TS"},
|
||||
{"label": "Mobile native (iOS / Android)",
|
||||
"description": "Typed SDK (Swift / Kotlin) — favours OpenAPI codegen or GraphQL client"},
|
||||
{"label": "Server-to-server",
|
||||
"description": "Partner backends; mTLS, signed requests, long-lived API keys"},
|
||||
{"label": "CLI / scripts",
|
||||
"description": "curl-friendly, URL-path versioning preferred, stable query params"},
|
||||
{"label": "Third-party developers (public docs)",
|
||||
"description": "Needs Swagger UI / Redoc / Scalar + SDK published to npm / crates.io / PyPI"},
|
||||
{"label": "Browser form / webhook receiver",
|
||||
"description": "Content-Type application/x-www-form-urlencoded or webhook body; idempotency-key required"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `CLIENTS`. Empty selection → re-ask (an API without a known
|
||||
client is premature; push back per NO DOWNGRADE with suggested defaults).
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `INTAKE` non-empty (≥40 chars).
|
||||
- `STYLE` exactly one label.
|
||||
- `AUDIENCE` and `SCALE` parsed from the Phase 1c label.
|
||||
- `CLIENTS` has ≥1 entry.
|
||||
- If `STYLE = tRPC` and `CLIENTS` contains any non-TS client → STOP and
|
||||
re-ask 1b (tRPC is TS-only; offer REST or GraphQL alternatives per NO
|
||||
DOWNGRADE).
|
||||
93
skills/api-design/phase-2-resource-model.md
Normal file
93
skills/api-design/phase-2-resource-model.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Phase 2 — Resource model (entities → resources / types)
|
||||
|
||||
Turn the app description into a list of entities, their relationships, and
|
||||
the actions on them. This is the second and last typed phase — the user
|
||||
types a short entity list in 2a, then one click in 2b locks the shape.
|
||||
|
||||
## 2a — Ask for entities + relationships (typed)
|
||||
|
||||
Emit a regular message (NOT AskUserQuestion):
|
||||
|
||||
> List the core entities (one per line) with an optional `owns→` arrow for
|
||||
> relationships. Example:
|
||||
> ```
|
||||
> User
|
||||
> Invoice owns→ InvoiceItem
|
||||
> Customer owns→ Invoice
|
||||
> Tag
|
||||
> Invoice many-to-many→ Tag
|
||||
> ```
|
||||
> Keep it to ≤10 entities. Anything beyond core can be added after launch.
|
||||
|
||||
Store the parsed list as `RESOURCES`. Each entry is
|
||||
`{name, owns: [child...], many_to_many: [peer...]}`. If parsing fails,
|
||||
re-ask with the exact syntax rules shown above.
|
||||
|
||||
## 2b — Shape click (AskUserQuestion, single-select)
|
||||
|
||||
Reference: `_blocks/api-rest-conventions.md` (REST resources),
|
||||
`_blocks/api-graphql.md` (types + connections).
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "How should the resources surface?",
|
||||
"header": "Shape",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "Flat REST (one resource per entity, ≤2 levels nesting)",
|
||||
"description": "`/invoices`, `/invoices/{id}/items`. Deeper nesting flattened via query filters. Default for REST."},
|
||||
{"label": "REST + sub-resources for every owns→ relation",
|
||||
"description": "`/customers/{id}/invoices`, `/invoices/{id}/items`. Readable, curl-friendly; nesting budget 2 levels."},
|
||||
{"label": "GraphQL types + Relay Connections",
|
||||
"description": "Each entity → `type Foo { ... }`, each list → `FooConnection` with cursor pagination. See api-graphql.md"},
|
||||
{"label": "GraphQL federation (subgraph per entity cluster)",
|
||||
"description": "Apollo Federation 2 @key directives. ONLY pick when you truly have multiple teams / subgraphs."},
|
||||
{"label": "Mixed: REST for public CRUD, GraphQL for dashboards",
|
||||
"description": "Record both — this skill will generate primary surface; rerun for secondary."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `SHAPE`. Gate on `STYLE` from Phase 1:
|
||||
|
||||
- `STYLE = REST` → accept Flat REST or sub-resource REST; reject GraphQL
|
||||
options (re-ask).
|
||||
- `STYLE = GraphQL` → accept GraphQL types or federation.
|
||||
- `STYLE = tRPC / gRPC` → SHAPE defaults to "flat" (procedures per entity);
|
||||
skip the click, record `SHAPE = flat-procedures`.
|
||||
- `STYLE = Hybrid` → emit a warning that both shapes will be skeleton'd in
|
||||
Phase 3.
|
||||
|
||||
## 2c — Emit resource-to-action matrix (inline, no AskUserQuestion)
|
||||
|
||||
Print a table the user can tweak before Phase 3 generates the contract.
|
||||
Example for a notes + tags API:
|
||||
|
||||
```markdown
|
||||
| Entity | Create | Read | Update | Delete | List | Search |
|
||||
|---------------|:------:|:----:|:------:|:------:|:----:|:------:|
|
||||
| User | - | ✓ | ✓ | - | ✓ | ✓ |
|
||||
| Note | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Tag | ✓ | ✓ | - | ✓ | ✓ | - |
|
||||
| NoteTag (m2m) | ✓ | - | - | ✓ | - | - |
|
||||
```
|
||||
|
||||
- Rows = entries from `RESOURCES`.
|
||||
- Columns = CRUD + List + Search (drop columns that don't apply per
|
||||
entity).
|
||||
- Cells = `✓` (endpoint exists) / `-` (intentionally absent).
|
||||
- Admin-only actions marked `admin✓`.
|
||||
|
||||
User ackowledges the table (no click — implicit); Phase 3 uses it as input.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `RESOURCES` has ≥1 entry; parsed shape `{name, owns, many_to_many}` valid.
|
||||
- `SHAPE` is compatible with `STYLE` (gate above).
|
||||
- Resource-to-action matrix printed with every entity as a row.
|
||||
- Non-trivial m2m relations surfaced as explicit join entities OR
|
||||
GraphQL edge types — no implicit joins hidden in handlers.
|
||||
111
skills/api-design/phase-3-contract.md
Normal file
111
skills/api-design/phase-3-contract.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Phase 3 — Contract skeleton (OpenAPI 3.1 OR GraphQL SDL)
|
||||
|
||||
Generate the machine-readable SSoT from `RESOURCES` + `SHAPE`. This is the
|
||||
ONLY phase that writes a file outside the skill — the user picks WHERE
|
||||
(phase 3a) and the skill writes a skeleton there (phase 3c).
|
||||
|
||||
Reference: `_blocks/api-openapi-first.md`, `_blocks/api-graphql.md`.
|
||||
|
||||
## 3a — Contract path click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Where should the contract skeleton be written?",
|
||||
"header": "Contract path",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "api/openapi.yaml (repo root / api/)",
|
||||
"description": "Default for REST. Picks up OpenAPI 3.1 tooling by convention."},
|
||||
{"label": "schema.graphql (repo root)",
|
||||
"description": "Default for GraphQL. Picks up graphql-codegen / async-graphql by convention."},
|
||||
{"label": "docs/api/<name>.yaml (docs folder)",
|
||||
"description": "Keep contract alongside human docs; publish site from same folder."},
|
||||
{"label": "Skip — I'll commit the skeleton manually from chat",
|
||||
"description": "Skill prints the full skeleton inline; user copies into repo."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `CONTRACT_PATH`. Validate: the path must be consistent with
|
||||
`STYLE` (REST → `.yaml`/`.json`; GraphQL → `.graphql`/`.graphqls`).
|
||||
If inconsistent → re-ask per NO DOWNGRADE, listing the two correct options.
|
||||
|
||||
## 3b — Contract skeleton rules
|
||||
|
||||
The skeleton is NOT the final spec — it is a scaffold the user will flesh
|
||||
out. Rules:
|
||||
|
||||
- **OpenAPI 3.1 skeleton** must include:
|
||||
- `openapi: 3.1.0` — never 3.0.x, never 2.0 (per `api-openapi-first.md`).
|
||||
- `info` with `title`, `version: 0.1.0`, `description` (first 2 lines of
|
||||
`INTAKE`).
|
||||
- `servers` list with at least `production` + `staging` placeholder URLs.
|
||||
- `components.schemas.Problem` ($ref'd by every 4xx/5xx — RFC 9457 shape).
|
||||
- `components.securitySchemes` placeholder (`bearerAuth` by default;
|
||||
`oauth2` if Phase 5 chooses OAuth; filled in Phase 5).
|
||||
- `components.parameters` for `cursor`, `limit`, `page`
|
||||
(depending on `PAGINATION` — filled in Phase 5).
|
||||
- One `paths` entry PER entity × action cell from Phase 2c:
|
||||
`GET /foos`, `POST /foos`, `GET /foos/{id}`, `PATCH /foos/{id}`,
|
||||
`DELETE /foos/{id}`. Each `$ref`s to `components/schemas/Foo` (stub
|
||||
with `id`, `created_at`, `updated_at`, plus placeholder fields).
|
||||
- ETag + idempotency hints as comments in the skeleton where relevant
|
||||
(`api-rest-conventions.md`).
|
||||
|
||||
- **GraphQL SDL skeleton** must include:
|
||||
- `scalar DateTime` + `scalar UUID` declared once.
|
||||
- `interface Node { id: ID! }` (Relay convention).
|
||||
- One `type Foo implements Node { id: ID! createdAt: DateTime! ... }` per
|
||||
entity.
|
||||
- `type FooConnection { edges: [FooEdge!]! pageInfo: PageInfo! totalCount: Int }`
|
||||
+ `type FooEdge { node: Foo! cursor: String! }` for every listable
|
||||
entity (Relay spec).
|
||||
- Root `type Query { foo(id: ID!): Foo foos(first: Int, after: String): FooConnection! }`
|
||||
per entity per action cell.
|
||||
- Root `type Mutation { createFoo(input: CreateFooInput!): Foo! ... }`
|
||||
with `input` types per action cell.
|
||||
- Root `type Subscription` stub — empty if Phase 5 says no realtime.
|
||||
- `enum ErrorCode { NOT_FOUND FORBIDDEN BAD_USER_INPUT RATE_LIMITED INTERNAL_SERVER_ERROR }`
|
||||
— referenced by resolver error extensions.
|
||||
|
||||
## 3c — Emit / write the skeleton
|
||||
|
||||
If `CONTRACT_PATH` != "Skip":
|
||||
- Compute absolute path in the current repo.
|
||||
- If file exists → STOP, re-ask: "File exists at <path>. Overwrite, merge,
|
||||
or pick a new name?" (three-option AskUserQuestion, fail-closed default
|
||||
= "pick a new name").
|
||||
- Write the skeleton. Record `CONTRACT = <absolute path>` and
|
||||
`CONTRACT_LINES = <LOC>` for the final report.
|
||||
|
||||
If `CONTRACT_PATH` == "Skip":
|
||||
- Emit the full skeleton as a fenced code block in chat.
|
||||
- Record `CONTRACT = <inline>` and `CONTRACT_LINES = <LOC>`.
|
||||
|
||||
## 3d — Lint + drift-gate hint (inline)
|
||||
|
||||
Remind the user ONCE:
|
||||
|
||||
> Add these to CI next:
|
||||
> 1. `spectral lint <contract>` — OpenAPI/Spectral ruleset OR
|
||||
> `graphql-schema-linter schema.graphql` — style + breaking-change catch.
|
||||
> 2. `oasdiff breaking` (REST) OR `graphql-inspector diff` (GraphQL) on
|
||||
> every PR — blocks breaking changes unless `breaking: approved`
|
||||
> label is set.
|
||||
> 3. Contract tests (Schemathesis / Dredd / Pact) run against the deployed
|
||||
> server in staging. Drift = test fail.
|
||||
|
||||
No AskUserQuestion here — this is guidance.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `CONTRACT_PATH` picked; file written or skeleton printed inline.
|
||||
- Every entity from `RESOURCES` surfaces in the skeleton as a schema/type.
|
||||
- Every action cell from Phase 2c has a matching path/operation OR a
|
||||
matching query/mutation field.
|
||||
- No field values invented — placeholder-only schemas, marked with a
|
||||
`# TODO: define fields` comment; RULE 0.4 no fabricated sample data.
|
||||
111
skills/api-design/phase-4-versioning.md
Normal file
111
skills/api-design/phase-4-versioning.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Phase 4 — Versioning strategy
|
||||
|
||||
Decide how the API evolves when backwards-incompatible changes happen.
|
||||
Reference: `_blocks/api-versioning-pagination-ratelimit.md`. Fail-closed
|
||||
bias — if the user is unsure, the skill defaults to URL-path versioning
|
||||
(most visible, hardest to break silently).
|
||||
|
||||
## 4a — Strategy click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Versioning strategy?",
|
||||
"header": "Versioning",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "URL path (/v1, /v2)",
|
||||
"description": "Most visible, curl-friendly, easy CDN routing. Coarse version bumps. GitHub v3/v4, public REST default."},
|
||||
{"label": "Header (media type, Accept: application/vnd.example.v2+json)",
|
||||
"description": "Clean URLs. Internal or typed-SDK-only APIs. Requires disciplined clients."},
|
||||
{"label": "Date-based (Stripe-Version: 2025-11-01)",
|
||||
"description": "Fine-grained; every breaking change pinnable. Keep N-1 versions live. Use for pay-for-stability APIs."},
|
||||
{"label": "Additive-only (no versioning, promise to never break)",
|
||||
"description": "Simplest. ONLY with small disciplined teams + strong typing + <3 consumers. Risk: one accidental break kills trust."},
|
||||
{"label": "GraphQL evolution (no version, @deprecated + telemetry)",
|
||||
"description": "Schema grows forever; remove fields after telemetry shows 0 usage. Required for GraphQL-only APIs."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `VERSIONING`. Gate on `STYLE` from Phase 1:
|
||||
|
||||
- `STYLE = GraphQL` → only `GraphQL evolution` is correct. If user picks
|
||||
anything else, STOP and re-ask with a one-line explanation.
|
||||
- `STYLE = REST` → any of URL / Header / Date / Additive.
|
||||
- `STYLE = gRPC` → versioning usually package-based (`example.v1`,
|
||||
`example.v2`); record as `url-path`-equivalent and note in final report.
|
||||
- `STYLE = tRPC` → additive-only is typical; record as `additive-only`.
|
||||
|
||||
## 4b — Deprecation runway click (AskUserQuestion, single-select)
|
||||
|
||||
Only if `VERSIONING != additive-only` AND `VERSIONING != GraphQL evolution`.
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Minimum deprecation runway for breaking changes?",
|
||||
"header": "Deprecation runway",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "6 months (RECOMMENDED for public APIs)",
|
||||
"description": "Industry standard (Stripe, GitHub). Deprecation + Sunset headers + changelog + migration guide."},
|
||||
{"label": "12 months",
|
||||
"description": "Regulated / enterprise partners. SLA-backed."},
|
||||
{"label": "3 months",
|
||||
"description": "Acceptable for partner or internal APIs where consumers are known and reachable."},
|
||||
{"label": "Same-day (internal only)",
|
||||
"description": "Inside the mesh; all callers are your own services. Still emit Sunset header for audit."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `DEPRECATION_MONTHS`. Block "Same-day" if `AUDIENCE = public`
|
||||
(fail-closed NO DOWNGRADE — re-offer 3 / 6 / 12 with a one-line warning).
|
||||
|
||||
## 4c — Emit deprecation headers snippet (inline, no AskUserQuestion)
|
||||
|
||||
Print the standards-track header contract — RFC 8594 (Sunset) + RFC 9745
|
||||
(Deprecation, 2024):
|
||||
|
||||
```http
|
||||
Deprecation: @1735689600 # Unix timestamp of deprecation
|
||||
Sunset: Wed, 11 Nov 2026 00:00:00 GMT
|
||||
Link: <https://api.example.com/migration-v2-to-v3>; rel="deprecation"
|
||||
```
|
||||
|
||||
- `Deprecation` — when the endpoint became deprecated (past or future).
|
||||
- `Sunset` — when it will be removed. `Sunset - Deprecation ≥ DEPRECATION_MONTHS`.
|
||||
- `Link rel="deprecation"` — URL of the migration guide.
|
||||
|
||||
For `VERSIONING = GraphQL evolution`: replace with SDL directive
|
||||
`@deprecated(reason: "Use field X — removed after 2026-11-01")` and the
|
||||
removal rule ("remove only after telemetry shows 0 usage for ≥30 days").
|
||||
|
||||
## 4d — Emit changelog + telemetry obligations (inline)
|
||||
|
||||
For any non-trivial versioning choice, print:
|
||||
|
||||
- **Changelog location:** `docs/api/CHANGELOG.md` or `/changelog` endpoint
|
||||
on the API itself. Entries: date, version, breaking/non-breaking,
|
||||
migration link.
|
||||
- **Telemetry obligations:** log the version used on every request
|
||||
(`api_version` field); alert when a deprecated version's usage does not
|
||||
trend down. Without telemetry, "deprecation" is a lie.
|
||||
- **Versioning + pagination cross-cut:** cursor tokens MUST be opaque and
|
||||
treated as versioned data (base64 of signed JSON); don't let a v1 cursor
|
||||
accidentally work in v2 with different fields.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `VERSIONING` exactly one choice, compatible with `STYLE`.
|
||||
- `DEPRECATION_MONTHS` set (or N/A for additive-only / GraphQL evolution).
|
||||
- Deprecation header snippet (4c) printed.
|
||||
- Changelog + telemetry obligations (4d) printed as a checklist.
|
||||
- If `AUDIENCE = public` and `DEPRECATION_MONTHS < 3` → STOP and re-ask.
|
||||
138
skills/api-design/phase-5-limits-auth.md
Normal file
138
skills/api-design/phase-5-limits-auth.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Phase 5 — Pagination + rate limits + auth handoff
|
||||
|
||||
Lock the three cross-cutting concerns that bite every production API in
|
||||
its first month. Reference: `_blocks/api-versioning-pagination-ratelimit.md`.
|
||||
Delegates auth wiring to `skills/auth-setup/SKILL.md`.
|
||||
|
||||
## 5a — Combined click (AskUserQuestion, multi-select, pre-checked)
|
||||
|
||||
Single AskUserQuestion with three axes fused to stay within the
|
||||
≥6-AskUserQuestion budget. Pre-select the fail-closed defaults; opting out
|
||||
requires a click.
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Confirm the pagination + rate-limit + auth policy (pre-checked fail-closed defaults; deselect only with a compensating control).",
|
||||
"header": "Limits+Auth",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "Pagination: cursor (opaque, keyset)",
|
||||
"description": "REQUIRED for any list that can exceed ~1k rows. Response envelope {data, meta:{next_cursor, has_more}}."},
|
||||
{"label": "Pagination: offer offset/page too (admin UIs only)",
|
||||
"description": "Accept for admin screens where page numbers are expected; clamp limit ≤100 server-side."},
|
||||
{"label": "Pagination: Relay Connections (GraphQL only)",
|
||||
"description": "Required if STYLE=GraphQL. edges/pageInfo/endCursor per Relay spec."},
|
||||
{"label": "Rate limit: per-principal token bucket",
|
||||
"description": "Redis-backed. Default tiers: anon < authenticated < partner < internal."},
|
||||
{"label": "Rate limit: per-endpoint cost budget",
|
||||
"description": "Expensive routes (search, export) get their own budget. GraphQL uses cost-based analyser instead."},
|
||||
{"label": "Rate limit: per-IP sliding window (anti-bot)",
|
||||
"description": "Defence-in-depth layer. Still applies under auth failures / unauthenticated endpoints."},
|
||||
{"label": "Rate-limit headers: IETF RateLimit-* + Retry-After",
|
||||
"description": "RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset (IETF draft, 2024 deployed). Plus Retry-After on 429."},
|
||||
{"label": "Auth: delegate to /auth-setup (RECOMMENDED)",
|
||||
"description": "Runs the hub-and-spoke auth pipeline after this skill finishes — OAuth / passkey / sessions / RBAC."},
|
||||
{"label": "Auth: API-key only (server-to-server)",
|
||||
"description": "Partner / internal S2S. Long-lived keys stored per client; rotation policy required."},
|
||||
{"label": "Auth: mTLS (internal service mesh)",
|
||||
"description": "Pick for internal boundaries in a mesh (Istio / Linkerd). Record the CA; no token on the wire."},
|
||||
{"label": "Auth: none (open public API)",
|
||||
"description": "Rate limits + anti-bot must compensate. Acceptable only for truly-public read-only data."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Parse the selection into three variables:
|
||||
|
||||
- `PAGINATION` ← the pagination option(s) picked (must be ≥1).
|
||||
- `RATELIMIT` ← the list of rate-limit layers selected.
|
||||
- `AUTH_HANDOFF` ← one of `run-auth-setup`, `api-key`, `mtls`, `none`.
|
||||
|
||||
Validation gates (NO DOWNGRADE: offer alternatives instead of rejecting):
|
||||
|
||||
- `STYLE = GraphQL` AND `PAGINATION` does not include "Relay" → STOP,
|
||||
re-ask — Relay is the standard for GraphQL list pagination.
|
||||
- `AUDIENCE = public` AND `AUTH_HANDOFF = none` AND no per-IP rate limit →
|
||||
STOP, re-ask with the warning "public + no auth + no per-IP = abuse
|
||||
vector".
|
||||
- `RATELIMIT` empty AND `AUDIENCE != internal` → STOP, re-ask with the
|
||||
warning "rate limits mandatory for non-internal APIs".
|
||||
|
||||
## 5b — Emit pagination contract (inline)
|
||||
|
||||
For `PAGINATION = cursor`:
|
||||
|
||||
```yaml
|
||||
# OpenAPI skeleton — added to components.parameters
|
||||
Cursor:
|
||||
name: cursor
|
||||
in: query
|
||||
schema: { type: string }
|
||||
description: Opaque cursor returned by the previous response.
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
|
||||
```
|
||||
|
||||
For `PAGINATION = Relay Connections`:
|
||||
|
||||
```graphql
|
||||
# GraphQL skeleton — already emitted in Phase 3
|
||||
type FooConnection { edges: [FooEdge!]! pageInfo: PageInfo! totalCount: Int }
|
||||
type FooEdge { node: Foo! cursor: String! }
|
||||
type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
|
||||
```
|
||||
|
||||
## 5c — Emit rate-limit policy table (inline)
|
||||
|
||||
Print a table the user fills in numbers for. Example tiers:
|
||||
|
||||
```markdown
|
||||
| Tier | Requests/min | Burst | Notes |
|
||||
|---------------|:------------:|:-----:|------------------------------------|
|
||||
| anonymous | 30 | 60 | per-IP sliding window |
|
||||
| authenticated | 600 | 1000 | per-principal token bucket |
|
||||
| partner | 6000 |10000 | per-API-key; negotiable in contract|
|
||||
| internal | none | - | inside VPC, trust boundary |
|
||||
```
|
||||
|
||||
Plus the header contract:
|
||||
|
||||
```http
|
||||
RateLimit-Limit: 600
|
||||
RateLimit-Remaining: 547
|
||||
RateLimit-Reset: 42
|
||||
Retry-After: 30 # only on 429 responses
|
||||
```
|
||||
|
||||
## 5d — Emit auth handoff (inline)
|
||||
|
||||
- If `AUTH_HANDOFF = run-auth-setup`:
|
||||
print "Next step: run `/auth-setup` with argument `<first 80 chars of INTAKE>`".
|
||||
Record this in the final report as the recommended next command.
|
||||
- If `AUTH_HANDOFF = api-key`:
|
||||
emit env-var scaffold into `secrets/api.env` (names only, per RULE 0.8):
|
||||
```bash
|
||||
# secrets/api.env — chmod 600, gitignored
|
||||
API_KEY_ISSUER_SECRET=
|
||||
API_KEY_ROTATION_DAYS=90
|
||||
```
|
||||
- If `AUTH_HANDOFF = mtls`:
|
||||
emit reminder — "Record CA fingerprint in `docs/mtls-trust.md`; client
|
||||
cert rotation cadence in runbook."
|
||||
- If `AUTH_HANDOFF = none`:
|
||||
record explicitly in the final report as an accepted risk.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `PAGINATION`, `RATELIMIT`, `AUTH_HANDOFF` all set.
|
||||
- At least one rate-limit layer selected unless `AUDIENCE = internal`.
|
||||
- No literal token value appears in the emitted text (RULE 0.8).
|
||||
- If `AUTH_HANDOFF = run-auth-setup`, the next-command line is printed.
|
||||
- No fabricated rate-limit numbers — table cells are placeholders clearly
|
||||
marked as defaults ("fill during capacity planning").
|
||||
165
skills/api-design/phase-6-codegen.md
Normal file
165
skills/api-design/phase-6-codegen.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Phase 6 — Codegen toolchain
|
||||
|
||||
Pick the generator(s) that turn the Phase 3 contract into server stubs,
|
||||
typed clients, and docs. This is the last click; after it, the skill emits
|
||||
the final report. Reference: `_blocks/api-openapi-first.md`,
|
||||
`_blocks/api-graphql.md`.
|
||||
|
||||
## 6a — Codegen click (AskUserQuestion, multi-select)
|
||||
|
||||
Options gate on `STYLE` from Phase 1 and `CLIENTS` from Phase 1d.
|
||||
|
||||
### If `STYLE = REST` (OpenAPI spec)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Pick the REST codegen targets (spec → server + clients + docs)?",
|
||||
"header": "REST Codegen",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "openapi-generator (multi-language server + clients)",
|
||||
"description": "Generates TS/Swift/Kotlin/Python/Rust/Go/Java stubs. Prefer axios (TS), okhttp (Kotlin), urlsession (Swift). Version 7.x as of 2026."},
|
||||
{"label": "orval (TS clients with React Query / SWR / Zod)",
|
||||
"description": "Best-in-class TS client DX. Integrates with msw mock server."},
|
||||
{"label": "oapi-codegen (Go, type-safe chi/echo/gin handlers)",
|
||||
"description": "Reference Go generator. Server interface + client + type-safe handlers."},
|
||||
{"label": "progenitor (Rust, async clients)",
|
||||
"description": "Oxide Computer's generator. reqwest-based, serde + typed errors."},
|
||||
{"label": "Swagger UI (interactive docs)",
|
||||
"description": "Classic, Try-it-out buttons. Good for partner onboarding."},
|
||||
{"label": "Redoc (read-only, pretty docs)",
|
||||
"description": "Stripe-style three-pane docs. Markdown-friendly."},
|
||||
{"label": "Scalar (modern docs, built-in request builder)",
|
||||
"description": "2024+ popular, React-based, fast. Good for public APIs."},
|
||||
{"label": "Stoplight Elements (embeddable React docs)",
|
||||
"description": "Drop into an existing marketing site as a component."},
|
||||
{"label": "Prism (mock server from spec)",
|
||||
"description": "stoplight/prism — mock server for frontend devs before backend exists."},
|
||||
{"label": "Schemathesis (contract tests)",
|
||||
"description": "Property-based tests that hit the real server and verify every operation against the spec."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### If `STYLE = GraphQL`
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Pick the GraphQL codegen targets (schema → server + clients + docs)?",
|
||||
"header": "GraphQL Codegen",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "graphql-codegen (TS clients + resolver types)",
|
||||
"description": "The Guild's codegen. Plugins for typescript-operations, typescript-react-apollo, typed-document-node."},
|
||||
{"label": "async-graphql (Rust, schema-first via derive)",
|
||||
"description": "Production Rust server. Derive macros implement schema; DataLoader built-in."},
|
||||
{"label": "gqlgen (Go, schema-first)",
|
||||
"description": "The Go standard. Resolver stubs from schema."},
|
||||
{"label": "Strawberry (Python, code-first with type hints)",
|
||||
"description": "Python async-friendly; emits SDL. NOT schema-first but acceptable if STYLE = GraphQL and team prefers Python types."},
|
||||
{"label": "Apollo Server 4 / GraphQL Yoga (TS server)",
|
||||
"description": "Runtime, not codegen, but paired with graphql-codegen for the types."},
|
||||
{"label": "Relay Compiler (persisted queries + typegen for React)",
|
||||
"description": "Required if FB Relay is the client. Produces persisted query IDs for production allow-list."},
|
||||
{"label": "GraphiQL / Apollo Sandbox (interactive docs)",
|
||||
"description": "Built into every GraphQL server; dev-only by default."},
|
||||
{"label": "graphql-inspector (schema diff + breaking-change gate)",
|
||||
"description": "CI gate. Fail PR on breaking changes unless label applied."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### If `STYLE = tRPC` / `gRPC`
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Pick the codegen targets?",
|
||||
"header": "Codegen (tRPC/gRPC)",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "tRPC: infer from server routers (no codegen)",
|
||||
"description": "TS end-to-end; client imports `AppRouter` type. Only works for TS clients."},
|
||||
{"label": "gRPC: buf + protoc-gen-go / protoc-gen-ts / protoc-gen-swift",
|
||||
"description": "Buf CLI for lint + breaking-change detection. Per-language protoc plugins for clients."},
|
||||
{"label": "gRPC: connectrpc (grpc-web + browser support)",
|
||||
"description": "Buf's connect — browser-compatible, simpler than grpc-web."},
|
||||
{"label": "Buf Schema Registry (docs + breaking-change gate)",
|
||||
"description": "Managed registry or self-host; PR comment on breaking schema change."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store the selection as `CODEGEN` (a list of labels). Validate:
|
||||
|
||||
- At least one server generator OR "tRPC infer from server" is picked.
|
||||
- At least one docs target is picked (unless `AUDIENCE = internal`).
|
||||
- At least one contract-test / drift-gate is picked (`Schemathesis`,
|
||||
`graphql-inspector`, or `Buf` — fail-closed NO DOWNGRADE: re-ask if
|
||||
none selected).
|
||||
|
||||
## 6b — Emit CI snippet (inline)
|
||||
|
||||
Print a minimal CI snippet (GitHub Actions shape; user adapts to their
|
||||
runner). Example for REST + openapi-generator + Spectral + oasdiff:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/api-contract.yml (SKELETON — adjust to your runner)
|
||||
name: api-contract
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npx -y @stoplight/spectral-cli lint api/openapi.yaml
|
||||
diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
- run: npx -y oasdiff breaking origin/main:api/openapi.yaml api/openapi.yaml
|
||||
codegen-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npx -y @openapitools/openapi-generator-cli validate -i api/openapi.yaml
|
||||
```
|
||||
|
||||
For GraphQL, emit the equivalent with `graphql-schema-linter` +
|
||||
`graphql-inspector diff` + `graphql-codegen --check`.
|
||||
|
||||
## 6c — Final report assembly
|
||||
|
||||
Emit the final report template from `SKILL.md` with all variables filled.
|
||||
Add at the bottom:
|
||||
|
||||
```
|
||||
Deselected / risk-accepted:
|
||||
- <item>: <one-line justification>
|
||||
(or "none" if the pipeline ran fail-closed)
|
||||
|
||||
Next recommended command:
|
||||
- /auth-setup "<first 80 chars of INTAKE>" (if AUTH_HANDOFF = run-auth-setup)
|
||||
- /compose-solution (to assemble blocks into a project plan)
|
||||
```
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `CODEGEN` has ≥1 entry and passes the two validation rules.
|
||||
- CI snippet (6b) printed.
|
||||
- Final report (6c) emits the full `=== API-DESIGN REPORT ===` block with
|
||||
every variable from `SKILL.md` filled.
|
||||
- No library version number invented — references are to current major
|
||||
versions per the upstream blocks (RULE 0.4).
|
||||
Loading…
Reference in a new issue