Merge branch 'feat/v0.7-api-design' — 4 blocks + /api-design

This commit is contained in:
Parfii-bot 2026-04-21 21:11:17 +08:00
commit 6d382ee939
11 changed files with 1000 additions and 0 deletions

33
_blocks/api-graphql.md Normal file
View 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.

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

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

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