--- 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. - 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) Hono v4+ with @hono/zod-openapi v1.0+ Check hono.dev for latest patterns. ## Route Chaining — Critical Pattern Type inference flows through method chain. Break chain = lose types. ```typescript // ✅ Chained routes preserve types const app = new Hono() .get('/users', (c) => c.json({ users: [] })) .get('/users/:id', (c) => { const id = c.req.param('id'); // Typed! 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; // Full route types! ``` **❌ NEVER break the chain:** ```typescript const app = new Hono(); app.get('/users', handler1); // Types LOST! app.post('/users', handler2); ``` **Path parameters** — typed automatically: ```typescript .get('/posts/:id/comments/:commentId', (c) => { const { id, commentId } = c.req.param(); // Both string return c.json({ postId: id, commentId }); }) ``` **Query parameters** — use Zod for validation: ```typescript 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'); // Fully typed! return c.json({ page, limit }); }); ``` **Middleware in chain:** ```typescript 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: [] })); ``` ## Factory Pattern — Context Typing Use `createFactory()` to type context variables across middleware and routes. ```typescript 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(); // Typed middleware 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); // Type-checked! await next(); }); // Typed handlers const getProfile = factory.createHandlers((c) => { const user = c.get('user'); // Typed: { id: string; role: 'admin' | 'user' } return c.json({ user }); }); // Assemble app const app = factory.createApp() .use('*', dbMiddleware) .use('/api/*', authMiddleware) .get('/api/profile', ...getProfile); export type AppType = typeof app; ``` **Multi-module structure:** ```typescript // routes/users.ts 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); }); // index.ts const app = factory.createApp() .use('*', dbMiddleware) .route('/users', usersRoute) .route('/posts', postsRoute); ``` See [factory-pattern.md](references/factory-pattern.md) for advanced patterns. ## Error Handling ```typescript import { HTTPException } from 'hono/http-exception'; // Throw typed errors 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 }); }); // Custom error classes class NotFoundError extends HTTPException { constructor(resource: string) { super(404, { message: `${resource} not found` }); } } class UnauthorizedError extends HTTPException { constructor(message = 'Unauthorized') { super(401, { message }); } } // Centralized handler app.onError((err, c) => { if (err instanceof HTTPException) { return c.json({ error: err.message }, err.status); } if (err instanceof ZodError) { return c.json({ error: 'Validation failed', issues: err.issues.map(i => ({ path: i.path.join('.'), message: i.message })) }, 400); } const isDev = Bun.env.NODE_ENV !== 'production'; return c.json({ error: isDev ? err.message : 'Internal server error' }, 500); }); app.notFound((c) => c.json({ error: 'Not found', path: c.req.path }, 404)); ``` See [error-handling.md](references/error-handling.md) for patterns. ## Zod OpenAPI ```typescript 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() }) } }, description: 'User not found', }, }, tags: ['Users'], summary: 'Get user by ID', }); const app = new OpenAPIHono(); app.openapi(route, (c) => { const { id } = c.req.valid('param'); // Typed! const user = db.query('SELECT * FROM users WHERE id = ?').get(id); if (!user) return c.json({ error: 'User not found' }, 404); return c.json(user, 200); }); // Swagger UI app.get('/docs', swaggerUI({ url: '/openapi.json' })); app.doc('/openapi.json', { openapi: '3.1.0', info: { title: 'API', version: '1.0.0' }, }); ``` See [zod-openapi.md](references/zod-openapi.md) for complete patterns. ## RPC Client — End-to-End Types ```typescript // Server 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; // Client import { hc } from 'hono/client'; import type { AppType } from './server'; const client = hc('http://localhost:3000'); // GET request const res = await client.posts.$get(); const data = await res.json(); // Typed: { posts: any[] } // GET with params const res2 = await client.posts[':id'].$get({ param: { id: '123' } }); // POST request const res3 = await client.posts.$post({ json: { title: 'Hello', content: 'World' } }); // With headers const res4 = await client.posts.$get({}, { headers: { Authorization: 'Bearer token' } }); ``` ## Testing with testClient ```typescript 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', async () => { const res = await client.posts.$post({ json: { title: 'Test', content: 'Content' } }); expect(res.status).toBe(201); const data = await res.json(); expect(data).toMatchObject({ title: 'Test' }); }); test('Protected route requires auth', async () => { const res = await client.api.profile.$get(); expect(res.status).toBe(401); }); test('Protected route accepts valid token', async () => { const res = await client.api.profile.$get({}, { headers: { Authorization: 'Bearer valid-token' } }); expect(res.status).toBe(200); }); }); ``` See [testing-patterns.md](examples/testing-patterns.md) for complete patterns. ## Middleware Patterns ```typescript // Logging import { logger } from 'hono/logger'; app.use('*', logger()); // CORS import { cors } from 'hono/cors'; app.use('/api/*', cors({ origin: ['http://localhost:3000'], credentials: true, })); // Rate limiting 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(); }); // Request ID const requestId = factory.createMiddleware(async (c, next) => { c.set('requestId', crypto.randomUUID()); await next(); c.res.headers.set('x-request-id', c.get('requestId')); }); ``` ## Rules **ALWAYS:** - Chain routes for type inference → `.get().post().put()` - Export `type AppType = typeof app` for RPC client - Use `createFactory()` 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:** - [typed-routes.md](examples/typed-routes.md) — Complete route chaining examples - [testing-patterns.md](examples/testing-patterns.md) — Testing with testClient **References:** - [factory-pattern.md](references/factory-pattern.md) — Context typing with createFactory - [zod-openapi.md](references/zod-openapi.md) — OpenAPI integration patterns - [error-handling.md](references/error-handling.md) — HTTPException and error patterns - [middleware.md](references/middleware.md) — Auth, logging, CORS patterns **External:** - Hono: - @hono/zod-openapi: