Hono BFF Middleware Stack
so1 intermediate 5 min read
ELI5
Every BFF request walks down an assembly line: a tag-and-stamp station (requestId), an ID check (auth), a recorder (logging), the workstation (route), and a quality inspector (error handler) that wraps any defects in a standard envelope before shipping.
Technical Deep Dive
Pipeline Order
flowchart LR inb[inbound HTTP] --> rid[requestId] rid --> auth[Clerk/BetterAuth verify] auth --> log[OTEL structured log] log --> route[route handler] route --> err[error handler] err --> outb[outbound HTTP] err -. catches throws .- routePer ADR-002, middleware runs before any route. The error handler is registered last but logically wraps everything: throws inside the route surface as ErrorEnvelope JSON with the correct HTTP status (see so1-011).
Responsibilities
| Stage | Responsibility |
|---|---|
| requestId | Generate UUID v4, attach to request context, set X-Request-Id response header |
| auth | Read Authorization: Bearer <token>, verify with auth backend, attach { userId, orgId } to context |
| log | Structured log per request: timestamp, requestId, method, path, status, duration_ms, userId, orgId, error |
| route | Business logic, calls adapters (so1-005) |
| error | Catch unhandled throws, redact, wrap in ErrorEnvelope, set status |
Route Surface (per ADR-002)
/api/auth/*, /api/catalog/*, /api/workflows/*, /api/jobs/*, /api/mcp/*.
Why Hono
Per ADR-002: ~14KB gzipped, compiled middleware, edge-ready (Cloudflare Workers, Deno, Node), TypeScript-first. The pipeline shape is a deliberate Express/Fastify analogue.
Key Terms
- Middleware → function that wraps
(c, next)and may run code before, after, or instead ofnext(). - Context → Hono’s per-request bag where middleware writes
userId,requestId, etc. - Adapter → integration-specific module called from a route (see so1-005).
Q&A
Q: If auth fails, do logging and the route still run?
A: No. Auth middleware short-circuits with 401 before logging the success path; the error handler still wraps the response and the request is logged with status: 401 and error.code: UNAUTHORIZED.
Q: What guarantees requestId exists for the error handler?
A: requestId is the first middleware. Every later stage, including thrown errors, has it in context.
Q: Where are validation errors raised?
A: ADR-002 says “contract validation (Zod middleware)” sits between auth and the route; failures throw VALIDATION_ERROR (400) and surface through the error handler.
Examples
A POST /api/jobs with an expired session: requestId stamped, auth verifies → throws 401, error handler emits { requestId, error: { code: "UNAUTHORIZED", message: "…" } }, log line shows status: 401, duration_ms: 3, userId: null.
neighbors on the map
- purr-api Layered Architecture adding a new feature to purr-api
- Two-Service Architecture onboarding to the chronicle-hq monorepo
- Airlock Bearer Auth diagnosing a 401 from /v1/ingest/*