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

8.8 KiB

Add @suaveplan/api — Hono orchestrator + middleware framework (5 of 5)

Package: @suaveplan/api (NEW, server-tier) Status: PROPOSED Type: new-package (split from upstream @suaveplan/api — this is the thin orchestrator slice)

  • Spec delta: ./specs/api/spec.md
  • Companions: add-api-plugin, add-api-tenant, add-api-openapi, add-api-migrations
  • Position in chain: depends on all 4 companions (composes them into the runtime)

Why

This is the final piece of the @suaveplan/api split. Upstream's single 8.7k-LOC package combined plugin + tenant + openapi + migrations + middleware + config + app orchestration into one. The 4 sibling changes peel off the first four concerns. What remains is the thin app orchestrator (SuaveApp/AppBuilder, ~739 LOC upstream), the Hono middleware framework (909 LOC upstream — request-id / cors / logger / error-handler + chain manager), the app config layer (595 LOC upstream — Zod-typed multi-source loader), and the events bus integration (the @suaveplan/events-typed event surface the plugin lifecycle emits to).

The genesis enterprise audit additionally fixes the upstream bugs found while reading: AppBuilder.tenant(resolver) plumbed but silently dropped (real bug); 3 AsyncLocalStorage instances (app + tenant + implicit globals); 3 global singletons (globalApp/globalEventBus/globalSchemaRegistry); error-handler statusCode closed-enum cast; hardcoded health-route paths; CORS exposing rate-limit headers without a rate-limiter wired; insertBefore/insertAfter silently fall back to push.

What changes

Net-new package at 0.1.0. ADDED:

  • SuaveApp class — thin orchestrator that composes rather than imports siblings directly. Constructor accepts pre-built PluginRegistry, OpenAPIRegistry, MiddlewareChain, TenantResolver?, MigrationRunner? (all optional except registry). Removes upstream's deep coupling.
  • AppState literal union (created/initializing/ready/running/stopping/stopped) — the FSM. state getter exposes it; transitions are private.
  • AppBuilder fluent API — chainable .name/.version/.env/.debug/.port/.host/.basePath/.database/.cors/.openapi/.use/.useAll/.middleware/.tenant/.skipConfigFile/.config/.migrations → .build() / .initialize() / .start(). Genesis fix: .tenant(resolver) is actually wired into the middleware chain (upstream silently drops it).
  • quickstart({name, port?, plugins?, debug?}) — one-call helper for trivial apps.
  • AppConfigSchema (Zod) — server / database / cors / logging / openapi / plugins / migrations sub-configs. Multi-source loader: file (json/ts/js — yaml/toml dropped from upstream's mis-advertised union) → env-specific file → env vars (SUAVEPLAN_SERVER_PORT → server.port).
  • ConfigLoader class + loadConfig() convenience + defineConfig() type helper + defineEnvConfig() for environment-keyed overrides.
  • validateConfig(input): AppConfig and tryValidateConfig(input): ValidationResult<AppConfig> (both Zod-backed).
  • MiddlewareChain class — name-addressable Hono middleware list with MiddlewareOrder slot constants (FIRST=-1000 → LAST=1000, with SECURITY=-500, CORS=-400, REQUEST_ID=-300, LOGGING=-200, TIMING=-100, AUTH=0, TENANT=100, VALIDATION=200, BUSINESS=500, RESPONSE=800, ERROR=900). Genesis fix: insertBefore(name, m) and insertAfter(name, m) SHALL raise ApiError(API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND) when the named middleware doesn't exist (upstream silently falls back to push).
  • 5 shipped middleware factories: createRequestId, createCors, createLogger, createErrorHandler, createNotFoundHandler. Plus helpers combineMiddleware, conditionalMiddleware, pathMiddleware, excludePathMiddleware.
  • Error handler widens status code: createErrorHandler SHALL pass the SuaveplanError's full statusCode through to Hono's response (no closed-enum cast — upstream caps at 400/401/403/404/500).
  • CORS conditional headers: createCors SHALL only expose X-RateLimit-* headers when the caller passes withRateLimitHeaders: true. Default omits them (upstream exposes unconditionally).
  • Configurable health routes: SuaveApp.setupHealthRoutes(options?: { healthPath?, readyPath?, livePath?, enabled? }). Defaults match upstream but can be disabled or renamed.
  • Event-bus integration: SuaveApp emits app:starting, app:started, app:stopping, app:stopped, app:error to the registry's typed EventBus (from @suaveplan/events).
  • No global singletons. Upstream globalApp/getGlobalApp/setGlobalApp/clearGlobalApp SHALL NOT exist on this package. Same for globalEventBus — every consumer constructs its own.
  • AsyncLocalStorage consolidated. ONE optional RequestContext ALS instance ({ app: SuaveApp; requestId: string; tenant?: Tenant; user?: User }) replaces upstream's 3 separate ALS instances. Opt-in via app.config.useAsyncLocalStorage: true. Default is off; consumers thread c from Hono.
  • ApiError extends SuaveplanError<ApiErrorCode> with codes API_INVALID_CONFIG, API_INVALID_STATE, API_MIDDLEWARE_INSERT_TARGET_NOT_FOUND, API_SHUTDOWN_TIMEOUT, API_SERVER_START_FAILED.
  • Subpath exports per feature module.

Out of scope

  • All 4 sibling packages. Plugin system (add-api-plugin), tenant (add-api-tenant), OpenAPI (add-api-openapi), and migrations (add-api-migrations) are separate dependencies — not bundled.
  • Database adapter (the upstream DatabaseAdapter/ConnectionManager/QueryBuilder). Owned by @suaveplan/db + dialect-specific adapters. The orchestrator wires a caller-supplied adapter into the plugin context via registerService("db", ...).
  • Auth middleware. The AUTH=0 order slot is reserved for @suaveplan/api-plugin-auth and friends — not shipped here.
  • Validation middleware. Plugin-level Zod validation is the @hono/zod-openapi integration via api-openapi; cross-cutting input validation belongs in middleware contributed by domain plugins.
  • Rate limit middleware. Tenant rate limit lives in api-tenant; global rate limit will be a separate api-plugin-rate-limit package.

Source mapping

Capability Upstream source Notes
SuaveApp class + FSM + start/stop/initialize ../suaveplan/packages/server/api/src/app/app.ts L66-L367 port shape; drop globalApp; consolidate ALS; fix tenant-resolver wiring; widen health-route configurability
AppBuilder fluent API ../suaveplan/packages/server/api/src/app/app-builder.ts L14-L217 port + actually wire .tenant(resolver) into the middleware chain
quickstart helper ../suaveplan/packages/server/api/src/app/app-builder.ts L229-L260 verbatim port
AppContext ALS ../suaveplan/packages/server/api/src/app/app-context.ts L1-L106 consolidate into single optional RequestContext ALS; drop globalApp
AppConfigSchema + sub-configs ../suaveplan/packages/server/api/src/config/config.interface.ts L1-L163 port; add migrations sub-config; drop yaml/toml from ConfigFileFormat union
ConfigLoader + multi-source merge + env-var parsing ../suaveplan/packages/server/api/src/config/config-loader.ts L1-L236 port; replace upstream deepMerge with @suaveplan/utils's
validateConfig + tryValidateConfig + plugin config validator + typed getter ../suaveplan/packages/server/api/src/config/config-validator.ts L1-L191 port shape; switch error from upstream ConfigValidationError to defineCodes-typed ApiError(API_INVALID_CONFIG)
MiddlewareChain + MiddlewareOrder constants ../suaveplan/packages/server/api/src/middleware/middleware-chain.ts L1-L178 port + fix insertBefore/insertAfter silent push fallback to raise typed error
5 middleware factories ../suaveplan/packages/server/api/src/middleware/{cors,logger,error-handler,request-id}.ts port; widen error-handler statusCode; conditional X-RateLimit-* in CORS; ID generator alternatives kept
Middleware composition helpers ../suaveplan/packages/server/api/src/middleware/middleware.interface.ts L75-L148 verbatim port
Event-bus integration ../suaveplan/packages/server/api/src/events/event-bus.ts L1-L385 NOT migrated whole — only the EventBus integration surface lives here. The genesis package consumes @suaveplan/events's typed bus directly; the Hooks transform-pipeline alter-ego is dropped (use the plugin HookExecutor for behaviour contribution).

Impact

Affected dependents

None today — depends on all 4 sibling packages. Will be consumed by every @suaveplan/api-plugin-* package (40+ in openspec/changes/) plus application code.

Risk

  • Medium. The thinnest of the 5 packages by responsibility but the most architectural impact — its tests cover every other layer's integration with Hono. The integration test suite is where most of the genesis bug fixes are pinned.