Zum Inhalt springen

ADR-0001: Adopt Hono as the HTTP framework on every non-Astro Worker

ADR-0001: Adopt Hono as the HTTP framework on every non-Astro Worker

Section titled “ADR-0001: Adopt Hono as the HTTP framework on every non-Astro Worker”
  • Status: Accepted (2026-05-24)
  • Deciders: @ArneFfm
  • Master design impact: Adds D40. No existing D-row (D1–D39) is contradicted.
  • Amended 2026-06-07 (master D42): the admin frontend is Hono, not an “eventual Astro layer.” apps/admin renders its HTML server-side via Hono and consumes the design system through the renderer-agnostic CVA *.variants.ts contract + @nowyourlink/design-tokens — not the .astro primitives. The Decision below is corrected accordingly.

Today every non-Astro Worker (apps/api, apps/admin, apps/webhook, apps/queue, apps/cron) hand-rolls its dispatch with new URL(request.url) + if (url.pathname === "...") chains. Each Worker also duplicates an errorBoundary(request, env, handler) function — ~50 lines of copy-pasted request-id + problem+json plumbing per worker. This is brittle (drift between workers is silent), uncovered (the inline errorBoundary in apps/admin/src/index.ts is dead code — the handler never wraps in it), and friction-heavy for sub-system specs 02, 03, 05, 08, 09 which add ~30 HTTP endpoints over the next phase.

Master design D1–D39 do not pick an HTTP framework. The slot is open.

Use Hono as the canonical HTTP framework on every non-Astro Worker. apps/web stays Astro per D17; apps/admin — including its frontend — is a Hono worker (master D42), consuming the design system via the CVA variant contract + design-tokens rather than the .astro primitives; docs-site/ stays Starlight per D18.

The Hono app lives in src/app.ts per Worker. src/index.ts keeps the Durable Object exports (SessionDO, UserRead) and wraps app.fetch in Sentry.withSentry to preserve the master §4.6 instrumentation contract.

OptionWhy rejected
Status quo (hand-rolled URL + if)Five copies of the same 50-line errorBoundary; drift between workers is undetected; admin’s copy is already dead code.
itty-routerSmaller bundle but no first-class middleware after-pattern (await next() then mutate response) — request-id echo would require manual response cloning. No onError contract.
@cloudflare/workers-routingNo app.request()/app.fetch() helper for in-memory tests; less mature TypeScript inference for Bindings.
Hono + hono/jwt + hono/cors + @hono/zod-validatorOut of scope — adopt only hono core in this PR. Helpers land per-spec when their owning issue does (e.g. spec 08 §6’s CORS is the natural moment to introduce hono/cors).
  • packages/errors — HttpError, toProblemJson, InvariantError are the byte-identical source of the RFC 7807 envelope. Hono app.onError calls toProblemJson(err); it does not use Hono’s HTTPException (which is not aware of Context-set headers per the Hono docs — HTTPException.getResponse() would drop the X-Request-Id set in middleware).
  • packages/logging — createLogger, sentryTransport, the LogLine schema and field order. Hono middleware c.set("logger", ...) exposes the same per-request Logger to handlers via c.get("logger").
  • @sentry/cloudflare Sentry.withSentry(opts, handler) — wraps the module fetch verbatim. The Hono app is passed in as { fetch: app.fetch }.
  • Route paths, status codes, response bodies, auth/session contracts, DO and Queue bindings, wrangler.jsonc.
  • Adopted: apps/api, apps/admin, apps/webhook, apps/queue, apps/cron.
  • apps/queue and apps/cron each currently have a fetch handler (queue’s returns a “use the binding” probe; cron’s returns “Hello from cron”). Both adopt Hono.
  • Not adopted in this PR: apps/web (Astro / D17), apps/ws (out of scope per the migration brief — left untouched), Durable Objects (their own fetch() contract is owned by Workers’ runtime, not by a router), Cloudflare Workflows (step.do orchestration is not HTTP).
  • apps/api, apps/webhook, apps/queue, apps/cron: Hono app with a request-id middleware that resolves the inbound X-Request-Id (regex /^[A-Za-z0-9._-]{1,128}$/), mints a UUID otherwise, sets it on the context, and echoes it on the outbound headers after await next() via c.header(REQUEST_ID_HEADER, requestId). app.onError maps HttpError → toProblemJson(err) (or InvariantError fallback) and stamps the request-id.
  • apps/admin: Hono route table with no request-id middleware and no app.onError. Its current handler returns plain Response objects (Forbidden, JSON, HTML) without a problem+json envelope and without echoing X-Request-Id — the migration preserves that. Any future symmetric errorBoundary requirement is tracked as a follow-up.

Positive:

  • One source of truth for request-id + problem+json envelope on api/webhook/queue/cron (the Hono middleware + app.onError).
  • app.fetch(request, env, ctx) enables fast in-process integration tests without spinning up wrangler dev — see spec 13 §3.1’s SELF.fetch is still available for end-to-end coverage, but unit-style route round-trips now go through app.fetch() directly.
  • New endpoints in spec 02/03/05/08/09 are a single .post('/path', handler) line each.

Negative:

  • One more dependency (Hono ^4, ~30 KB unminified, ~5 KB gzipped; well under Workers’ 1 MiB script limit).
  • Each worker’s existing handler unit tests stay valid; app.fetch() round-trips are added on top, growing the test surface ~3x for the small workers.
  • Migrate apps/ws to Hono when the WebSocket upgrade routing pattern is reviewed alongside the spec 06 Durable Object work.
  • Adopt hono/cors when spec 08 §6’s POST /conv ingest endpoint lands (issue not yet open).
  • Adopt @hono/zod-validator when a sub-system needs schema-validated request bodies — current handlers do their own narrow type guards inline (see apps/api/src/auth-routes.ts:144-156).
  • Extract the request-id middleware + Sentry-logger wiring into packages/middleware once at least three workers are stable on the current pattern — deferring keeps this PR’s invariant (“shape stays the same”) intact.