| name | hono-dev |
| description | This skill should be used when building APIs with Hono, using hc client, implementing OpenAPI, or when "Hono", "RPC", or "type-safe API" are mentioned. |
| metadata | {"version":"1.0.0"} |
Hono API Development
Route chaining → type-safe RPC → end-to-end types.
<when_to_use>
- Building REST APIs with Hono
- Type-safe RPC with hono/client
- OpenAPI documentation with Zod
- Testing APIs with testClient
- When user mentions "Hono", "RPC", or "OpenAPI"
NOT for: Bun runtime APIs (use bun-dev), other frameworks (Express, Fastify)
</when_to_use>
<version_notes>
Hono v4+ with @hono/zod-openapi v1.0+
Check hono.dev for latest patterns.
</version_notes>
Route Chaining — Critical Pattern
Type inference flows through method chain. Break chain = lose types.
<route_chaining>
const app = new Hono()
.get('/users', (c) => c.json({ users: [] }))
.get('/users/:id', (c) => {
const id = c.req.param('id');
return c.json({ id });
})
.post('/users', async (c) => {
const body = await c.req.json();
return c.json({ created: true }, 201);
});
export type AppType = typeof app;
❌ NEVER break the chain:
const app = new Hono();
app.get('/users', handler1);
app.post('/users', handler2);
Path parameters — typed automatically:
.get('/posts/:id/comments/:commentId', (c) => {
const { id, commentId } = c.req.param();
return c.json({ postId: id, commentId });
})
Query parameters — use Zod for validation:
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
});
const app = new Hono()
.get('/search', zValidator('query', QuerySchema), (c) => {
const { page, limit } = c.req.valid('query');
return c.json({ page, limit });
});
Middleware in chain:
const app = new Hono()
.use('*', logger())
.use('/api/*', cors())
.get('/api/public', (c) => c.json({ public: true }))
.use('/api/admin/*', authMiddleware)
.get('/api/admin/users', (c) => c.json({ users: [] }));
</route_chaining>
Factory Pattern — Context Typing
Use createFactory<Env>() to type context variables across middleware and routes.
<factory_pattern>
import { createFactory } from 'hono/factory';
import type { Database } from 'bun:sqlite';
type Env = {
Variables: {
user: { id: string; role: 'admin' | 'user' };
requestId: string;
db: Database;
};
};
const factory = createFactory<Env>();
const authMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) throw new HTTPException(401, { message: 'Unauthorized' });
const user = await verifyToken(token);
c.set('user', user);
await next();
});
const getProfile = factory.( {
user = c.();
c.({ user });
});
app = factory.()
.(, dbMiddleware)
.(, authMiddleware)
.(, ...getProfile);
= app;
Multi-module structure:
export const usersRoute = factory.createApp()
.get('/', (c) => c.json({ users: [] }))
.post('/', zValidator('json', CreateUserSchema), async (c) => {
const data = c.req.valid('json');
return c.json({ created: true }, 201);
});
const app = factory.createApp()
.use('*', dbMiddleware)
.route('/users', usersRoute)
.route('/posts', postsRoute);
See factory-pattern.md for advanced patterns.
</factory_pattern>
Error Handling
<error_handling>
import { HTTPException } from 'hono/http-exception';
app.get('/users/:id', async (c) => {
const user = await findUser(c.req.param('id'));
if (!user) {
throw new HTTPException(404, { message: 'User not found' });
}
return c.json({ user });
});
class NotFoundError extends HTTPException {
constructor(resource: string) {
super(404, { message: `${resource} not found` });
}
}
class UnauthorizedError extends HTTPException {
constructor(message = 'Unauthorized') {
super(401, { message });
}
}
app.onError((err, c) => {
if (err ) {
c.({ : err. }, err.);
}
(err ) {
c.({
: ,
: err..( ({ : i..(), : i. }))
}, );
}
isDev = .. !== ;
c.({ : isDev ? err. : }, );
});
app.( c.({ : , : c.. }, ));
See error-handling.md for patterns.
</error_handling>
Zod OpenAPI
<zod_openapi>
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
import { swaggerUI } from '@hono/swagger-ui';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
}).openapi('User');
const route = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: z.object({ id: z.string().uuid() }),
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'User found',
},
404: {
content: { 'application/json': { schema: z.object({ error: z.string() }) } },
: ,
},
},
: [],
: ,
});
app = ();
app.(route, {
{ id } = c..();
user = db.().(id);
(!user) c.({ : }, );
c.(user, );
});
app.(, ({ : }));
app.(, {
: ,
: { : , : },
});
See zod-openapi.md for complete patterns.
</zod_openapi>
RPC Client — End-to-End Types
<rpc_client>
const app = new Hono()
.get('/posts', (c) => c.json({ posts: [] }))
.get('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
.post('/posts', zValidator('json', CreatePostSchema), async (c) => {
const data = c.req.valid('json');
return c.json({ id: '123', ...data }, 201);
});
export type AppType = typeof app;
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
const res = await client..$get();
data = res.();
res2 = client.[].$get({ : { : } });
res3 = client..$post({
: { : , : }
});
res4 = client..$get({}, {
: { : }
});
</rpc_client>
Testing with testClient
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import { testClient } from 'hono/testing';
import { Database } from 'bun:sqlite';
import app from './server';
describe('API Tests', () => {
let db: Database;
beforeEach(() => {
db = new Database(':memory:');
db.run('CREATE TABLE posts (id TEXT PRIMARY KEY, title TEXT, content TEXT)');
});
afterEach(() => {
db.close();
});
const client = testClient(app);
test('GET /posts returns posts', async () => {
const res = await client.posts.$get();
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty('posts');
});
test('POST /posts creates post', () => {
res = client..$post({
: { : , : }
});
(res.).();
data = res.();
(data).({ : });
});
(, () => {
res = client...$get();
(res.).();
});
(, () => {
res = client...$get({}, {
: { : }
});
(res.).();
});
});
See testing-patterns.md for complete patterns.
Middleware Patterns
import { logger } from 'hono/logger';
app.use('*', logger());
import { cors } from 'hono/cors';
app.use('/api/*', cors({
origin: ['http://localhost:3000'],
credentials: true,
}));
const rateLimiter = factory.createMiddleware(async (c, next) => {
const ip = c.req.header('x-forwarded-for') || 'unknown';
const key = `rate:${ip}`;
const count = await cache.incr(key);
if (count === 1) await cache.expire(key, 60);
if (count > 100) {
throw new HTTPException(429, { message: 'Rate limit exceeded' });
}
await next();
});
const requestId = factory.createMiddleware( (c, next) => {
c.(, crypto.());
();
c...(, c.());
});
Rules
ALWAYS:
- Chain routes for type inference →
.get().post().put()
- Export
type AppType = typeof app for RPC client
- Use
createFactory<Env>() for typed context variables
- Validate with Zod schemas via
zValidator
- Handle errors with
HTTPException and centralized onError
- Test with
testClient for type safety
NEVER:
- Break method chain with variable assignment between routes
- Use
any types — let Hono infer or define explicitly
- Use
JSON.parse(await c.req.text()) — use c.req.json() or Zod validator
- Skip request validation on user input
- Expose stack traces in production
When Type Errors Occur:
- Check route chaining not broken
- Verify
export type AppType matches actual app
- Ensure middleware uses
createFactory for context types
- Check client using correct
param, query, or json keys
References
Examples:
References:
External: