feat(skills): /api-design 6-phase pipeline

This commit is contained in:
Parfii-bot 2026-04-21 20:54:54 +08:00
parent e3c20b2b01
commit 2262cc3f69
7 changed files with 847 additions and 0 deletions

114
skills/api-design/SKILL.md Normal file
View 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 26). 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 (10010k) / 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 23 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.

View 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 (10010k 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).

View 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.

View 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.

View 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.

View 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").

View 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).