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:
SuaveAppclass — thin orchestrator that composes rather than imports siblings directly. Constructor accepts pre-builtPluginRegistry,OpenAPIRegistry,MiddlewareChain,TenantResolver?,MigrationRunner?(all optional except registry). Removes upstream's deep coupling.AppStateliteral union (created/initializing/ready/running/stopping/stopped) — the FSM.stategetter exposes it; transitions are private.AppBuilderfluent 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).ConfigLoaderclass +loadConfig()convenience +defineConfig()type helper +defineEnvConfig()for environment-keyed overrides.validateConfig(input): AppConfigandtryValidateConfig(input): ValidationResult<AppConfig>(both Zod-backed).MiddlewareChainclass — name-addressable Hono middleware list withMiddlewareOrderslot constants (FIRST=-1000→LAST=1000, withSECURITY=-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)andinsertAfter(name, m)SHALL raiseApiError(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 helperscombineMiddleware,conditionalMiddleware,pathMiddleware,excludePathMiddleware. - Error handler widens status code:
createErrorHandlerSHALL pass the SuaveplanError's fullstatusCodethrough to Hono's response (no closed-enum cast — upstream caps at 400/401/403/404/500). - CORS conditional headers:
createCorsSHALL only exposeX-RateLimit-*headers when the caller passeswithRateLimitHeaders: 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:errorto the registry's typedEventBus(from@suaveplan/events). - No global singletons. Upstream
globalApp/getGlobalApp/setGlobalApp/clearGlobalAppSHALL NOT exist on this package. Same forglobalEventBus— every consumer constructs its own. - AsyncLocalStorage consolidated. ONE optional
RequestContextALS instance ({ app: SuaveApp; requestId: string; tenant?: Tenant; user?: User }) replaces upstream's 3 separate ALS instances. Opt-in viaapp.config.useAsyncLocalStorage: true. Default is off; consumers threadcfrom Hono. ApiError extends SuaveplanError<ApiErrorCode>with codesAPI_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 viaregisterService("db", ...). - Auth middleware. The
AUTH=0order slot is reserved for@suaveplan/api-plugin-authand friends — not shipped here. - Validation middleware. Plugin-level Zod validation is the
@hono/zod-openapiintegration viaapi-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 separateapi-plugin-rate-limitpackage.
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.