Files
asepharyana-hub-guide/skills/hono-backend/SKILL.md
T

5.4 KiB

name, description
name description
hono-backend Use when building Hono backend APIs — middleware, RPC, Zod validation, Drizzle integration, and project organization. Triggers from Hono file patterns and project config.

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