Files
suaveplan/openspec/archive/2026-05-25-api-router/proposal.md
2026-05-27 23:29:53 +01:00

5.8 KiB

@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)