Use when building APIs on Hono (Cloudflare Workers, Bun, Deno, Node), debugging route ordering, wiring middleware, validating with @hono/zod-validator, returning streaming responses, configuring CORS with credentials, handling errors via app.onError, or composing typed RPC clients. Triggers: c.env binding types, c.header + c.redirect interaction, missing await next() bugs, Set-Cookie not attaching to redirect, route-precedence surprises, JWT middleware setup, hono/client typed RPC end-to-end....
Scanned 9/11/2026
Install to Claude Code
npx -y skills add curiositech/windags-skills --skill hono-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Hono Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/curiositech-hono-patterns)More formats (shields.io, HTML) on the badges page.
---
name: hono-patterns
description: 'Use when building APIs on Hono (Cloudflare Workers, Bun, Deno, Node), debugging route ordering, wiring middleware, validating with @hono/zod-validator, returning streaming responses, configuring CORS with credentials, handling errors via app.onError, or composing typed RPC clients. Triggers: c.env binding types, c.header + c.redirect interaction, missing await next() bugs, Set-Cookie not attaching to redirect, route-precedence surprises, JWT middleware setup, hono/client typed RPC end-to-end. NOT for Express/Fastify/Koa idioms, tRPC/GraphQL paradigms, or Next.js Route Handlers.'
category: Backend & Infrastructure
tags:
- hono
- cloudflare-workers
- bun
- edge
- middleware
- typescript
---
# Hono Patterns
Hono is a small router with a strong middleware model and ergonomic typing. Most surprises come from middleware ordering, deferred header semantics, and the `c.env`/`Variables` generic dance.
## When to use
- Designing an API on Workers, Bun, or Deno where Express would be overkill.
- Type-safe end-to-end RPC client (`hono/client`).
- Streaming SSE / NDJSON responses.
- A redirect-driven login flow where Set-Cookie must attach to the response.
- CORS with credentials, where `*` won't work.
- Custom error responses through `app.onError`.
## Core capabilities
### The `Hono` generic — typed bindings and locals
```ts
type Bindings = {
DB: D1Database;
KV: KVNamespace;
WINDAGS_INTERNAL_TOKEN: string;
};
type Variables = {
user: { id: string; role: 'admin' | 'member' };
startedAt: number;
};
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
```
Now `c.env.DB` is typed `D1Database`; `c.var.user` is typed; `c.set('user', …)` and `c.get('user')` are checked.
### Route ordering
Hono matches in registration order. Specific before wildcard:
```ts
app.get('/v1/skills/search', searchHandler); // specific
app.get('/v1/skills/:id', getOneHandler); // param
app.all('/v1/*', fallback); // catch-all LAST
```
Mounting sub-apps:
```ts
const v2 = new Hono<{ Bindings: Bindings }>();
v2.get('/skills', listV2);
app.route('/v2', v2);
// Or basePath for the whole app:
const api = new Hono().basePath('/api');
```
### Middleware
The chain is request → middleware → handler → middleware-after → response. `await next()` is where you switch direction.
```ts
app.use('*', async (c, next) => {
const start = Date.now();
await next();
c.header('Server-Timing', `total;dur=${Date.now() - start}`);
});
app.use('/admin/*', async (c, next) => {
const session = c.req.header('Cookie');
if (!session) return c.text('forbidden', 403);
c.set('user', await loadUser(session));
await next();
});
```
Forgetting `await next()` is the most common bug. The handler never runs and the request hangs at middleware.
### Validator middleware
```ts
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const createSkill = z.object({
id: z.string().regex(/^[a-z0-9-]+$/).max(80),
name: z.string().min(1).max(120),
tags: z.array(z.string()).max(20).optional(),
});
app.post(
'/v1/skills',
zValidator('json', createSkill),
async (c) => {
const body = c.req.valid('json'); // typed, narrowed
await c.env.DB.prepare(
'INSERT INTO skills (id, name, tags) VALUES (?, ?, ?)'
).bind(body.id, body.name, JSON.stringify(body.tags ?? [])).run();
return c.json({ ok: true }, 201);
},
);
```
Validation slots: `'json' | 'form' | 'query' | 'param' | 'header' | 'cookie'`. Each adds a typed accessor.
### `c.header()` + `c.redirect()` — the deferred-headers model
```ts
app.get('/_internal/login', (c) => {
const token = c.req.query('token');
if (token !== c.env.WINDAGS_INTERNAL_TOKEN) return c.text('forbidden', 403);
c.header(
'Set-Cookie',
`session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/_internal; Max-Age=${30*86400}`,
);
return c.redirect('/_internal/dashboard');
});
```
`c.header()` registers a deferred header on the context; the final `Response` (whether from `c.json`, `c.text`, or `c.redirect`) merges those deferred headers. The cookie WILL attach. If a downstream observer says it didn't, the issue is browser-side (SameSite=Strict on a redirect) or upstream (proxy stripping headers), not Hono.
### Streaming
```ts
import { streamSSE } from 'hono/streaming';
app.get('/v1/events', (c) => {
return streamSSE(c, async (stream) => {
let id = 0;
while (!stream.aborted) {
await stream.writeSSE({ id: String(id++), event: 'tick', data: JSON.stringify({ now: Date.now() }) });
await stream.sleep(1000);
}
});
});
```
`stream.aborted` flips when the client disconnects — check it in any long loop or you'll leak.
For raw bytes/NDJSON:
```ts
import { stream } from 'hono/streaming';
app.get('/v1/dump', (c) => stream(c, async (s) => {
for await (const row of fetchRows()) {
await s.write(JSON.stringify(row) + '\n');
}
}));
```
### Error handling
```ts
import { HTTPException } from 'hono/http-exception';
app.onError((err, c) => {
if (err instanceof HTTPException) return err.getResponse();
// Log with the right severity; don't leak internals.
console.error('unhandled', err);
return c.json({ error: 'internal' }, 500);
});
// Throw a typed error from anywhere:
throw new HTTPException(429, { message: 'rate limited', res: new Response('go slow', { status: 429 }) });
```
`onError` wraps the whole app. Routes that throw end up here unless an explicit try/catch handles them.
### Cookies via `hono/cookie`
```ts
import { setCookie, getCookie, deleteCookie } from 'hono/cookie';
setCookie(c, 'session', token, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/_internal',
maxAge: 30 * 86400,
});
const session = getCookie(c, 'session');
deleteCookie(c, 'session', { path: '/_internal' });
```
Helper-based cookies are easier to read than raw `Set-Cookie` strings; both work.
### CORS with credentials
```ts
import { cors } from 'hono/cors';
app.use('/v1/*', cors({
origin: (origin) => origin?.endsWith('.windags.ai') ? origin : null,
credentials: true, // sends cookies cross-origin
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
}));
```
`credentials: true` requires the explicit origin echoed back — `*` won't work and the browser will block.
### Typed RPC client
```ts
// server
const route = app
.get('/v1/skills/:id', (c) => c.json({ id: c.req.param('id'), name: 'x' }))
.post('/v1/skills', zValidator('json', createSkill), (c) => c.json({ ok: true as const }));
export type AppType = typeof route;
// client
import { hc } from 'hono/client';
const client = hc<AppType>('https://api.windags.ai');
const res = await client.v1.skills[':id'].$get({ param: { id: 'foo' } });
const data = await res.json(); // typed { id: string; name: string }
```
The client mirrors the server type tree exactly. Refactor a route → the client breaks at compile time.
## Anti-patterns
### Catch-all before specific routes
**Symptom:** Specific routes return 404 / wrong handler responses.
**Diagnosis:** `app.all('*', …)` registered before specific GETs. First match wins.
**Fix:** Register catch-alls last. Use `app.notFound(handler)` for 404 instead of a wildcard.
### Forgetting `await next()`
**Symptom:** Request hangs or returns nothing; logs show middleware ran but handler didn't.
**Diagnosis:** Middleware returned without awaiting `next()`.
**Fix:** Always `await next()` unless you intentionally short-circuit (auth fail, rate limit). Linting rule helps.
### `c.set` without typed Variables
**Symptom:** `c.get('user')` returns `unknown`; downstream code does string casts.
**Diagnosis:** Variables generic not declared.
**Fix:** Add `Variables` to the `Hono<{Bindings, Variables}>` generic. Now both `set` and `get` are typed.
### Heavy work in middleware
**Symptom:** Every endpoint, even health checks, costs 50ms.
**Diagnosis:** Middleware does DB-loaded user-loading or audit-logging on every path.
**Fix:** Scope middleware narrowly (`app.use('/api/v1/*', loadUser)`), or memoize per-request.
### SameSite=Strict on a redirect-driven login
**Symptom:** Browser shows 302 with Set-Cookie; immediate follow-up request to dashboard returns 403.
**Diagnosis:** Strict drops cookies on cross-context navigation in some browsers, including the auto-followed redirect from the login URL.
**Fix:** `SameSite=Lax` for session cookies that ride a redirect.
### Returning a raw `Response` and expecting middleware to mutate it
**Symptom:** Middleware "after `next()`" can't read body, can't set headers reliably.
**Diagnosis:** The middleware mutates `c.res`, but the handler returned a fresh Response that bypassed it.
**Fix:** Either use `c.res = newResponse;` in the handler, or return through `c.json/c.text/c.html/c.body`.
## Quality gates
- [ ] Every route has a validator (zod) or explicit typed parsing.
- [ ] Catch-all routes registered last; `app.notFound` set.
- [ ] Every middleware that intends to continue calls `await next()`.
- [ ] `Bindings` and `Variables` generics declared on the root app.
- [ ] `app.onError` returns a sanitized response — no stack leaks.
- [ ] CORS with `credentials: true` echoes a typed allowlist of origins, never `*`.
- [ ] Streaming endpoints check `stream.aborted` in any long loop.
- [ ] Cookie helpers used over raw Set-Cookie unless a specific reason.
## NOT for
- **Express/Fastify/Koa** — different middleware models, different ergonomics.
- **tRPC/GraphQL** — different paradigms; Hono's RPC is REST-flavored.
- **Next.js Route Handlers** — `app/api/*/route.ts` is a different framework.
- **Pure node http** — Hono's value is the middleware + typing; if you need neither, drop down. No dedicated skill.
- **Cloudflare Workers platform issues** (binding errors, deploy failures, secret upload) — once it's the platform, → `cloudflare-workers-debugging`.
- **Webhook signature verification on Hono routes** — Hono is the framework, not the protocol. → `webhook-receiver-design` for HMAC, raw-body, idempotency.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!