irahardianto / hono-idioms
Hono lightweight web framework patterns: type-safe route handlers, middleware composition, Zod validation, and RPC clients for Cloudflare Workers, Node, or Bun. Use when building Hono APIs. Pair with typescript-idioms.
Hono Idioms and Patterns
Core Philosophy
Hono rewards thin handlers, middleware composition, multi-runtime portability, and end-to-end type safety. Idiomatic Hono = typed routes, validator middleware, RPC client — no code generation needed.
Scope: Hono-specific patterns. For TypeScript fundamentals: @.agents/skills/typescript-idioms/SKILL.md. For project structure: @.agents/skills/hono-idioms/references/project-structure.md.
Loading guard / auto-detection: Hono has no canonical marker file, so it is not reliably auto-detected by file glob (only
wrangler.toml— the Cloudflare Workers case — triggers this skill directly). For Node.js / Bun / Deno Hono projects, co-load this skill alongside@.agents/skills/typescript-idioms/SKILL.mdwheneverhonoappears inpackage.json. Do NOT load this skill for Express, Fastify, or NestJS — it is Hono-only.
When to Load References
Load these before writing code in the matching context — not after.
| Situation | Reference to Load |
|---|---|
| Starting a Hono project or reviewing file layout | references/project-structure.md |
| TypeScript type system, async, Zod, error types | @.agents/skills/typescript-idioms/SKILL.md (always co-load) |
Zod schemas / boundary validation (zValidator) | @.agents/skills/typescript-idioms/references/zod-patterns.md |
| Async / I/O / coercion / security pitfalls | @.agents/skills/typescript-idioms/references/ts-patterns-and-anti-patterns.md |
| Shared tooling (ESLint, Prettier, Vitest, tsconfig) | @.agents/skills/typescript-idioms/references/recommended-dependencies.md |
Router and Route Organization
Create apps with
new Hono()and method routing:`typescript
// ✅ Group by resource, compose with app.route()
const tasks = new Hono()
.get('/', listTasks)
.post('/', createTask)
.get('/:id', getTask)
.put('/:id', updateTask)
.delete('/:id', deleteTask);const app = new Hono()
.route('/api/tasks', tasks)
.route('/api/users', users);`- Use
app.route('/prefix', subApp)to compose sub-routers — each feature exports its ownHonoinstance. app.basePath('/api/v1')for API versioning — applied once at root level.app.all('*', handler)for catch-all fallback routes.- Path parameters use
:namesyntax — accessed viac.req.param('name').
Context (c)
Response helpers — always use typed helpers, never raw
Response:`typescript
// ✅ c.json(), c.text(), c.html(), c.redirect(), c.notFound()
app.get('/tasks/:id', async (c) => {
const task = await taskService.find(c.req.param('id'));
return c.json(task);
});`- Request data:
c.req.param('id')(path),c.req.query('page')(query string),c.req.header('Authorization')(header),await c.req.json()(body — preferzValidatorinstead). Request-scoped typed variables with
c.set()/c.get():`typescript
type Env = { Variables: { userId: string; requestId: string } };
const app = new Hono<Env>();app.use(async (c, next) => {
c.set('requestId', crypto.randomUUID());
await next();
});
app.get('/me', (c) => c.json({ id: c.get('userId') })); // fully typed`
Middleware
Built-in middleware —
cors(),logger(),secureHeaders(),compress(),timing(),prettyJSON()(each imported fromhono/<name>):`typescript
app.use('*', logger(), secureHeaders(), compress());`Global vs scoped middleware:
`typescript
// ✅ Global — applies to all routes
app.use('*', cors());// ✅ Scoped — applies only to /api/* routes
app.use('/api/*', authMiddleware);`Custom middleware with
createMiddleware<>():`typescript
import { createMiddleware } from 'hono/factory';type AuthEnv = { Variables: { userId: string } };
const authMiddleware = createMiddleware<AuthEnv>(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token) throw new HTTPException(401, { message: 'Unauthorized' });
const payload = await verifyToken(token);
c.set('userId', payload.sub);
await next();
});`- Middleware ordering matters — auth before route handlers, logging outermost.
Validation
@hono/zod-validatorfor type-safe request validation:`typescript
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
priority: z.enum(['low', 'medium', 'high']),
});// ✅ Compose multiple validators — validated data is fully typed
app.post('/tasks', zValidator('json', CreateTaskSchema), async (c) => {
const body = c.req.valid('json'); // { title: string; priority: 'low'|'medium'|'high' }
const task = await taskService.create(body);
return c.json(task, 201);
});app.get('/tasks/:id', zValidator('param', z.object({ id: z.string().uuid() })), async (c) => {
const task = await taskService.find(c.req.valid('param').id);
return c.json(task);
});`- Validate all input sources:
zValidator('json', ...),zValidator('param', ...),zValidator('query', ...),zValidator('header', ...).
Error Handling
app.onError()for global error handling:`typescript
import { HTTPException } from 'hono/http-exception';app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
console.error(err);
return c.json({ error: 'Internal Server Error' }, 500);
});`HTTPExceptionfor typed HTTP errors:`typescript
// ✅ Throw in handlers or middleware — caught by onError
if (!task) throw new HTTPException(404, { message:Task ${id} not found});`app.notFound()for custom 404 handling:`typescript
app.notFound((c) => c.json({ error: 'Not Found' }, 404));`
RPC Client
End-to-end type-safe API calls — no code generation:
`typescript
// server.ts — chain routes and export the type
const routes = app
.get('/tasks', async (c) => c.json(await taskService.list()))
.post('/tasks', zValidator('json', CreateTaskSchema), async (c) => {
return c.json(await taskService.create(c.req.valid('json')), 201);
});
export type AppType = typeof routes;// client.ts — fully typed, changes propagate compile errors automatically
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
const res = await client.tasks.$post({ json: { title: 'New', priority: 'high' } });
const task = await res.json(); // fully typed`
Multi-Runtime
Only the entry point differs — all handler/middleware code is runtime-agnostic:
`typescript
// Node.js: serve({ fetch: app.fetch, port: 3000 }) // @hono/node-server
// Bun / CF Workers: export default app;
// Deno: Deno.serve(app.fetch);`- Never use runtime-specific APIs in handlers — isolate them in
platform/adapters.
Testing
For universal testing principles, see
.agents/rules/testing-strategy.md. Below: Hono-specific patterns only.
Test-file naming:*.test.tsco-located next to source (matches the generic-TS convention; see@.agents/skills/typescript-idioms/references/project-structure.md§Test Organization for the cross-framework rule).
app.request()— test handlers without starting a server:`typescript
import { describe, it, expect } from 'vitest';
import { app } from './app';describe('GET /api/tasks/:id', () => {
it('returns 200 with task data', async () => {
const res = await app.request('/api/tasks/abc-123');
expect(res.status).toBe(200);
expect((await res.json()).id).toBe('abc-123');
});it('returns 404 for unknown task', async () => {
const res = await app.request('/api/tasks/unknown');
expect(res.status).toBe(404);
});
});// POST with body
const res = await app.request('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test', priority: 'high' }),
});`- Mock services via dependency injection — create the
Honoapp with test doubles in the test setup, not by mocking modules.
Streaming
streamTextfor plain-text / LLM token streaming:`typescript
import { streamText } from 'hono/streaming';app.get('/stream/tokens', (c) =>
streamText(c, async (stream) => {
stream.onAbort(() => console.log('client disconnected'));
for (const chunk of ['Hello', ' ', 'World']) {
await stream.write(chunk);
await stream.sleep(100);
}
})
);`streamSSEfor Server-Sent Events:`typescript
import { streamSSE } from 'hono/streaming';app.get('/sse', (c) =>
streamSSE(c, async (stream) => {
stream.onAbort(() => cleanup());
let id = 0;
while (true) {
await stream.writeSSE({
data: JSON.stringify({ ts: Date.now() }),
event: 'tick',
id: String(id++),
});
await stream.sleep(1000);
}
})
);`streamfor binary or NDJSON (newline-delimited JSON):`typescript
import { stream } from 'hono/streaming';app.get('/stream/tasks', (c) =>
stream(c, async (stream) => {
stream.onAbort(() => cleanup());
const tasks = await taskService.list();
for (const task of tasks) {
await stream.write(JSON.stringify(task) + '\n');
}
})
);`- Always call
stream.onAbort()to release resources when the client disconnects — without it, the generator keeps running after the connection drops.
Anti-Patterns
- ❌ Business logic in handlers — extract to a service/logic layer; handlers only validate, delegate, respond
- ❌ Manual
JSON.parse(await c.req.text())— usec.req.json()orzValidator('json', schema)for type-safe parsing - ❌ Untyped context variables — always use
Hono<{ Variables: ... }>generics forc.set()/c.get() - ❌ Ignoring middleware ordering — auth must run before route handlers; logging outermost
- ❌
app.use()without path scope — useapp.use('/api/*', ...)when middleware should be scoped, not global - ❌ Runtime-specific code in handlers — use adapters at the entry point only; handlers must be portable
Formatting and Static Analysis
Same tooling as TypeScript. See @.agents/skills/typescript-idioms/SKILL.md.
Related
- Code Idioms and Conventions @.agents/rules/code-idioms-and-conventions.md
- TypeScript Idioms @.agents/skills/typescript-idioms/SKILL.md
- API Design Principles @.agents/rules/api-design-principles.md
- Security Principles @.agents/rules/security-principles.md
- Error Handling Principles @.agents/rules/error-handling-principles.md
- Architectural Patterns @.agents/rules/architectural-pattern.md
- Testing Strategy @.agents/rules/testing-strategy.md
- Logging and Observability Mandate @.agents/rules/logging-and-observability-mandate.md
- Logging Implementation @.agents/skills/logging-implementation/SKILL.md