Files
2026-05-27 23:29:53 +01:00

75 lines
5.8 KiB
Markdown

# `@suaveplan/api-router` — abstract Router, Middleware, Request/Response, blue-green slot contract
**Package**: `@suaveplan/api-router`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Every concrete router adapter (`api-router-hono`, `api-router-in-memory`, the worker-bridge router, future Fastify / Bun-native flavours) needs the same set of contracts: `Router`, `Middleware`, `Request`, `Response`, `RouteOptions`, plus the blue-green slot management interface that lets the host atomically flip from `live=N` to `live=N+1` during hot reload (design §6, §8.1, §10.7). Putting these contracts in `api-router` keeps the abstract surface independent of any one HTTP library so the host (`api-host`) and downstream adapters can be developed in parallel — and so plugin authors can author against a stable router type they import once.
Per design §8.1: "The router's per-plugin version pointer is the single atomic point that determines which module gets this request." That pointer's contract — `BlueGreenSlot` — lives here.
## What changes
Net-new package. Mostly types but ships some runtime: route-options Zod schemas, the abstract composition helpers (prefix-mounting, middleware-chain builder), and the blue-green slot management *interfaces* (concrete implementation lives in `api-host`).
- ADDED: `./router` — abstract `Router` interface (verb methods: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `all`; plus `use(middleware)` and `mount(prefix, sub)`).
- ADDED: `./request` — host-side `Request` shape: `method`, `url`, `headers`, `body` (already validated by step 6), `params`, `query`, `tenantId`, `principal`, `requestId`, `span`.
- ADDED: `./response` — host-side `Response` builder: `status`, `headers`, `body`, `etag`, `serverTiming`, `streaming` mode toggle.
- ADDED: `./route-options` — `RouteOptions` Zod schema covering: `auth` (scope id), `summary`, `tags`, `request` (Zod body/params/query), `responses` (per-status Zod), `runtimeValidation` (default true), `openapi` (default true), `streaming`, `csrf`, `idempotent`, `rateLimit`.
- ADDED: `./middleware` — `Middleware` contract: `(req, next) => Response | Promise<Response>`; `composeMiddleware([...])` builder; insertion-point predicate (plugin middleware allowed only between pipeline steps 7-8, never before).
- ADDED: `./route-match` — abstract `RouteMatch` shape returned from `Router.match(method, url)`: handler reference + params + plugin-of-record.
- ADDED: `./slot` — `RouterSlot` (per-plugin mount point) + `BlueGreenSlot` interface (`live`, `shadow`, `draining` slots; atomic `flip(toShadow)`; `dispose(slot)`). Concrete impl lives in `api-host`.
- ADDED: `./error-codes` — frozen `ROUTER_CODES` registry: `ROUTE_COLLISION`, `INVALID_PREFIX`, `MIDDLEWARE_INSERTION_DENIED`, `SLOT_FLIP_FAILED`, `SLOT_DISPOSE_TIMEOUT`.
- ADDED: `./testing` — `runRouterConformance(factory)` reusable harness every concrete adapter must pass.
## Out of scope
- Any concrete adapter — `api-router-hono`, `api-router-in-memory`, and the worker bridge each land in their own changes.
- HTTP intake / wire parsing — that's the adapter's job. This package's `Request` is the post-parse shape.
- OpenAPI generation — `api-openapi` reads `RouteOptions` schemas; this package only exports the schema shape.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Per-request pipeline (steps 1-11) | §6 |
| Plugin middleware allowed only between steps 7-8 | §6 ("That surface was a CVE waiting to happen.") |
| `RouteOptions` keys (`auth`, `runtimeValidation`, `openapi`, `streaming`, `csrf`, `idempotent`) | §6 (CSRF, idempotency, streaming), §7 (OpenAPI route example) |
| `BlueGreenSlot` semantics (live / shadow / draining) | §8.1, §8.5 |
| Coexistence with GraphQL | §10.7 |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `composeMiddleware` builder | Standard pattern; design doc references middleware composition without specifying the constructor — this package owns the canonical builder. |
| `runRouterConformance` harness | Conformance suites are explicit Stage-5 deliverable (§13.4); contract package owns the harness signature. |
## Verification approach
- `src/router/router.test.ts` — verb methods present, `mount(prefix, sub)` rewrites paths, `use(middleware)` appends to chain.
- `src/route-options/route-options.test.ts` — Zod accept/reject for every option; defaults verified (`runtimeValidation: true`, `openapi: true`).
- `src/middleware/compose-middleware.test.ts` — chain ordering, short-circuit, `next()` skipping, error propagation.
- `src/middleware/insertion-point.test.ts` — plugin-supplied middleware accepted only at the steps-7-8 boundary; rejection emits `MIDDLEWARE_INSERTION_DENIED`.
- `src/slot/slot.test.ts` — `BlueGreenSlot` state machine: live → shadow set → flip → draining → disposed. Type-level test that `flip` is atomic (returns the old live slot's draining-handle).
- `src/route-options/route-options.spec-conformance.test.ts` — `expectTypeOf<RouteOptions>()` matches `@suaveplan/api-types`'s router contract.
- `src/testing/conformance.test.ts` — runs the harness against a stub `Router` to verify the harness itself is correct.
- `tests/dep-graph.test.ts` — pins deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod` only.
## Migration impact
None — additive only. No existing consumer.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/api-router` to enumerate. No dependents at Stage 1.
- (none) — [x] no action needed (reason: net-new package; first consumer is `api-host` in Stage 2)