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— abstractRouterinterface (verb methods:get,post,put,patch,delete,head,options,all; plususe(middleware)andmount(prefix, sub)). - ADDED:
./request— host-sideRequestshape:method,url,headers,body(already validated by step 6),params,query,tenantId,principal,requestId,span. - ADDED:
./response— host-sideResponsebuilder:status,headers,body,etag,serverTiming,streamingmode toggle. - ADDED:
./route-options—RouteOptionsZod 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—Middlewarecontract:(req, next) => Response | Promise<Response>;composeMiddleware([...])builder; insertion-point predicate (plugin middleware allowed only between pipeline steps 7-8, never before). - ADDED:
./route-match— abstractRouteMatchshape returned fromRouter.match(method, url): handler reference + params + plugin-of-record. - ADDED:
./slot—RouterSlot(per-plugin mount point) +BlueGreenSlotinterface (live,shadow,drainingslots; atomicflip(toShadow);dispose(slot)). Concrete impl lives inapi-host. - ADDED:
./error-codes— frozenROUTER_CODESregistry: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
Requestis the post-parse shape. - OpenAPI generation —
api-openapireadsRouteOptionsschemas; 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 emitsMIDDLEWARE_INSERTION_DENIED.src/slot/slot.test.ts—BlueGreenSlotstate machine: live → shadow set → flip → draining → disposed. Type-level test thatflipis 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 stubRouterto verify the harness itself is correct.tests/dep-graph.test.ts— pins deps:@suaveplan/api-types,@suaveplan/error,@suaveplan/types,zodonly.
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-hostin Stage 2)