{"slug":"hono-ops","title":"hono-ops","summary":"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.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-30T19:36:46.422882Z","repo":{"url":"https://github.com/0xDarkMatter/claude-mods","stars":43,"forks":7,"license":"MIT","updatedAt":"2026-09-30T15:18:48Z"},"bodyHtml":"<hr>\n<h2>name: hono-ops\ndescription: \"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.\"\nlicense: MIT\nallowed-tools: \"Read Write Bash Grep Glob\"\nmetadata:\nauthor: claude-mods\nrelated-skills: \"cloudflare-ops, typescript-ops, sqlite-ops, rest-ops, testing-ops, auth-ops\"</h2>\n<h1>Hono Operations</h1>\n<p>Hono on Cloudflare Workers: composing multi-app APIs in one Worker, middleware\ndiscipline, typed errors, validation at the HTTP boundary, SPA co-serving, RPC\nclients, and testing under vitest-pool-workers. Patterns here are distilled from a\nproduction multi-tenant Worker (one Hono app, 6+ mounted sub-apps, ~1350 tests).</p>\n<blockquote>\n<p>Verified against Hono v4 (2026). Workers-first; the Node/Bun/Deno deltas and\nporting checklist live in references/runtime-adapters.md.</p>\n</blockquote>\n<p><strong>Staleness check:</strong> <code>python scripts/check-hono-facts.py --offline</code> asserts the\nversion-bearing facts (Hono major, <code>@hono/zod-validator</code>,\n<code>@cloudflare/vitest-pool-workers</code>) are still named in the prose and the dated\ncurrency note above is present; <code>--live</code> confirms each package's npm major still\nmatches. Catalog: <code>assets/hono-facts.json</code>.</p>\n<h2>Decision Tree</h2>\n<pre><code>What are you doing with Hono?\n│\n├─ Structuring an app (generics, sub-apps, env typing)\n│  └─ Below + references/app-composition.md\n│\n├─ Middleware (ordering, auth, headers, exclusion boundaries)\n│  └─ Below + references/middleware.md\n│\n├─ Errors / 404s / request validation\n│  └─ Below + references/errors-validation.md\n│\n├─ Path syntax, routers, c.req/c.res surface, cookies\n│  └─ references/routing-and-request.md\n│\n├─ Serving a SPA / static assets from the same Worker\n│  └─ references/workers-runtime.md\n│\n├─ Cron / queues alongside fetch; runtime gotchas\n│  └─ references/workers-runtime.md\n│\n├─ Streaming / SSE / WebSockets / proxying / service bindings\n│  └─ references/streaming-and-realtime.md\n│\n├─ Durable Objects (Hono in a DO, hibernated WS, alarms)\n│  └─ references/durable-objects.md\n│\n├─ OpenAPI docs from routes (@hono/zod-openapi)\n│  └─ references/openapi.md\n│\n├─ Server-rendered HTML / JSX / HTML emails\n│  └─ references/jsx-ssr.md\n│\n├─ Running or porting to Node / Bun / Deno\n│  └─ references/runtime-adapters.md\n│\n├─ Typed client (hc RPC vs hand-rolled)\n│  └─ references/rpc-clients.md\n│\n├─ Testing (app.request, pool-workers, middleware isolation)\n│  └─ references/testing.md + assets/vitest.config.template.ts\n│\n├─ Starting a new Worker from scratch\n│  └─ assets/worker-template.ts (commented composition-root skeleton)\n│\n└─ Auditing an existing app's routes / middleware order\n   └─ scripts/route-inventory.py (below)\n</code></pre>\n<h2>App Composition (the 80%)</h2>\n<p>Type the app once with <code>Bindings</code> (wrangler-provided env) and <code>Variables</code>\n(per-request context you <code>c.set</code>):</p>\n<pre><code>import { Hono } from 'hono';\n\ninterface Env {\n  DB: D1Database;\n  ASSETS: Fetcher;          // static assets binding (SPA)\n  API_KEYS?: string;        // optional secret: gate features on presence, 503 when unset\n}\ntype Vars = { identity: Identity; repo: ScopedRepository };\n\nexport const app = new Hono&lt;{ Bindings: Env; Variables: Vars }&gt;();\n</code></pre>\n<ul>\n<li><code>c.env.DB</code> — bindings, typed via <code>Bindings</code>.</li>\n<li><code>c.set('identity', id)</code> / <code>c.get('identity')</code> / <code>c.var.identity</code> — per-request\nstate, typed via <code>Variables</code>. Middleware writes it; handlers read it.</li>\n<li>Prefer the per-app <code>Variables</code> generic over global <code>ContextVariableMap</code>\naugmentation; the map is app-wide and leaks types across unrelated sub-apps\n(see <a href=\"references/app-composition.md\">references/app-composition.md</a>).</li>\n</ul>\n<p><strong>Sub-app mounting</strong> — one Worker, many feature apps, each its own file:</p>\n<pre><code>// src/time/api.ts\nexport const timeApi = new Hono&lt;{ Bindings: Env; Variables: Vars }&gt;();\ntimeApi.get('/entries', (c) =&gt; { /* identity + repo already in context */ });\n\n// src/index.ts — mounted under the auth middleware (see Middleware below)\napp.route('/api/time', timeApi);       // timeApi sees paths relative to the mount\napp.route('/api/time', billingApi);    // two sub-apps on one base is fine when\n                                       // their paths are disjoint — Hono matches across both\n</code></pre>\n<p>The mounted sub-app inherits nothing implicitly except position: whatever\nmiddleware was registered on a matching path <em>before</em> the mount runs first.\nPosition IS the security boundary — see Middleware.</p>\n<h2>Middleware: Order Is the Contract</h2>\n<p>Hono middleware is an onion — code before <code>await next()</code> runs inbound, code\nafter runs outbound — and <strong>registration order is matching order</strong>. A middleware\nregistered after a matching handler never runs for it.</p>\n<pre><code>app.use('*', securityHeaders());        // 1. outermost: response hardening\napp.get('/api/health', (c) =&gt; c.json({ ok: true }));  // 2. before auth = unauthenticated\n\napp.use('/api/*', async (c, next) =&gt; {  // 3. auth: verify, then stash identity\n  if (c.req.path === '/api/health') return next();   // skip-list for exceptions\n  const user = await verifyAndResolve(c.req.raw, c.env);   // throws/403s on failure\n  if (!user) return c.json({ error: 'forbidden' }, 403);\n  c.set('identity', user);\n  c.set('repo', scopedRepo(c.env.DB, user));  // handlers never touch raw bindings\n  await next();\n});\n\napp.route('/api/time', timeApi);        // 4. inside the auth boundary\napp.route('/vesper', vesper);           // 5. OUTSIDE /api/* — bearer-key auth, on purpose\napp.route('/ingest', ingest);           // machine-to-machine, own auth in the sub-app\n\napp.all('/api/*', (c) =&gt; c.json({ error: 'not_found' }, 404));  // JSON 404 for API\napp.all('*', (c) =&gt; c.env.ASSETS.fetch(c.req.raw));             // SPA fallback, LAST\n</code></pre>\n<p>Two load-bearing rules:</p>\n<ol>\n<li><strong>Auth middleware verifies, then builds the request's whole world</strong> (identity,\nscoped repo/session) into context. Handlers read <code>c.get(...)</code> and can't reach\nunscoped resources by construction.</li>\n<li><strong>Routes with a different auth model mount OUTSIDE the middleware's path\npattern</strong> (<code>/vesper</code>, <code>/ingest/*</code> above), each carrying its own auth middleware.\nDon't punch exemptions through session auth with flags — move the mount.</li>\n</ol>\n<p>Depth (skip-lists vs path shape, security headers + the immutable-headers trap,\ntiming-safe bearer compare): <a href=\"references/middleware.md\">references/middleware.md</a>.</p>\n<h2>Errors: One Typed Boundary</h2>\n<p>Throw typed errors anywhere below the handler; map them to HTTP in exactly one\nplace:</p>\n<pre><code>export class AppError extends Error {\n  constructor(public readonly status: number, public readonly code: string, message: string) {\n    super(message); this.name = 'AppError';\n  }\n}\nexport const NotFound  = (m = 'not found')  =&gt; new AppError(404, 'not_found', m);\nexport const Forbidden = (m = 'forbidden')  =&gt; new AppError(403, 'forbidden', m);\nexport const Conflict  = (m = 'version conflict, reload and retry') =&gt; new AppError(409, 'conflict', m);\n\napp.onError((err, c) =&gt; {\n  if (err instanceof AppError)    return c.json({ error: err.code, message: err.message }, err.status as 400);\n  if (err instanceof SyntaxError) return c.json({ error: 'bad_request', message: 'invalid JSON body' }, 400);\n  console.error('unhandled error', err);          // log the real thing…\n  return c.json({ error: 'internal' }, 500);      // …never leak it to the wire\n});\n</code></pre>\n<ul>\n<li>Cross-scope access returns <strong>404, not 403</strong> — a 403 confirms the row exists in\nsomeone else's scope.</li>\n<li>Unmatched <code>/api/*</code> gets a JSON 404; everything else falls through to the SPA\nshell. Never let an API typo return <code>index.html</code>.</li>\n<li><code>app.notFound()</code> exists but only fires when <em>nothing</em> matched — with a\ncatch-all SPA route it never runs; use the explicit two-route split above.</li>\n</ul>\n<p>Validation at the boundary (zValidator vs hand-rolled assertions, and when each\nwins): <a href=\"references/errors-validation.md\">references/errors-validation.md</a>.</p>\n<h2>Testing Quickstart</h2>\n<p><code>app.request()</code> / <code>app.fetch()</code> run the real app — middleware, routing, errors —\nwith no server:</p>\n<pre><code>import { env } from 'cloudflare:test';   // vitest-pool-workers: real bindings\nimport { app } from '../src/index';\n\nconst res = await app.request('/api/health', {}, env);   // env = 3rd arg (Bindings)\nexpect(res.status).toBe(200);\n</code></pre>\n<p>Under <code>@cloudflare/vitest-pool-workers</code> the test runs inside workerd with real\nD1/KV/R2 bindings from <code>defineWorkersConfig</code>. Full setup — migrations into the\ntest DB, isolated storage, an Access-JWT signing harness, testing one middleware\nin isolation, and the workerd-version-lag trap:\n<a href=\"references/testing.md\">references/testing.md</a>.</p>\n<h2>Route Inventory Script</h2>\n<p><code>scripts/route-inventory.py</code> statically scans a Hono TypeScript source tree and\nlists every route, middleware registration, and <code>app.route()</code> mount with\n<code>file:line</code> — plus <code>--check</code>, three registration-order lints (every finding is\na consequence of Hono matching in registration order):</p>\n<ul>\n<li><strong>bypass</strong> — a route registered <em>before</em> a middleware whose pattern covers it\n(it silently skips that middleware: the #1 Hono ordering bug)</li>\n<li><strong>duplicate</strong> — the same <code>(method, path)</code> registered twice (the second is dead)</li>\n<li><strong>shadowed</strong> — a route after an earlier broader same-method route (never matches)</li>\n</ul>\n<pre><code># Inventory a Worker's HTTP surface (TSV: kind, method, path, file:line)\npython skills/hono-ops/scripts/route-inventory.py src/\n\n# JSON envelope for downstream tooling\npython skills/hono-ops/scripts/route-inventory.py --json src/ | jq '.data[] | select(.kind==\"mount\")'\n\n# Lint registration order: exit 10 = findings (each carries an `issue` field in --json)\npython skills/hono-ops/scripts/route-inventory.py --check src/\n</code></pre>\n<p>Exit codes: <code>0</code> clean, <code>2</code> usage, <code>3</code> path not found, <code>10</code> findings\n(<code>--check</code>). Regex-based on purpose — it needs no TypeScript compiler API and\nworks on any checkout.</p>\n<h2>Gotchas (Workers-Specific)</h2>\n<table>\n<thead>\n<tr>\n<th>Gotcha</th>\n<th>Why</th>\n<th>Fix</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>\"Illegal invocation\" on fetch</td>\n<td>Calling <code>this.fetchImpl(...)</code> binds <code>this</code> to your object; global fetch requires no receiver</td>\n<td>Detach first: <code>const doFetch = this.fetchImpl; await doFetch(url, ...)</code></td>\n</tr>\n<tr>\n<td>Mutating <code>ASSETS.fetch</code> response headers throws</td>\n<td>Any <code>fetch()</code>-derived Response has immutable headers in workerd</td>\n<td>Rebuild: <code>new Response(res.body, { status, headers: new Headers(res.headers) })</code></td>\n</tr>\n<tr>\n<td><code>caches</code> API \"cache\" misses constantly</td>\n<td>It's per-colo, not global — every PoP has its own</td>\n<td>Treat as a short-TTL local collapse (poll-storm absorber), never as KV</td>\n</tr>\n<tr>\n<td><code>waitUntil</code> work vanishes</td>\n<td>Post-response work must be registered before the handler returns; unregistered promises are cancelled</td>\n<td><code>c.executionCtx.waitUntil(promise)</code> inside the handler</td>\n</tr>\n<tr>\n<td>Middleware doesn't run for a route</td>\n<td>Registered after the handler — order is matching order</td>\n<td>Register middleware first; verify with <code>route-inventory.py --check</code></td>\n</tr>\n<tr>\n<td><code>wrangler dev</code> host surprises</td>\n<td>Dev rewrites the request host to the <code>[[routes]]</code> pattern</td>\n<td>Pin <code>[dev] host</code> in wrangler config when auth branches on hostname</td>\n</tr>\n<tr>\n<td>Optional secret unset</td>\n<td>Route depends on an env secret that isn't configured</td>\n<td>Gate on presence: <code>if (!c.env.KEY) return c.json({ error: 'unavailable' }, 503)</code></td>\n</tr>\n</tbody>\n</table>\n<p>More depth (SPA assets config, <code>run_worker_first</code>, scheduled/queue handlers,\nper-cron branching): <a href=\"references/workers-runtime.md\">references/workers-runtime.md</a>.</p>\n<h2>Reference Files</h2>\n<table>\n<thead>\n<tr>\n<th>Reference</th>\n<th>When to Load</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"references/app-composition.md\">references/app-composition.md</a></td>\n<td>Generics (<code>Bindings</code>/<code>Variables</code>), <code>ContextVariableMap</code> trade-offs, sub-app mounting semantics, <code>basePath</code>, env-shape design</td>\n</tr>\n<tr>\n<td><a href=\"references/middleware.md\">references/middleware.md</a></td>\n<td>Onion model, ordering proofs, auth middleware that builds context, security headers, bearer-auth sub-apps outside the session boundary</td>\n</tr>\n<tr>\n<td><a href=\"references/errors-validation.md\">references/errors-validation.md</a></td>\n<td><code>onError</code> mapping, typed error classes, 404 strategy, zValidator vs hand-rolled validation trade-offs</td>\n</tr>\n<tr>\n<td><a href=\"references/routing-and-request.md\">references/routing-and-request.md</a></td>\n<td>Router internals, path syntax (params/regex/optional/wildcards), matching precedence, <code>c.req</code>/response helpers, cookies (incl. signed), JSX/html</td>\n</tr>\n<tr>\n<td><a href=\"references/testing.md\">references/testing.md</a></td>\n<td><code>app.request()</code> patterns, vitest-pool-workers config (D1 migrations, bindings, isolation), JWT test harness, middleware-in-isolation</td>\n</tr>\n<tr>\n<td><a href=\"references/rpc-clients.md\">references/rpc-clients.md</a></td>\n<td><code>hc&lt;AppType&gt;</code> RPC client, chained-route inference requirement, when a hand-rolled typed client is the better call</td>\n</tr>\n<tr>\n<td><a href=\"references/workers-runtime.md\">references/workers-runtime.md</a></td>\n<td>SPA/static assets from one Worker, <code>scheduled()</code> + queue handlers beside <code>fetch</code>, <code>waitUntil</code>, <code>caches</code>, detached fetch</td>\n</tr>\n<tr>\n<td><a href=\"references/streaming-and-realtime.md\">references/streaming-and-realtime.md</a></td>\n<td><code>stream</code>/<code>streamText</code>/<code>streamSSE</code>, WebSockets (plain Worker vs Durable Object hibernation), proxying, service bindings</td>\n</tr>\n<tr>\n<td><a href=\"references/durable-objects.md\">references/durable-objects.md</a></td>\n<td>Routing into DOs, a Hono app per object, hibernated WebSockets, alarms, Hono-in-DO vs RPC methods</td>\n</tr>\n<tr>\n<td><a href=\"references/openapi.md\">references/openapi.md</a></td>\n<td><code>@hono/zod-openapi</code> schema-first routes, swagger/Scalar UI, <code>hono-openapi</code> annotations, when to skip OpenAPI entirely</td>\n</tr>\n<tr>\n<td><a href=\"references/jsx-ssr.md\">references/jsx-ssr.md</a></td>\n<td><code>hono/jsx</code> server rendering, <code>jsxRenderer</code> layouts, async components + Suspense streaming, <code>raw()</code> escaping rules, the SPA-scope guard (HonoX ladder)</td>\n</tr>\n<tr>\n<td><a href=\"references/runtime-adapters.md\">references/runtime-adapters.md</a></td>\n<td>Node (<code>@hono/node-server</code>) / Bun / Deno deltas — env, static files, WebSockets, cron — plus the Workers→Node porting checklist</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Starter assets:</strong></p>\n<ul>\n<li><a href=\"assets/worker-template.ts\">assets/worker-template.ts</a> — commented\ncomposition-root skeleton (typed env, security headers, auth middleware,\nbearer sub-app, 404 split, <code>onError</code>, cron) with adapt-points marked. Copy it\nas the seed of a new Worker.</li>\n<li><a href=\"assets/vitest.config.template.ts\">assets/vitest.config.template.ts</a> —\nvitest-pool-workers config (D1 migrations into the test DB, isolation,\nworktree excludes, the compatibility-date pin) ready to adapt.</li>\n</ul>\n<h2>See Also</h2>\n<ul>\n<li><code>cloudflare-ops</code> — wrangler config, bindings provisioning, deploy/CI</li>\n<li><code>sqlite-ops</code> — D1 specifics (sessions/bookmarks, batch semantics, query plans)</li>\n<li><code>typescript-ops</code> — generics, Zod 4, type-narrowing the payloads you validate</li>\n<li><code>rest-ops</code> / <code>api-design-ops</code> — endpoint and contract design above the framework</li>\n<li><code>auth-ops</code> — JWT/session/token theory behind the auth middleware patterns</li>\n</ul>\n","files":[{"path":"assets/hono-facts.json","sizeBytes":883,"isText":true},{"path":"assets/vitest.config.template.ts","sizeBytes":2948,"isText":true},{"path":"assets/worker-template.ts","sizeBytes":5892,"isText":true},{"path":"references/app-composition.md","sizeBytes":5906,"isText":true},{"path":"references/durable-objects.md","sizeBytes":5058,"isText":true},{"path":"references/errors-validation.md","sizeBytes":7070,"isText":true},{"path":"references/jsx-ssr.md","sizeBytes":5399,"isText":true},{"path":"references/middleware.md","sizeBytes":10001,"isText":true},{"path":"references/openapi.md","sizeBytes":3769,"isText":true},{"path":"references/routing-and-request.md","sizeBytes":6096,"isText":true},{"path":"references/rpc-clients.md","sizeBytes":6144,"isText":true},{"path":"references/runtime-adapters.md","sizeBytes":4970,"isText":true},{"path":"references/streaming-and-realtime.md","sizeBytes":5838,"isText":true},{"path":"references/testing.md","sizeBytes":7538,"isText":true},{"path":"references/workers-runtime.md","sizeBytes":7307,"isText":true},{"path":"scripts/check-hono-facts.py","sizeBytes":10279,"isText":true},{"path":"scripts/route-inventory.py","sizeBytes":11699,"isText":true},{"path":"SKILL.md","sizeBytes":14851,"isText":true},{"path":"tests/fixtures/sample-app.ts","sizeBytes":1970,"isText":true},{"path":"tests/run.sh","sizeBytes":8278,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-30T19:37:46.195621Z","sha256":"C247D8872540223C0A12A20E366BE2151296695133908DE51B281B8945D5306B","sizeBytes":60121},"review":null,"source":{"repositoryUrl":"https://github.com/0xDarkMatter/claude-mods","path":"skills/hono-ops","license":"MIT","commit":"3dfaf0ba5753026a99ee13f9d9ed56b9793bb6e8","subtreeSha":"BDD8C7D6253113AA2FBC5236238F683DDDBC006C8D97A2B19B1A2AC3C61B649B","lastSyncedAt":"2026-09-30T19:37:28.226022Z"},"reviewedAt":"2026-09-30T19:39:37.583986Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/hono-ops"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart"},{"target":"git","command":"git clone https://github.com/0xDarkMatter/claude-mods.git"}]}