Hono Operations
Hono on Cloudflare Workers: composing multi-app APIs in one Worker, middleware
discipline, typed errors, validation at the HTTP boundary, SPA co-serving, RPC
clients, and testing under vitest-pool-workers. Patterns here are distilled from a
production multi-tenant Worker (one Hono app, 6+ mounted sub-apps, ~1350 tests).
Verified against Hono v4 (2026). Workers-first; the Node/Bun/Deno deltas and
porting checklist live in references/runtime-adapters.md.
Staleness check: python scripts/check-hono-facts.py --offline asserts the
version-bearing facts (Hono major, @hono/zod-validator,
@cloudflare/vitest-pool-workers) are still named in the prose and the dated
currency note above is present; --live confirms each package's npm major still
matches. Catalog: assets/hono-facts.json.
Decision Tree
What are you doing with Hono?
│
├─ Structuring an app (generics, sub-apps, env typing)
│ └─ Below + references/app-composition.md
│
├─ Middleware (ordering, auth, headers, exclusion boundaries)
│ └─ Below + references/middleware.md
│
├─ Errors / 404s / request validation
│ └─ Below + references/errors-validation.md
│
├─ Path syntax, routers, c.req/c.res surface, cookies
│ └─ references/routing-and-request.md
│
├─ Serving a SPA / static assets from the same Worker
│ └─ references/workers-runtime.md
│
├─ Cron / queues alongside fetch; runtime gotchas
│ └─ references/workers-runtime.md
│
├─ Streaming / SSE / WebSockets / proxying / service bindings
│ └─ references/streaming-and-realtime.md
│
├─ Durable Objects (Hono in a DO, hibernated WS, alarms)
│ └─ references/durable-objects.md
│
├─ OpenAPI docs from routes (@hono/zod-openapi)
│ └─ references/openapi.md
│
├─ Server-rendered HTML / JSX / HTML emails
│ └─ references/jsx-ssr.md
│
├─ Running or porting to Node / Bun / Deno
│ └─ references/runtime-adapters.md
│
├─ Typed client (hc RPC vs hand-rolled)
│ └─ references/rpc-clients.md
│
├─ Testing (app.request, pool-workers, middleware isolation)
│ └─ references/testing.md + assets/vitest.config.template.ts
│
├─ Starting a new Worker from scratch
│ └─ assets/worker-template.ts (commented composition-root skeleton)
│
└─ Auditing an existing app's routes / middleware order
└─ scripts/route-inventory.py (below)
App Composition (the 80%)
Type the app once with Bindings (wrangler-provided env) and Variables
(per-request context you c.set):
import { Hono } from 'hono';
interface Env {
DB: D1Database;
ASSETS: Fetcher; // static assets binding (SPA)
API_KEYS?: string; // optional secret: gate features on presence, 503 when unset
}
type Vars = { identity: Identity; repo: ScopedRepository };
export const app = new Hono<{ Bindings: Env; Variables: Vars }>();
c.env.DB — bindings, typed via Bindings.
c.set('identity', id) / c.get('identity') / c.var.identity — per-request
state, typed via Variables. Middleware writes it; handlers read it.
- Prefer the per-app
Variables generic over global ContextVariableMap
augmentation; the map is app-wide and leaks types across unrelated sub-apps
(see references/app-composition.md).
Sub-app mounting — one Worker, many feature apps, each its own file:
// src/time/api.ts
export const timeApi = new Hono<{ Bindings: Env; Variables: Vars }>();
timeApi.get('/entries', (c) => { /* identity + repo already in context */ });
// src/index.ts — mounted under the auth middleware (see Middleware below)
app.route('/api/time', timeApi); // timeApi sees paths relative to the mount
app.route('/api/time', billingApi); // two sub-apps on one base is fine when
// their paths are disjoint — Hono matches across both
The mounted sub-app inherits nothing implicitly except position: whatever
middleware was registered on a matching path before the mount runs first.
Position IS the security boundary — see Middleware.
Middleware: Order Is the Contract
Hono middleware is an onion — code before await next() runs inbound, code
after runs outbound — and registration order is matching order. A middleware
registered after a matching handler never runs for it.
app.use('*', securityHeaders()); // 1. outermost: response hardening
app.get('/api/health', (c) => c.json({ ok: true })); // 2. before auth = unauthenticated
app.use('/api/*', async (c, next) => { // 3. auth: verify, then stash identity
if (c.req.path === '/api/health') return next(); // skip-list for exceptions
const user = await verifyAndResolve(c.req.raw, c.env); // throws/403s on failure
if (!user) return c.json({ error: 'forbidden' }, 403);
c.set('identity', user);
c.set('repo', scopedRepo(c.env.DB, user)); // handlers never touch raw bindings
await next();
});
app.route('/api/time', timeApi); // 4. inside the auth boundary
app.route('/vesper', vesper); // 5. OUTSIDE /api/* — bearer-key auth, on purpose
app.route('/ingest', ingest); // machine-to-machine, own auth in the sub-app
app.all('/api/*', (c) => c.json({ error: 'not_found' }, 404)); // JSON 404 for API
app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw)); // SPA fallback, LAST
Two load-bearing rules:
- Auth middleware verifies, then builds the request's whole world (identity,
scoped repo/session) into context. Handlers read
c.get(...) and can't reach
unscoped resources by construction.
- Routes with a different auth model mount OUTSIDE the middleware's path
pattern (
/vesper, /ingest/* above), each carrying its own auth middleware.
Don't punch exemptions through session auth with flags — move the mount.
Depth (skip-lists vs path shape, security headers + the immutable-headers trap,
timing-safe bearer compare): references/middleware.md.
Errors: One Typed Boundary
Throw typed errors anywhere below the handler; map them to HTTP in exactly one
place:
export class AppError extends Error {
constructor(public readonly status: number, public readonly code: string, message: string) {
super(message); this.name = 'AppError';
}
}
export const NotFound = (m = 'not found') => new AppError(404, 'not_found', m);
export const Forbidden = (m = 'forbidden') => new AppError(403, 'forbidden', m);
export const Conflict = (m = 'version conflict, reload and retry') => new AppError(409, 'conflict', m);
app.onError((err, c) => {
if (err instanceof AppError) return c.json({ error: err.code, message: err.message }, err.status as 400);
if (err instanceof SyntaxError) return c.json({ error: 'bad_request', message: 'invalid JSON body' }, 400);
console.error('unhandled error', err); // log the real thing…
return c.json({ error: 'internal' }, 500); // …never leak it to the wire
});
- Cross-scope access returns 404, not 403 — a 403 confirms the row exists in
someone else's scope.
- Unmatched
/api/* gets a JSON 404; everything else falls through to the SPA
shell. Never let an API typo return index.html.
app.notFound() exists but only fires when nothing matched — with a
catch-all SPA route it never runs; use the explicit two-route split above.
Validation at the boundary (zValidator vs hand-rolled assertions, and when each
wins): references/errors-validation.md.
Testing Quickstart
app.request() / app.fetch() run the real app — middleware, routing, errors —
with no server:
import { env } from 'cloudflare:test'; // vitest-pool-workers: real bindings
import { app } from '../src/index';
const res = await app.request('/api/health', {}, env); // env = 3rd arg (Bindings)
expect(res.status).toBe(200);
Under @cloudflare/vitest-pool-workers the test runs inside workerd with real
D1/KV/R2 bindings from defineWorkersConfig. Full setup — migrations into the
test DB, isolated storage, an Access-JWT signing harness, testing one middleware
in isolation, and the workerd-version-lag trap:
references/testing.md.
Route Inventory Script
scripts/route-inventory.py statically scans a Hono TypeScript source tree and
lists every route, middleware registration, and app.route() mount with
file:line — plus --check, three registration-order lints (every finding is
a consequence of Hono matching in registration order):
- bypass — a route registered before a middleware whose pattern covers it
(it silently skips that middleware: the #1 Hono ordering bug)
- duplicate — the same
(method, path) registered twice (the second is dead)
- shadowed — a route after an earlier broader same-method route (never matches)
# Inventory a Worker's HTTP surface (TSV: kind, method, path, file:line)
python skills/hono-ops/scripts/route-inventory.py src/
# JSON envelope for downstream tooling
python skills/hono-ops/scripts/route-inventory.py --json src/ | jq '.data[] | select(.kind=="mount")'
# Lint registration order: exit 10 = findings (each carries an `issue` field in --json)
python skills/hono-ops/scripts/route-inventory.py --check src/
Exit codes: 0 clean, 2 usage, 3 path not found, 10 findings
(--check). Regex-based on purpose — it needs no TypeScript compiler API and
works on any checkout.
Gotchas (Workers-Specific)
| Gotcha |
Why |
Fix |
| "Illegal invocation" on fetch |
Calling this.fetchImpl(...) binds this to your object; global fetch requires no receiver |
Detach first: const doFetch = this.fetchImpl; await doFetch(url, ...) |
Mutating ASSETS.fetch response headers throws |
Any fetch()-derived Response has immutable headers in workerd |
Rebuild: new Response(res.body, { status, headers: new Headers(res.headers) }) |
caches API "cache" misses constantly |
It's per-colo, not global — every PoP has its own |
Treat as a short-TTL local collapse (poll-storm absorber), never as KV |
waitUntil work vanishes |
Post-response work must be registered before the handler returns; unregistered promises are cancelled |
c.executionCtx.waitUntil(promise) inside the handler |
| Middleware doesn't run for a route |
Registered after the handler — order is matching order |
Register middleware first; verify with route-inventory.py --check |
wrangler dev host surprises |
Dev rewrites the request host to the [[routes]] pattern |
Pin [dev] host in wrangler config when auth branches on hostname |
| Optional secret unset |
Route depends on an env secret that isn't configured |
Gate on presence: if (!c.env.KEY) return c.json({ error: 'unavailable' }, 503) |
More depth (SPA assets config, run_worker_first, scheduled/queue handlers,
per-cron branching): references/workers-runtime.md.
Reference Files
| Reference |
When to Load |
| references/app-composition.md |
Generics (Bindings/Variables), ContextVariableMap trade-offs, sub-app mounting semantics, basePath, env-shape design |
| references/middleware.md |
Onion model, ordering proofs, auth middleware that builds context, security headers, bearer-auth sub-apps outside the session boundary |
| references/errors-validation.md |
onError mapping, typed error classes, 404 strategy, zValidator vs hand-rolled validation trade-offs |
| references/routing-and-request.md |
Router internals, path syntax (params/regex/optional/wildcards), matching precedence, c.req/response helpers, cookies (incl. signed), JSX/html |
| references/testing.md |
app.request() patterns, vitest-pool-workers config (D1 migrations, bindings, isolation), JWT test harness, middleware-in-isolation |
| references/rpc-clients.md |
hc<AppType> RPC client, chained-route inference requirement, when a hand-rolled typed client is the better call |
| references/workers-runtime.md |
SPA/static assets from one Worker, scheduled() + queue handlers beside fetch, waitUntil, caches, detached fetch |
| references/streaming-and-realtime.md |
stream/streamText/streamSSE, WebSockets (plain Worker vs Durable Object hibernation), proxying, service bindings |
| references/durable-objects.md |
Routing into DOs, a Hono app per object, hibernated WebSockets, alarms, Hono-in-DO vs RPC methods |
| references/openapi.md |
@hono/zod-openapi schema-first routes, swagger/Scalar UI, hono-openapi annotations, when to skip OpenAPI entirely |
| references/jsx-ssr.md |
hono/jsx server rendering, jsxRenderer layouts, async components + Suspense streaming, raw() escaping rules, the SPA-scope guard (HonoX ladder) |
| references/runtime-adapters.md |
Node (@hono/node-server) / Bun / Deno deltas — env, static files, WebSockets, cron — plus the Workers→Node porting checklist |
Starter assets:
- assets/worker-template.ts — commented
composition-root skeleton (typed env, security headers, auth middleware,
bearer sub-app, 404 split,
onError, cron) with adapt-points marked. Copy it
as the seed of a new Worker.
- assets/vitest.config.template.ts —
vitest-pool-workers config (D1 migrations into the test DB, isolation,
worktree excludes, the compatibility-date pin) ready to adapt.
See Also
cloudflare-ops — wrangler config, bindings provisioning, deploy/CI
sqlite-ops — D1 specifics (sessions/bookmarks, batch semantics, query plans)
typescript-ops — generics, Zod 4, type-narrowing the payloads you validate
rest-ops / api-design-ops — endpoint and contract design above the framework
auth-ops — JWT/session/token theory behind the auth middleware patterns
1---2name: hono-ops3description: "Hono on Cloudflare Workers - composition, middleware, typed bindings, validation, RPC, streaming, testing. Use for: hono, hono middleware, app.route, hono rpc, c.env bindings, onError, zValidator, vitest-pool-workers, spa fallback worker."4license: MIT5allowed-tools: "Read Write Bash Grep Glob"6metadata:7 author: claude-mods8 related-skills: "cloudflare-ops, typescript-ops, sqlite-ops, rest-ops, testing-ops, auth-ops"9---1011# Hono Operations1213Hono on Cloudflare Workers: composing multi-app APIs in one Worker, middleware14discipline, typed errors, validation at the HTTP boundary, SPA co-serving, RPC15clients, and testing under vitest-pool-workers. Patterns here are distilled from a16production multi-tenant Worker (one Hono app, 6+ mounted sub-apps, ~1350 tests).1718> Verified against Hono v4 (2026). Workers-first; the Node/Bun/Deno deltas and19> porting checklist live in references/runtime-adapters.md.2021**Staleness check:** `python scripts/check-hono-facts.py --offline` asserts the22version-bearing facts (Hono major, `@hono/zod-validator`,23`@cloudflare/vitest-pool-workers`) are still named in the prose and the dated24currency note above is present; `--live` confirms each package's npm major still25matches. Catalog: `assets/hono-facts.json`.2627## Decision Tree2829```30What are you doing with Hono?31│32├─ Structuring an app (generics, sub-apps, env typing)33│ └─ Below + references/app-composition.md34│35├─ Middleware (ordering, auth, headers, exclusion boundaries)36│ └─ Below + references/middleware.md37│38├─ Errors / 404s / request validation39│ └─ Below + references/errors-validation.md40│41├─ Path syntax, routers, c.req/c.res surface, cookies42│ └─ references/routing-and-request.md43│44├─ Serving a SPA / static assets from the same Worker45│ └─ references/workers-runtime.md46│47├─ Cron / queues alongside fetch; runtime gotchas48│ └─ references/workers-runtime.md49│50├─ Streaming / SSE / WebSockets / proxying / service bindings51│ └─ references/streaming-and-realtime.md52│53├─ Durable Objects (Hono in a DO, hibernated WS, alarms)54│ └─ references/durable-objects.md55│56├─ OpenAPI docs from routes (@hono/zod-openapi)57│ └─ references/openapi.md58│59├─ Server-rendered HTML / JSX / HTML emails60│ └─ references/jsx-ssr.md61│62├─ Running or porting to Node / Bun / Deno63│ └─ references/runtime-adapters.md64│65├─ Typed client (hc RPC vs hand-rolled)66│ └─ references/rpc-clients.md67│68├─ Testing (app.request, pool-workers, middleware isolation)69│ └─ references/testing.md + assets/vitest.config.template.ts70│71├─ Starting a new Worker from scratch72│ └─ assets/worker-template.ts (commented composition-root skeleton)73│74└─ Auditing an existing app's routes / middleware order75 └─ scripts/route-inventory.py (below)76```7778## App Composition (the 80%)7980Type the app once with `Bindings` (wrangler-provided env) and `Variables`81(per-request context you `c.set`):8283```typescript84import { Hono } from 'hono';8586interface Env {87 DB: D1Database;88 ASSETS: Fetcher; // static assets binding (SPA)89 API_KEYS?: string; // optional secret: gate features on presence, 503 when unset90}91type Vars = { identity: Identity; repo: ScopedRepository };9293export const app = new Hono<{ Bindings: Env; Variables: Vars }>();94```9596- `c.env.DB` — bindings, typed via `Bindings`.97- `c.set('identity', id)` / `c.get('identity')` / `c.var.identity` — per-request98 state, typed via `Variables`. Middleware writes it; handlers read it.99- Prefer the per-app `Variables` generic over global `ContextVariableMap`100 augmentation; the map is app-wide and leaks types across unrelated sub-apps101 (see [references/app-composition.md](references/app-composition.md)).102103**Sub-app mounting** — one Worker, many feature apps, each its own file:104105```typescript106// src/time/api.ts107export const timeApi = new Hono<{ Bindings: Env; Variables: Vars }>();108timeApi.get('/entries', (c) => { /* identity + repo already in context */ });109110// src/index.ts — mounted under the auth middleware (see Middleware below)111app.route('/api/time', timeApi); // timeApi sees paths relative to the mount112app.route('/api/time', billingApi); // two sub-apps on one base is fine when113 // their paths are disjoint — Hono matches across both114```115116The mounted sub-app inherits nothing implicitly except position: whatever117middleware was registered on a matching path *before* the mount runs first.118Position IS the security boundary — see Middleware.119120## Middleware: Order Is the Contract121122Hono middleware is an onion — code before `await next()` runs inbound, code123after runs outbound — and **registration order is matching order**. A middleware124registered after a matching handler never runs for it.125126```typescript127app.use('*', securityHeaders()); // 1. outermost: response hardening128app.get('/api/health', (c) => c.json({ ok: true })); // 2. before auth = unauthenticated129130app.use('/api/*', async (c, next) => { // 3. auth: verify, then stash identity131 if (c.req.path === '/api/health') return next(); // skip-list for exceptions132 const user = await verifyAndResolve(c.req.raw, c.env); // throws/403s on failure133 if (!user) return c.json({ error: 'forbidden' }, 403);134 c.set('identity', user);135 c.set('repo', scopedRepo(c.env.DB, user)); // handlers never touch raw bindings136 await next();137});138139app.route('/api/time', timeApi); // 4. inside the auth boundary140app.route('/vesper', vesper); // 5. OUTSIDE /api/* — bearer-key auth, on purpose141app.route('/ingest', ingest); // machine-to-machine, own auth in the sub-app142143app.all('/api/*', (c) => c.json({ error: 'not_found' }, 404)); // JSON 404 for API144app.all('*', (c) => c.env.ASSETS.fetch(c.req.raw)); // SPA fallback, LAST145```146147Two load-bearing rules:1481491. **Auth middleware verifies, then builds the request's whole world** (identity,150 scoped repo/session) into context. Handlers read `c.get(...)` and can't reach151 unscoped resources by construction.1522. **Routes with a different auth model mount OUTSIDE the middleware's path153 pattern** (`/vesper`, `/ingest/*` above), each carrying its own auth middleware.154 Don't punch exemptions through session auth with flags — move the mount.155156Depth (skip-lists vs path shape, security headers + the immutable-headers trap,157timing-safe bearer compare): [references/middleware.md](references/middleware.md).158159## Errors: One Typed Boundary160161Throw typed errors anywhere below the handler; map them to HTTP in exactly one162place:163164```typescript165export class AppError extends Error {166 constructor(public readonly status: number, public readonly code: string, message: string) {167 super(message); this.name = 'AppError';168 }169}170export const NotFound = (m = 'not found') => new AppError(404, 'not_found', m);171export const Forbidden = (m = 'forbidden') => new AppError(403, 'forbidden', m);172export const Conflict = (m = 'version conflict, reload and retry') => new AppError(409, 'conflict', m);173174app.onError((err, c) => {175 if (err instanceof AppError) return c.json({ error: err.code, message: err.message }, err.status as 400);176 if (err instanceof SyntaxError) return c.json({ error: 'bad_request', message: 'invalid JSON body' }, 400);177 console.error('unhandled error', err); // log the real thing…178 return c.json({ error: 'internal' }, 500); // …never leak it to the wire179});180```181182- Cross-scope access returns **404, not 403** — a 403 confirms the row exists in183 someone else's scope.184- Unmatched `/api/*` gets a JSON 404; everything else falls through to the SPA185 shell. Never let an API typo return `index.html`.186- `app.notFound()` exists but only fires when *nothing* matched — with a187 catch-all SPA route it never runs; use the explicit two-route split above.188189Validation at the boundary (zValidator vs hand-rolled assertions, and when each190wins): [references/errors-validation.md](references/errors-validation.md).191192## Testing Quickstart193194`app.request()` / `app.fetch()` run the real app — middleware, routing, errors —195with no server:196197```typescript198import { env } from 'cloudflare:test'; // vitest-pool-workers: real bindings199import { app } from '../src/index';200201const res = await app.request('/api/health', {}, env); // env = 3rd arg (Bindings)202expect(res.status).toBe(200);203```204205Under `@cloudflare/vitest-pool-workers` the test runs inside workerd with real206D1/KV/R2 bindings from `defineWorkersConfig`. Full setup — migrations into the207test DB, isolated storage, an Access-JWT signing harness, testing one middleware208in isolation, and the workerd-version-lag trap:209[references/testing.md](references/testing.md).210211## Route Inventory Script212213`scripts/route-inventory.py` statically scans a Hono TypeScript source tree and214lists every route, middleware registration, and `app.route()` mount with215`file:line` — plus `--check`, three registration-order lints (every finding is216a consequence of Hono matching in registration order):217218- **bypass** — a route registered *before* a middleware whose pattern covers it219 (it silently skips that middleware: the #1 Hono ordering bug)220- **duplicate** — the same `(method, path)` registered twice (the second is dead)221- **shadowed** — a route after an earlier broader same-method route (never matches)222223```bash224# Inventory a Worker's HTTP surface (TSV: kind, method, path, file:line)225python skills/hono-ops/scripts/route-inventory.py src/226227# JSON envelope for downstream tooling228python skills/hono-ops/scripts/route-inventory.py --json src/ | jq '.data[] | select(.kind=="mount")'229230# Lint registration order: exit 10 = findings (each carries an `issue` field in --json)231python skills/hono-ops/scripts/route-inventory.py --check src/232```233234Exit codes: `0` clean, `2` usage, `3` path not found, `10` findings235(`--check`). Regex-based on purpose — it needs no TypeScript compiler API and236works on any checkout.237238## Gotchas (Workers-Specific)239240| Gotcha | Why | Fix |241|---|---|---|242| "Illegal invocation" on fetch | Calling `this.fetchImpl(...)` binds `this` to your object; global fetch requires no receiver | Detach first: `const doFetch = this.fetchImpl; await doFetch(url, ...)` |243| Mutating `ASSETS.fetch` response headers throws | Any `fetch()`-derived Response has immutable headers in workerd | Rebuild: `new Response(res.body, { status, headers: new Headers(res.headers) })` |244| `caches` API "cache" misses constantly | It's per-colo, not global — every PoP has its own | Treat as a short-TTL local collapse (poll-storm absorber), never as KV |245| `waitUntil` work vanishes | Post-response work must be registered before the handler returns; unregistered promises are cancelled | `c.executionCtx.waitUntil(promise)` inside the handler |246| Middleware doesn't run for a route | Registered after the handler — order is matching order | Register middleware first; verify with `route-inventory.py --check` |247| `wrangler dev` host surprises | Dev rewrites the request host to the `[[routes]]` pattern | Pin `[dev] host` in wrangler config when auth branches on hostname |248| Optional secret unset | Route depends on an env secret that isn't configured | Gate on presence: `if (!c.env.KEY) return c.json({ error: 'unavailable' }, 503)` |249250More depth (SPA assets config, `run_worker_first`, scheduled/queue handlers,251per-cron branching): [references/workers-runtime.md](references/workers-runtime.md).252253## Reference Files254255| Reference | When to Load |256|-----------|-------------|257| [references/app-composition.md](references/app-composition.md) | Generics (`Bindings`/`Variables`), `ContextVariableMap` trade-offs, sub-app mounting semantics, `basePath`, env-shape design |258| [references/middleware.md](references/middleware.md) | Onion model, ordering proofs, auth middleware that builds context, security headers, bearer-auth sub-apps outside the session boundary |259| [references/errors-validation.md](references/errors-validation.md) | `onError` mapping, typed error classes, 404 strategy, zValidator vs hand-rolled validation trade-offs |260| [references/routing-and-request.md](references/routing-and-request.md) | Router internals, path syntax (params/regex/optional/wildcards), matching precedence, `c.req`/response helpers, cookies (incl. signed), JSX/html |261| [references/testing.md](references/testing.md) | `app.request()` patterns, vitest-pool-workers config (D1 migrations, bindings, isolation), JWT test harness, middleware-in-isolation |262| [references/rpc-clients.md](references/rpc-clients.md) | `hc<AppType>` RPC client, chained-route inference requirement, when a hand-rolled typed client is the better call |263| [references/workers-runtime.md](references/workers-runtime.md) | SPA/static assets from one Worker, `scheduled()` + queue handlers beside `fetch`, `waitUntil`, `caches`, detached fetch |264| [references/streaming-and-realtime.md](references/streaming-and-realtime.md) | `stream`/`streamText`/`streamSSE`, WebSockets (plain Worker vs Durable Object hibernation), proxying, service bindings |265| [references/durable-objects.md](references/durable-objects.md) | Routing into DOs, a Hono app per object, hibernated WebSockets, alarms, Hono-in-DO vs RPC methods |266| [references/openapi.md](references/openapi.md) | `@hono/zod-openapi` schema-first routes, swagger/Scalar UI, `hono-openapi` annotations, when to skip OpenAPI entirely |267| [references/jsx-ssr.md](references/jsx-ssr.md) | `hono/jsx` server rendering, `jsxRenderer` layouts, async components + Suspense streaming, `raw()` escaping rules, the SPA-scope guard (HonoX ladder) |268| [references/runtime-adapters.md](references/runtime-adapters.md) | Node (`@hono/node-server`) / Bun / Deno deltas — env, static files, WebSockets, cron — plus the Workers→Node porting checklist |269270**Starter assets:**271272- [assets/worker-template.ts](assets/worker-template.ts) — commented273 composition-root skeleton (typed env, security headers, auth middleware,274 bearer sub-app, 404 split, `onError`, cron) with adapt-points marked. Copy it275 as the seed of a new Worker.276- [assets/vitest.config.template.ts](assets/vitest.config.template.ts) —277 vitest-pool-workers config (D1 migrations into the test DB, isolation,278 worktree excludes, the compatibility-date pin) ready to adapt.279280## See Also281282- `cloudflare-ops` — wrangler config, bindings provisioning, deploy/CI283- `sqlite-ops` — D1 specifics (sessions/bookmarks, batch semantics, query plans)284- `typescript-ops` — generics, Zod 4, type-narrowing the payloads you validate285- `rest-ops` / `api-design-ops` — endpoint and contract design above the framework286- `auth-ops` — JWT/session/token theory behind the auth middleware patterns