# Middleware composition — site + rate-limit + audit `@suaveplan/api-plugin-site` ships **zero runtime dependencies** on `@suaveplan/api-plugin-rate-limit` and `@suaveplan/api-plugin-audit`. This is deliberate. The composition knob lives in the host application: callers wire the middleware via Hono's `app.use(prefix, middleware)` pattern so they can swap or skip implementations as their deployment requires. This guide documents the canonical pattern, the recommended layering order, and a copy-pasteable end-to-end example. A runnable verification of every assertion lives in `src/integration/middleware-composition.integration.test.ts`. ## Layering order ``` ┌────────────────────────────────────────┐ inbound │ audit middleware (OUTERMOST) │ request ──▶ │ ──▶ rate-limit middleware │ │ ──▶ site routes (INNERMOST) │ └────────────────────────────────────────┘ ``` The order matters and is **not interchangeable**: 1. **Audit OUTERMOST.** The audit middleware records one event per inbound request — including requests that are rejected by rate-limit with `429`. Putting audit inside rate-limit would silently drop the audit record on every throttled call, defeating the purpose of an immutable trail. The audit middleware is also a no-throw boundary by contract, so it adds no failure modes. 2. **Rate-limit MIDDLE.** The rate-limit middleware is mounted on the site prefix (`/sites/*`) so it does not consume budget for unrelated routes mounted on the same host (health checks, OpenAPI documents, etc). Putting rate-limit inside the site routes would force every site route to opt in by hand and would miss requests that hit `404` route stubs. 3. **Site routes INNERMOST.** The site plugin's `createPrivateSiteRoutes` and `createPublicSiteRoutes` factories return `OpenAPIHono` instances that are mounted via `app.route(prefix, routes)`. They make zero assumptions about the surrounding middleware stack. ## Canonical example ```ts import { AuditLogger, MemoryAuditAdapter, } from "@suaveplan/audit-logging"; import { createAuditMiddleware } from "@suaveplan/api-plugin-audit"; import { createDefaultStorage, createRateLimitService, rateLimit, RateLimitPluginOptionsSchema, } from "@suaveplan/api-plugin-rate-limit"; import { createPrivateSiteRoutes, createPublicSiteRoutes, MIDDLEWARE_RECOMMENDATIONS, SiteService, } from "@suaveplan/api-plugin-site"; import { Hono } from "hono"; // ── 1. Site service + routes ──────────────────────────────────────── const siteService = new SiteService(db, mergeConfig()); const privateRoutes = createPrivateSiteRoutes({ siteService }); const publicRoutes = createPublicSiteRoutes({ domainResolver }); // ── 2. Rate-limit service driven by the recommendations bundle ───── const clock = () => Date.now(); const rateLimitConfig = RateLimitPluginOptionsSchema.parse({ defaultLimit: 100, defaultWindowMs: 60_000, routes: MIDDLEWARE_RECOMMENDATIONS.map((r) => ({ id: r.routeGroup, path: r.rateLimit.pathPattern, limit: r.rateLimit.limit, windowMs: r.rateLimit.windowMs, })), }); const rateLimitService = createRateLimitService({ config: rateLimitConfig, storage: createDefaultStorage({ clock }), clock, logger: hostLogger, }); // ── 3. Audit logger backed by your AuditStorageAdapter ───────────── const auditSink = new MemoryAuditAdapter(); const auditLogger = new AuditLogger({ storage: auditSink, secretKey: process.env.AUDIT_SECRET ?? "x".repeat(32), enableChainIntegrity: true, }); const auditMiddleware = createAuditMiddleware({ logger: auditLogger, config: { sink: auditSink, secretKey: process.env.AUDIT_SECRET ?? "x".repeat(32), samplingRate: 1, excludePaths: ["/health"], redactQueryParams: ["token"], redactHeaders: ["authorization", "cookie"], redactBodyFields: ["password", "secret"], recordResponseSize: true, recordRequestBody: false, enableChainIntegrity: true, rng: Math.random, }, hostLogger, }); // ── 4. Compose: audit outermost → rate-limit → site routes ──────── const app = new Hono(); app.use("*", auditMiddleware); app.use("/sites/*", rateLimit({ service: rateLimitService })); app.route("/", publicRoutes); app.route("/", privateRoutes); ``` ## Recommendations The `MIDDLEWARE_RECOMMENDATIONS` constant exported from this package describes the recommended rate-limit ceilings and audit categories per documented route group (`public:resolve`, `public:read`, `private:write`). See `src/integration/middleware-recommendations.md` for the per-field rationale and an advanced consumption example. ## Verification The integration test at `src/integration/middleware-composition.integration.test.ts` boots a real Hono app composed exactly as documented above, exercises `POST /sites`, and asserts: - The response status is `201` and the site row is persisted (site behaviour preserved). - `X-RateLimit-Limit` is present on the response (rate-limit observed the request). - The audit sink contains exactly one `AuditEvent` with `details.method === "POST"` and `details.path === "/sites"`.