Hono
Hono cheat sheet covering routing, middleware, context API, validation, RPC client, and deployment to Cloudflare Workers and Bun.
Getting Started
Initialize a Hono project with create-hono CLI
# Create new Hono project
npm create hono@latest my-app
# or
bun create hono my-app
# or
yarn create hono my-app
# Select your runtime:
# - cloudflare-workers
# - cloudflare-pages
# - deno
# - bun
# - nodejs
# - vercel
# - aws-lambdaCreate your first Hono application
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default appRouting
Define routes for different HTTP methods
// HTTP Methods
app.get('/users', (c) => c.text('GET /users'))
app.post('/users', (c) => c.text('POST /users'))
app.put('/users/:id', (c) => c.text('PUT /users/:id'))
app.delete('/users/:id', (c) => c.text('DELETE /users/:id'))
app.patch('/users/:id', (c) => c.text('PATCH'))
// Any HTTP method
app.all('/hello', (c) => c.text('Any Method'))
// Custom method
app.on('PURGE', '/cache', (c) => c.text('PURGE'))
// Multiple methods
app.on(['PUT', 'DELETE'], '/resource', (c) => {
return c.text('PUT or DELETE')
})Extract dynamic values from URLs
// Single parameter
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 (using regex)
app.get('/books/:id{[0-9]+}?', (c) => {
const id = c.req.param('id') // undefined if not provided
return c.json({ id })
})Organize routes with grouping and base paths
// Basepath for entire app
const app = new Hono().basePath('/api/v1')
app.get('/users', (c) => c.json([])) // /api/v1/users
app.get('/posts', (c) => c.json([])) // /api/v1/posts
// Route grouping with sub-apps
const users = new Hono()
users.get('/', (c) => c.json([]))
users.get('/:id', (c) => c.json({}))
users.post('/', (c) => c.json({}, 201))
const posts = new Hono()
posts.get('/', (c) => c.json([]))
const app = new Hono()
app.route('/users', users)
app.route('/posts', posts)Context & Request
Access query params, headers, body, and more
// Query parameters
app.get('/search', (c) => {
const query = c.req.query('q') // single
const all = c.req.queries('tags') // array
return c.json({ query, all })
})
// Headers
app.get('/auth', (c) => {
const auth = c.req.header('Authorization')
const userAgent = c.req.header('User-Agent')
return c.json({ auth, userAgent })
})
// Body parsing
app.post('/users', async (c) => {
const body = await c.req.json() // JSON
const form = await c.req.formData() // FormData
const text = await c.req.text() // Raw text
return c.json(body)
})Different ways to send responses
// Text response
app.get('/text', (c) => c.text('Hello!'))
// JSON response
app.get('/json', (c) => c.json({ msg: 'Hello!' }))
// HTML response
app.get('/html', (c) => c.html('<h1>Hello!</h1>'))
// With status code
app.post('/create', (c) => c.json({ id: 1 }, 201))
// Redirect
app.get('/old', (c) => c.redirect('/new'))
app.get('/external', (c) => c.redirect('https://hono.dev', 301))
// Set headers
app.get('/custom', (c) => {
c.header('X-Custom', 'value')
c.status(201)
return c.json({ ok: true })
})Store and retrieve values in request context
// Set context variable in middleware
app.use('*', async (c, next) => {
c.set('userId', '12345')
c.set('startTime', Date.now())
await next()
})
// Get context variable in handler
app.get('/profile', (c) => {
const userId = c.get('userId')
return c.json({ userId })
})
// Type-safe context with generics
type Variables = {
userId: string
user: { name: string; email: string }
}
const app = new Hono<{ Variables: Variables }>()Middleware
Define and use custom middleware
// Basic middleware
app.use('*', async (c, next) => {
console.log('Before handler')
await next()
console.log('After handler')
})
// Path-specific middleware
app.use('/api/*', async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', \`\${ms}ms\`)
})
// Using createMiddleware for type safety
import { createMiddleware } from 'hono/factory'
const authMiddleware = createMiddleware(async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
})Common middleware included with Hono
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// Logging
app.use('*', logger())
// CORS
app.use('/api/*', cors({
origin: 'https://example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}))
// Pretty JSON (add ?pretty to URLs)
app.use('*', prettyJSON())
// Response compression
app.use('*', compress())Basic Auth, Bearer Auth, and JWT middleware
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret123',
}))
// Bearer Token Auth
app.use('/api/*', bearerAuth({
token: 'my-secret-token',
}))
// JWT Auth
app.use('/auth/*', jwt({
secret: 'my-jwt-secret',
}))
app.get('/auth/profile', (c) => {
const payload = c.get('jwtPayload')
return c.json(payload)
})Validation
Type-safe validation with Zod schemas
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
// JSON body validation
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(18).optional(),
})
app.post('/users',
zValidator('json', createUserSchema),
(c) => {
const { name, email, age } = c.req.valid('json')
return c.json({ name, email, age }, 201)
}
)
// Query params validation
const searchSchema = z.object({
q: z.string(),
page: z.coerce.number().positive().default(1),
limit: z.coerce.number().max(100).default(10),
})
app.get('/search',
zValidator('query', searchSchema),
(c) => {
const { q, page, limit } = c.req.valid('query')
return c.json({ q, page, limit })
}
)Use Hono's built-in validator without external deps
import { validator } from 'hono/validator'
// Custom validation function
app.post('/posts',
validator('json', (value, c) => {
const { title, body } = value
if (!title || typeof title !== 'string') {
return c.json({ error: 'Title is required' }, 400)
}
if (!body || body.length < 10) {
return c.json({ error: 'Body too short' }, 400)
}
return { title, body } // Return validated data
}),
(c) => {
const { title, body } = c.req.valid('json')
return c.json({ title, body }, 201)
}
)RPC Client
Create a type-safe API client from your Hono app
// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
.get('/users', (c) => c.json({ users: [] }))
.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'John' })
})
.post('/users',
zValidator('json', z.object({
name: z.string(),
email: z.string().email(),
})),
(c) => {
const data = c.req.valid('json')
return c.json({ id: '123', ...data }, 201)
}
)
export type AppType = typeof app
export default appUse the type-safe RPC client
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
// GET request
const res = await client.users.$get()
const data = await res.json() // Typed!
// GET with path params
const user = await client.users[':id'].$get({
param: { id: '123' }
})
// POST with JSON body
const created = await client.users.$post({
json: { name: 'John', email: 'john@example.com' }
})
// Check status
if (created.status === 201) {
const newUser = await created.json()
}Streaming & SSE
Stream real-time updates to clients
import { streamSSE } from 'hono/streaming'
let eventId = 0
app.get('/sse', async (c) => {
return streamSSE(c, async (stream) => {
while (true) {
await stream.writeSSE({
data: JSON.stringify({ time: new Date().toISOString() }),
event: 'time-update',
id: String(eventId++),
})
await stream.sleep(1000)
}
})
})
// Client-side
const eventSource = new EventSource('/sse')
eventSource.addEventListener('time-update', (e) => {
console.log(JSON.parse(e.data))
})Real-time bidirectional communication
import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/cloudflare-workers'
// or: 'hono/bun', 'hono/deno'
const app = new Hono()
app.get('/ws', upgradeWebSocket((c) => {
return {
onOpen(event, ws) {
console.log('Client connected')
ws.send('Welcome!')
},
onMessage(event, ws) {
console.log('Received:', event.data)
ws.send(\`Echo: \${event.data}\`)
},
onClose(event, ws) {
console.log('Client disconnected')
},
onError(event, ws) {
console.log('Error:', event)
},
}
}))Error Handling
Handle errors gracefully in your app
import { HTTPException } from 'hono/http-exception'
// Throw HTTP errors
app.get('/users/:id', async (c) => {
const user = await db.findUser(c.req.param('id'))
if (!user) {
throw new HTTPException(404, { message: 'User not found' })
}
return c.json(user)
})
// 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)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found', path: c.req.path }, 404)
})Deployment
Deploy to Cloudflare Workers edge network
// wrangler.toml
name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
API_KEY = "dev-key"
// src/index.ts
import { Hono } from 'hono'
type Bindings = {
API_KEY: string
MY_KV: KVNamespace
MY_DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/', (c) => {
const apiKey = c.env.API_KEY
return c.json({ key: apiKey })
})
export default app
# Deploy
npx wrangler deployDeploy to other JavaScript runtimes
// ========== BUN ==========
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Bun!'))
export default app
// or: export default { port: 3000, fetch: app.fetch }
# Run
bun run src/index.ts
// ========== DENO ==========
// main.ts
import { Hono } from 'npm:hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Deno!'))
Deno.serve(app.fetch)
# Run
deno run --allow-net main.ts
// ========== NODE.JS ==========
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/', (c) => c.text('Hello Node!'))
serve({ fetch: app.fetch, port: 3000 })Testing
Test your Hono app without starting a server
import { describe, it, expect } from 'vitest'
import app from './app'
describe('API Tests', () => {
it('GET / returns hello', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello Hono!')
})
it('POST /users creates user', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John' }),
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data.name).toBe('John')
})
})