LLMSkillHub
mirroredCopied from irahardianto/awesome-agv under MIT. irahardianto has not claimed this page, and is not affiliated with LLM SkillHub.

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.

0
SKILL.md
11.6 KB

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.

SituationReference to Load
Starting a Hono project or reviewing file layoutreferences/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