Back to skills
SKILL.md
Hono Idioms
ASecurityHono 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.
- 157 stars
- 0 votes
- 0 copies
- 2 views
- Added September 5, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add irahardianto/antigravity-setup --skill hono-idioms --agent claude-codeAre you the author of Hono Idioms?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/irahardianto-hono-idioms)---
name: hono-idioms
description: >-
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.md` whenever `hono` appears in `package.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
1. **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);
```
2. **Use `app.route('/prefix', subApp)` to compose sub-routers** — each feature exports its own `Hono` instance.
3. **`app.basePath('/api/v1')`** for API versioning — applied once at root level.
4. **`app.all('*', handler)`** for catch-all fallback routes.
5. **Path parameters** use `:name` syntax — accessed via `c.req.param('name')`.
### Context (`c`)
1. **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);
});
```
2. **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).
3. **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
1. **Built-in middleware** — `cors()`, `logger()`, `secureHeaders()`, `compress()`, `timing()`, `prettyJSON()` (each imported from `hono/<name>`):
```typescript
app.use('*', logger(), secureHeaders(), compress());
```
2. **Global vs scoped middleware:**
```typescript
// ✅ Global — applies to all routes
app.use('*', cors());
// ✅ Scoped — applies only to /api/* routes
app.use('/api/*', authMiddleware);
```
3. **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();
});
```
4. **Middleware ordering matters** — auth before route handlers, logging outermost.
### Validation
1. **`@hono/zod-validator` for 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);
});
```
2. **Validate all input sources:** `zValidator('json', ...)`, `zValidator('param', ...)`, `zValidator('query', ...)`, `zValidator('header', ...)`.
### Error Handling
1. **`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);
});
```
2. **`HTTPException` for typed HTTP errors:**
```typescript
// ✅ Throw in handlers or middleware — caught by onError
if (!task) throw new HTTPException(404, { message: `Task ${id} not found` });
```
3. **`app.notFound()` for custom 404 handling:**
```typescript
app.notFound((c) => c.json({ error: 'Not Found' }, 404));
```
### RPC Client
1. **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
1. **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);
```
2. **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.ts` co-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).
1. **`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' }),
});
```
2. **Mock services via dependency injection** — create the `Hono` app with test doubles in the test setup, not by mocking modules.
### Streaming
1. **`streamText` for 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);
}
})
);
```
2. **`streamSSE` for 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);
}
})
);
```
3. **`stream` for 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');
}
})
);
```
4. **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
Files in this skill
- SKILL.md
- references/project-structure.md
Attribution
Comments
Loading comments…