Files
asepharyana-hub-guide/skills/hono-backend/SKILL.md
T
asepharyana 5780d94ccf feat(hub-guide): language-agnostic auto-trigger across all skills
- Add principle 27: 'Language-Agnostic Auto-Trigger' to engineering-principles
- Remove hardcoded English-only keyword lists from all 26 skill descriptions
- Replace with concept-based detection: triggers from code context, project files,
  and file types regardless of spoken language
- Update detect-project.sh hook output with language-agnostic message
2026-07-25 12:59:43 +07:00

5.6 KiB

name, description
name description
hono-backend Hono best practices — middleware, RPC, validation (Zod), Drizzle integration, and project organization. Use when building Hono backend APIs. Triggers when working with this framework's files and patterns, not just explicit mentions.js," "hono RPC," "hono middleware," "hono Zod OpenAPI," "hono validation," or "hono Drizzle."

Hono Backend Best Practices

Project Structure

src/
├── modules/           # Feature modules
│   ├── users/
│   │   ├── routes.ts       # Hono routes (thin — validation + delegation)
│   │   ├── service.ts      # Business logic / use cases
│   │   └── repository.ts   # Data access (Drizzle queries)
│   └── orders/
├── middleware/         # Custom middleware
├── lib/               # Shared utilities (env, JWT, hashing)
├── db/                # Drizzle schema, migrations
└── index.ts           # App entry + composition root

Route Definition

import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
import { userService } from './service';
import type { Env } from '../lib/env';

const users = new Hono<Env>()
  .get('/', async (c) => {
    const page = Number(c.req.query('page') || '1');
    const limit = Number(c.req.query('limit') || '20');
    const result = await userService.list({ page, limit });
    return c.json(result);
  })
  .post('/', zValidator('json', CreateUserSchema), async (c) => {
    const body = c.req.valid('json');
    const user = await userService.create(body);
    return c.json(user, 201);
  })
  .get('/:id', async (c) => {
    const id = c.req.param('id');
    const user = await userService.findById(id);
    if (!user) return c.json({ error: 'Not found' }, 404);
    return c.json(user);
  });

export { users };

Zod Validation

import { z } from 'zod';

// Define at the boundary
export const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100).optional(),
  role: z.enum(['admin', 'user']).default('user'),
});

export const PaginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

// Infer types
export type CreateUserInput = z.infer<typeof CreateUserSchema>;

Hono RPC (Type-Safe Client)

// Server
import { hono } from 'hono';
import { routes } from './routes';

const app = new Hono().route('/api', routes);
export type App = typeof app;

// Client (no treaty needed — direct fetch wrapper)
import { hc } from 'hono/client';
import type { App } from '../server';

const client = hc<App>('http://localhost:3000');
const res = await client.api.users.$post({
  json: { email: 'test@example.com', name: 'Alice' },
});
// res is fully typed — status codes, response body

Middleware

import { Hono } from 'hono';

const app = new Hono()
  // Built-in
  .use('*', cors())
  .use('*', logger())

  // Custom
  .use('*', async (c, next) => {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    c.header('X-Response-Time', `${ms}ms`);
  })

  // Auth middleware
  .use('/api/*', async (c, next) => {
    const auth = c.req.header('Authorization');
    if (!auth?.startsWith('Bearer ')) return c.json({ error: 'Unauthorized' }, 401);
    const user = await verifyToken(auth.slice(7));
    if (!user) return c.json({ error: 'Invalid token' }, 401);
    c.set('user', user);
    await next();
  });

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); // log unexpected errors
  return c.json({ error: 'Internal server error' }, 500);
});

// In routes
app.post('/orders', async (c) => {
  const result = await orderService.create(body);
  if (!result.ok) {
    throw new HTTPException(422, { message: result.error.message });
  }
  return c.json(result.value, 201);
});

OpenAI / Swagger Integration

import { OpenAPIHono } from '@hono/zod-openapi';

const app = new OpenAPIHono();

app.openapi(
  createRoute({
    method: 'post',
    path: '/users',
    request: { body: { content: { 'application/json': { schema: CreateUserSchema } } } },
    responses: {
      201: { description: 'User created', content: { 'application/json': { schema: UserSchema } } },
      422: { description: 'Validation error' },
    },
  }),
  async (c) => {
    const body = c.req.valid('json');
    const user = await userService.create(body);
    return c.json(user, 201);
  },
);

Testing

import { describe, expect, it } from 'bun:test';
import { app } from '../src/index';

describe('users', () => {
  it('creates a user', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'test@example.com' }),
    });
    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.email).toBe('test@example.com');
  });
});

Anti-patterns

  • Business logic in route handlers — routes validate + delegate
  • No Zod validation on inputs — every route that accepts input must validate
  • c.req.raw instead of Hono's c.req.json/query/valid
  • Mixing Hono and Express middleware patterns
  • Global error handled only at route level — use app-level onError
  • Not using c.set for typed variables — declare Env type with bindings/variables