| name | hono |
| description | Use when building Hono web applications deployed to Vercel, or when the user asks about Hono APIs, routing, middleware, JSX, validation, testing, or streaming. TRIGGER when code imports from 'hono' or 'hono/*', or user mentions Hono. Use `npx hono request` to test endpoints. |
Hono Skill
Build Hono web applications and deploy them to Vercel with zero configuration. This skill provides inline API knowledge for AI. Use npx hono request to test endpoints. If the hono-docs MCP server is configured, prefer its tools for the latest documentation over the inline reference.
Hono CLI Usage
Request Testing
Test endpoints without starting an HTTP server. Uses app.request() internally.
npx hono request [file] -P /path
npx hono request [file] -X POST -P /api/users -d '{"name": "test"}'
Note: Do not pass credentials directly in CLI arguments. Use environment variables for sensitive values.
Deploying Hono on Vercel
Hono apps deploy to Vercel with zero configuration. Your server routes automatically become Vercel Functions using Fluid compute by default.
File Placement
Export your Hono app as the default export from any of these locations:
app.{js,cjs,mjs,ts,cts,mts}
index.{js,cjs,mjs,ts,cts,mts}
server.{js,cjs,mjs,ts,cts,mts}
src/app.{js,cjs,mjs,ts,cts,mts}
src/index.{js,cjs,mjs,ts,cts,mts}
src/server.{js,cjs,mjs,ts,cts,mts}
import { Hono } from 'hono'
const app = new Hono()
export default app
No Node.js adapter (@hono/node-server) is needed for Vercel — just use the default export. Use @hono/node-server only for local development if not using vc dev.
Local Development
Use Vercel CLI to run locally with the same behavior as production:
vc dev
Static Assets
Place static files in the public/ directory. They are served via the Vercel CDN with default headers.
Important: Hono's serveStatic() is ignored on Vercel — use the public/ directory instead.
Streaming
Vercel Functions support streaming, which works with Hono's stream(), streamText(), and streamSSE() helpers out of the box.
Middleware: Hono vs Vercel
- Hono Middleware — Runs inside your app before route handlers (CORS, auth, logging, etc.)
- Vercel Routing Middleware — Runs at the edge before your app is invoked (rewrites, redirects, headers). See Vercel Routing Middleware docs.
Both can be used together.
Hono API Reference
App Constructor
import { Hono } from 'hono'
const app = new Hono()
type Env = {
Variables: { user: User }
}
const app = new Hono<Env>()
Routing Methods
app.get('/path', handler)
app.post('/path', handler)
app.put('/path', handler)
app.delete('/path', handler)
app.patch('/path', handler)
app.options('/path', handler)
app.all('/path', handler)
app.on('PURGE', '/path', handler)
app.on(['PUT', 'DELETE'], '/path', handler)
Routing Patterns
app.get('/user/:name', (c) => {
const name = c.req.param('name')
return c.json({ name })
})
app.get('/posts/:id/comments/:commentId', (c) => {
const { id, commentId } = c.req.param()
})
app.get('/api/animal/:type?', (c) => c.text('Animal!'))
app.get('/wild/*/card', (c) => c.text('Wildcard'))
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
const { date, title } = c.req.param()
})
app
.get('/endpoint', (c) => c.text('GET'))
.post((c) => c.text('POST'))
.delete((c) => c.text('DELETE'))
Route Grouping
const api = new Hono()
api.get('/users', (c) => c.json([]))
const app = new Hono()
app.route('/api', api)
const app = new Hono().basePath('/api')
app.get('/users', (c) => c.json([]))
Error Handling
app.notFound((c) => c.json({ message: 'Not Found' }, 404))
app.onError((err, c) => {
console.error(err)
return c.json({ message: 'Internal Server Error' }, 500)
})
Context (c)
Response Methods
c.text('Hello')
c.json({ message: 'Hello' })
c.html('<h1>Hello</h1>')
c.redirect('/new-path')
c.redirect('/new-path', 301)
c.body('raw body', 200, headers)
c.notFound()
Headers & Status
c.status(201)
c.header('X-Custom', 'value')
c.header('Cache-Control', 'no-store')
Variables (request-scoped data)
c.set('user', { id: 1, name: 'Alice' })
const user = c.get('user')
const user = c.var.user
Environment Variables
On Vercel, use process.env to access environment variables:
const apiKey = process.env.API_KEY
Renderer
app.use(async (c, next) => {
c.setRenderer((content) =>
c.html(
<html><body>{content}</body></html>
)
)
await next()
})
app.get('/', (c) => c.render(<h1>Hello</h1>))
HonoRequest (c.req)
c.req.param('id')
c.req.param()
c.req.query('page')
c.req.query()
c.req.queries('tags')
c.req.header('Authorization')
c.req.header()
await c.req.json()
await c.req.text()
await c.req.formData()
await c.req.parseBody()
await c.req.arrayBuffer()
await c.req.blob()
c.req.valid('json')
c.req.valid('query')
c.req.valid('form')
c.req.valid('param')
c.req.url
c.req.path
c.req.method
c.req.raw
Middleware
Using Built-in Middleware
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { basicAuth } from 'hono/basic-auth'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
import { etag } from 'hono/etag'
import { compress } from 'hono/compress'
import { poweredBy } from 'hono/powered-by'
import { timing } from 'hono/timing'
import { cache } from 'hono/cache'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
import { csrf } from 'hono/csrf'
import { ipRestriction } from 'hono/ip-restriction'
import { bodyLimit } from 'hono/body-limit'
import { requestId } from 'hono/request-id'
import { methodOverride } from 'hono/method-override'
import { trailingSlash, trimTrailingSlash } from 'hono/trailing-slash'
app.use(logger())
app.use('/api/*', cors())
app.post('/api/*', basicAuth({ username: 'admin', password: 'secret' }))
Custom Middleware
app.use(async (c, next) => {
const start = Date.now()
await next()
const elapsed = Date.now() - start
c.res.headers.set('X-Response-Time', `${elapsed}ms`)
})
import { createMiddleware } from 'hono/factory'
const auth = createMiddleware(async (c, next) => {
const token = c.req.header('Authorization')
if (!token) return c.json({ error: 'Unauthorized' }, 401)
await next()
})
app.use('/api/*', auth)
Middleware Execution Order
Middleware executes in registration order. await next() calls the next middleware/handler, and code after next() runs on the way back:
Request → mw1 before → mw2 before → handler → mw2 after → mw1 after → Response
app.use(async (c, next) => {
await next()
})
Validation
Validation targets: json, form, query, header, param, cookie.
Zod Validator
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1),
body: z.string()
})
app.post('/posts', zValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json(data, 201)
})
Valibot / Standard Schema Validator
import { sValidator } from '@hono/standard-validator'
import * as v from 'valibot'
const schema = v.object({ name: v.string(), age: v.number() })
app.post('/users', sValidator('json', schema), (c) => {
const data = c.req.valid('json')
return c.json(data, 201)
})
JSX
Setup
In tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
Or use pragma: /** @jsxImportSource hono/jsx */
Important: Files using JSX must have a .tsx extension. Rename .ts to .tsx or the compiler will fail.
Components
import type { PropsWithChildren } from 'hono/jsx'
const Layout = (props: PropsWithChildren) => (
<html>
<head>
<title>My App</title>
</head>
<body>{props.children}</body>
</html>
)
const UserCard = ({ name }: { name: string }) => (
<div class="card">
<h2>{name}</h2>
</div>
)
app.get('/', (c) => {
return c.html(
<Layout>
<UserCard name="Alice" />
</Layout>
)
})
jsxRenderer Middleware
Use jsxRenderer middleware for layouts. See npx hono docs /docs/middleware/builtin/jsx-renderer for details.
Async Components
const UserList = async () => {
const users = await fetchUsers()
return (
<ul>
{users.map((u) => (
<li>{u.name}</li>
))}
</ul>
)
}
Fragments
const Items = () => (
<>
<li>Item 1</li>
<li>Item 2</li>
</>
)
Streaming
import { stream, streamText, streamSSE } from 'hono/streaming'
app.get('/stream', (c) => {
return stream(c, async (stream) => {
stream.onAbort(() => console.log('Aborted'))
await stream.write(new Uint8Array([0x48, 0x65]))
await stream.pipe(readableStream)
})
})
app.get('/stream-text', (c) => {
return streamText(c, async (stream) => {
await stream.writeln('Hello')
await stream.sleep(1000)
await stream.write('World')
})
})
app.get('/sse', (c) => {
return streamSSE(c, async (stream) => {
let id = 0
while (true) {
await stream.writeSSE({
data: JSON.stringify({ time: new Date().toISOString() }),
event: 'time-update',
id: String(id++)
})
await stream.sleep(1000)
}
})
})
Testing with app.request()
Test endpoints without starting an HTTP server:
const res = await app.request('/posts')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ posts: [] })
const res = await app.request('/posts', {
method: 'POST',
body: JSON.stringify({ title: 'Hello' }),
headers: { 'Content-Type': 'application/json' }
})
const formData = new FormData()
formData.append('name', 'Alice')
const res = await app.request('/users', { method: 'POST', body: formData })
const req = new Request('http://localhost/api', { method: 'DELETE' })
const res = await app.request(req)
Hono Client (RPC)
Type-safe API client using shared types between server and client.
IMPORTANT: Routes MUST be chained for type inference to work. Without chaining, the client cannot infer route types.
const route = app
.post('/posts', zValidator('json', schema), (c) => {
return c.json({ ok: true }, 201)
})
.get('/posts', (c) => {
return c.json({ posts: [] })
})
export type AppType = typeof route
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$post({ json: { title: 'Hello' } })
const data = await res.json()
Type utilities:
import type { InferRequestType, InferResponseType } from 'hono/client'
type ReqType = InferRequestType<typeof client.posts.$post>
type ResType = InferResponseType<typeof client.posts.$post, 200>
Helpers
Helpers are utility functions imported from hono/<helper-name>:
import { getConnInfo } from 'hono/conninfo'
import { getCookie, setCookie, deleteCookie } from 'hono/cookie'
import { css, Style } from 'hono/css'
import { createFactory } from 'hono/factory'
import { html, raw } from 'hono/html'
import { stream, streamText, streamSSE } from 'hono/streaming'
import { testClient } from 'hono/testing'
Available helpers: Accepts, Adapter, ConnInfo, Cookie, css, Dev, Factory, html, JWT, Proxy, Route, SSG, Streaming, Testing.
For details, use npx hono docs /docs/helpers/<helper-name>.
Factory
Use createFactory to define Env once and share it across app, middleware, and handlers:
import { createFactory } from 'hono/factory'
const factory = createFactory<Env>()
const app = factory.createApp()
const mw = factory.createMiddleware(async (c, next) => {
await next()
})
const handlers = factory.createHandlers(logger(), (c) => c.json({ message: 'Hello' }))
app.get('/api', ...handlers)
Best Practices
- Write handlers inline in route definitions for proper type inference of path params.
- Use
app.route() to organize large apps by feature, not Rails-style controllers.
- Use
createFactory() to share Env type across app, middleware, and handlers.
- Use
c.set()/c.get() to pass data between middleware and handlers.
- Chain validators for multiple request parts (param + query + json).
- Export app type for RPC:
export type AppType = typeof routes
- Use
app.request() for testing — no server startup needed.
Vercel Deployment
On Vercel, just use the default export — no adapter needed:
export default app
For local development without vc dev, you can use the Node.js adapter:
import { serve } from '@hono/node-server'
serve(app)
Keep this in a separate file (e.g., src/lib/local.ts) so it doesn't affect the Vercel deployment.