| name | hono-idioms |
| description | Hono HTTP framework patterns — routing, middleware, Zod validation, RPC client. For TypeScript see typescript-idioms. |
| paths | ["**/wrangler.toml"] |
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.
Router and Route Organization
-
Create apps with new Hono() and method routing:
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 own Hono instance.
-
app.basePath('/api/v1') for API versioning — applied once at root level.
-
app.all('*', handler) for catch-all fallback routes.
-
Path parameters use :name syntax — accessed via c.req.param('name').
Context (c)
-
Response helpers — always use typed helpers, never raw Response:
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 — prefer zValidator instead).
-
Request-scoped typed variables with c.set() / c.get():
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') }));
Middleware
-
Built-in middleware — cors(), logger(), secureHeaders(), compress(), timing(), prettyJSON() (each imported from hono/<name>):
app.use('*', logger(), secureHeaders(), compress());
-
Global vs scoped middleware:
app.use('*', cors());
app.use('/api/*', authMiddleware);
-
Custom middleware with createMiddleware<>():
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-validator for type-safe request validation:
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']),
});
app.post('/tasks', zValidator('json', CreateTaskSchema), async (c) => {
const body = c.req.valid('json');
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:
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);
});
-
HTTPException for typed HTTP errors:
if (!task) throw new HTTPException(404, { message: `Task ${id} not found` });
-
app.notFound() for custom 404 handling:
app.notFound((c) => c.json({ error: 'Not Found' }, 404));
RPC Client
- End-to-end type-safe API calls — no code generation:
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;
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();
Multi-Runtime
-
Only the entry point differs — all handler/middleware code is runtime-agnostic:
-
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.
-
app.request() — test handlers without starting a server:
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);
});
});
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 Hono app with test doubles in the test setup, not by mocking modules.
Streaming
-
streamText for plain-text / LLM token streaming:
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);
}
})
);
-
streamSSE for Server-Sent Events:
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);
}
})
);
-
stream for binary or NDJSON (newline-delimited JSON):
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()) — use c.req.json() or zValidator('json', schema) for type-safe parsing
- ❌ Untyped context variables — always use
Hono<{ Variables: ... }> generics for c.set()/c.get()
- ❌ Ignoring middleware ordering — auth must run before route handlers; logging outermost
- ❌
app.use() without path scope — use app.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