Hono
Ultrafast web framework for the edge, supporting Cloudflare Workers, Deno, Bun, Vercel, AWS Lambda, and Node.js with zero dependencies.
Installation
Section titled “Installation”Create New Project
Section titled “Create New Project”| Command | Description |
|---|---|
npm create hono@latest my-app | Create new Hono project with npm |
yarn create hono my-app | Create with Yarn |
pnpm create hono my-app | Create with pnpm |
bun create hono my-app | Create with Bun |
deno init --lib hono | Create with Deno |
Add to Existing Project
Section titled “Add to Existing Project”| Command | Description |
|---|---|
npm install hono | Install Hono in existing project |
yarn add hono | Install with Yarn |
pnpm add hono | Install with pnpm |
bun add hono | Install with Bun |
Starter Templates
Section titled “Starter Templates”# Create project with specific runtime template
npm create hono@latest my-app -- --template cloudflare-workers
npm create hono@latest my-app -- --template bun
npm create hono@latest my-app -- --template deno
npm create hono@latest my-app -- --template nodejs
npm create hono@latest my-app -- --template vercel
npm create hono@latest my-app -- --template aws-lambda
npm create hono@latest my-app -- --template cloudflare-pages
npm create hono@latest my-app -- --template fastly
Basic Routing
Section titled “Basic Routing”HTTP Methods
Section titled “HTTP Methods”| Command | Description |
|---|---|
app.get('/path', handler) | Handle GET requests |
app.post('/path', handler) | Handle POST requests |
app.put('/path', handler) | Handle PUT requests |
app.delete('/path', handler) | Handle DELETE requests |
app.patch('/path', handler) | Handle PATCH requests |
app.options('/path', handler) | Handle OPTIONS requests |
app.all('/path', handler) | Handle all HTTP methods |
app.on('PURGE', '/path', handler) | Handle custom methods |
Hello World
Section titled “Hello World”import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.get('/json', (c) => {
return c.json({ message: 'Hello', status: 'ok' })
})
app.get('/html', (c) => {
return c.html('<h1>Hello Hono!</h1>')
})
export default app
Route Parameters
Section titled “Route Parameters”// Path parameters
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param()
return c.json({ postId, commentId })
})
// Optional parameter
app.get('/api/:version?/users', (c) => {
const version = c.req.param('version') || 'v1'
return c.json({ version })
})
// Wildcard
app.get('/files/*', (c) => {
const path = c.req.path
return c.text(`File path: ${path}`)
})
// Regex-like patterns
app.get('/user/:id{[0-9]+}', (c) => {
const id = c.req.param('id')
return c.json({ id: Number(id) })
})
Request Handling
Section titled “Request Handling”Query Parameters
Section titled “Query Parameters”| Command | Description |
|---|---|
c.req.query('key') | Get single query parameter |
c.req.query() | Get all query parameters |
c.req.queries('tags') | Get array of values for key |
Request Body
Section titled “Request Body”| Command | Description |
|---|---|
c.req.json() | Parse JSON body |
c.req.text() | Get body as text |
c.req.formData() | Parse form data |
c.req.blob() | Get body as Blob |
c.req.arrayBuffer() | Get body as ArrayBuffer |
c.req.parseBody() | Auto-parse body by content type |
Headers & Metadata
Section titled “Headers & Metadata”| Command | Description |
|---|---|
c.req.header('Content-Type') | Get request header |
c.req.header() | Get all headers |
c.req.method | Get HTTP method |
c.req.url | Get full request URL |
c.req.path | Get request path |
c.req.raw | Get raw Request object |
c.req.valid('json') | Get validated data |
Request Example
Section titled “Request Example”app.post('/users', async (c) => {
const body = await c.req.json()
const page = c.req.query('page') || '1'
const auth = c.req.header('Authorization')
return c.json({
user: body,
page: Number(page),
authenticated: !!auth
}, 201)
})
Response Helpers
Section titled “Response Helpers”Response Types
Section titled “Response Types”| Command | Description |
|---|---|
c.text('Hello') | Plain text response |
c.json({ key: 'value' }) | JSON response |
c.html('<h1>Hi</h1>') | HTML response |
c.redirect('/new-url') | 302 redirect |
c.redirect('/new-url', 301) | 301 permanent redirect |
c.notFound() | 404 response |
c.body(data) | Raw body response |
c.newResponse(body, status, headers) | Full custom response |
Response Headers & Streaming
Section titled “Response Headers & Streaming”// Set headers
app.get('/api/data', (c) => {
c.header('X-Custom-Header', 'value')
c.header('Cache-Control', 'max-age=3600')
return c.json({ data: 'example' })
})
// Set status code
app.post('/items', async (c) => {
const item = await c.req.json()
return c.json(item, 201)
})
// Stream response
app.get('/stream', (c) => {
return c.streamText(async (stream) => {
await stream.write('Hello ')
await stream.sleep(1000)
await stream.write('World!')
})
})
Middleware
Section titled “Middleware”Built-in Middleware
Section titled “Built-in Middleware”| Command | Description |
|---|---|
import { cors } from 'hono/cors' | CORS middleware |
import { logger } from 'hono/logger' | Request logging |
import { prettyJSON } from 'hono/pretty-json' | Pretty-print JSON responses |
import { basicAuth } from 'hono/basic-auth' | Basic authentication |
import { bearerAuth } from 'hono/bearer-auth' | Bearer token auth |
import { jwt } from 'hono/jwt' | JWT authentication |
import { compress } from 'hono/compress' | Gzip/Brotli compression |
import { etag } from 'hono/etag' | ETag caching |
import { secureHeaders } from 'hono/secure-headers' | Security headers |
import { csrf } from 'hono/csrf' | CSRF protection |
import { timing } from 'hono/timing' | Server-Timing header |
import { cache } from 'hono/cache' | Cache control |
import { bodyLimit } from 'hono/body-limit' | Request body size limit |
Middleware Usage
Section titled “Middleware Usage”import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { bearerAuth } from 'hono/bearer-auth'
const app = new Hono()
// Global middleware
app.use('*', logger())
app.use('*', secureHeaders())
app.use('*', cors({
origin: ['https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
}))
// Route-specific middleware
app.use('/api/*', bearerAuth({ token: 'my-secret-token' }))
// Custom middleware
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
c.header('X-Response-Time', `${duration}ms`)
})
JWT Authentication
Section titled “JWT Authentication”import { jwt, sign } from 'hono/jwt'
const app = new Hono()
const SECRET = 'my-secret-key'
app.use('/api/*', jwt({ secret: SECRET }))
app.post('/login', async (c) => {
const { email, password } = await c.req.json()
const token = await sign(
{ email, exp: Math.floor(Date.now() / 1000) + 3600 },
SECRET
)
return c.json({ token })
})
app.get('/api/profile', (c) => {
const payload = c.get('jwtPayload')
return c.json({ email: payload.email })
})
Validation
Section titled “Validation”Zod Validator
Section titled “Zod Validator”import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
})
app.post(
'/users',
zValidator('json', userSchema),
(c) => {
const user = c.req.valid('json')
return c.json(user, 201)
}
)
// Validate query parameters
const querySchema = z.object({
page: z.string().optional().default('1'),
limit: z.string().optional().default('10'),
})
app.get(
'/users',
zValidator('query', querySchema),
(c) => {
const { page, limit } = c.req.valid('query')
return c.json({ page, limit })
}
)
Routing Groups
Section titled “Routing Groups”const app = new Hono()
// Group routes with basePath
const api = new Hono().basePath('/api')
api.get('/users', (c) => c.json({ users: [] }))
api.post('/users', (c) => c.json({ created: true }))
app.route('/', api)
// Versioned APIs
const v1 = new Hono()
v1.get('/health', (c) => c.json({ status: 'ok' }))
const v2 = new Hono()
v2.get('/health', (c) => c.json({ status: 'ok', version: 2 }))
app.route('/api/v1', v1)
app.route('/api/v2', v2)
Error Handling
Section titled “Error Handling”import { HTTPException } from 'hono/http-exception'
// Custom 404
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
// Throw HTTP exceptions
app.get('/admin', (c) => {
if (!c.req.header('Authorization')) {
throw new HTTPException(401, { message: 'Unauthorized' })
}
return c.json({ admin: true })
})
Runtime-Specific Setup
Section titled “Runtime-Specific Setup”Cloudflare Workers
Section titled “Cloudflare Workers”import { Hono } from 'hono'
type Bindings = {
MY_KV: KVNamespace
MY_DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/kv/:key', async (c) => {
const value = await c.env.MY_KV.get(c.req.param('key'))
return c.json({ value })
})
export default app
Node.js
Section titled “Node.js”import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))
serve({ fetch: app.fetch, port: 3000 })
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Bun!'))
export default { port: 3000, fetch: app.fetch }
import { Hono } from 'npm:hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Deno!'))
Deno.serve(app.fetch)
RPC & Client
Section titled “RPC & Client”Type-Safe RPC Client
Section titled “Type-Safe RPC Client”// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
.get('/api/users', (c) => {
return c.json({ users: [{ id: 1, name: 'Alice' }] })
})
.post(
'/api/users',
zValidator('json', z.object({ name: z.string() })),
(c) => {
const { name } = c.req.valid('json')
return c.json({ id: 2, name }, 201)
}
)
export type AppType = typeof app
export default app
// client.ts — fully typed
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:3000')
// Fully typed — autocomplete for routes + responses
const res = await client.api.users.$get()
const data = await res.json()
// data: { users: { id: number, name: string }[] }
Testing
Section titled “Testing”import { testClient } from 'hono/testing'
const app = new Hono()
.get('/hello', (c) => c.json({ message: 'Hello!' }))
// Using testClient (type-safe)
const client = testClient(app)
const res = await client.hello.$get()
const body = await res.json()
// body.message === 'Hello!'
// Using app.request() directly
const res2 = await app.request('/hello')
// res2.status === 200
Best Practices
Section titled “Best Practices”-
Use TypeScript generics for env bindings — Define
Hono<{ Bindings: MyBindings }>to get type safety for environment variables, KV namespaces, and D1 databases. -
Validate all inputs with Zod — Use
@hono/zod-validatorfor request body, query, and param validation. This gives runtime safety and TypeScript types in one step. -
Use the RPC client for frontend-backend — Export
AppTypefrom your server and usehc<AppType>()on the client for end-to-end type safety without code generation. -
Apply middleware with path patterns — Use
app.use('/api/*', middleware)to scope middleware to specific routes rather than applying everything globally. -
Group routes with
new Hono().basePath()— Organize related endpoints into separate Hono instances and mount them withapp.route()for clean separation. -
Handle errors globally — Use
app.onError()andapp.notFound()to ensure every error returns a structured JSON response instead of stack traces. -
Use
c.executionCtx.waitUntil()— For background work on Cloudflare Workers (analytics, logging), usewaitUntil()to avoid blocking the response. -
Keep handlers thin — Extract business logic into separate functions/modules. Route handlers should parse input, call logic, and return responses.
-
Test with
testClient— Hono’s built-in test client is type-safe and doesn’t need a running server, making unit tests fast and reliable. -
Deploy where your users are — Hono runs on every major edge runtime. Choose Cloudflare Workers for global edge, Bun for raw speed, or Node.js for ecosystem compatibility.