Skip to content

Hono

Ultrafast web framework for the edge, supporting Cloudflare Workers, Deno, Bun, Vercel, AWS Lambda, and Node.js with zero dependencies.

CommandDescription
npm create hono@latest my-appCreate new Hono project with npm
yarn create hono my-appCreate with Yarn
pnpm create hono my-appCreate with pnpm
bun create hono my-appCreate with Bun
deno init --lib honoCreate with Deno
CommandDescription
npm install honoInstall Hono in existing project
yarn add honoInstall with Yarn
pnpm add honoInstall with pnpm
bun add honoInstall with Bun
# 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
CommandDescription
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
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
// 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) })
})
CommandDescription
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
CommandDescription
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
CommandDescription
c.req.header('Content-Type')Get request header
c.req.header()Get all headers
c.req.methodGet HTTP method
c.req.urlGet full request URL
c.req.pathGet request path
c.req.rawGet raw Request object
c.req.valid('json')Get validated data
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)
})
CommandDescription
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
// 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!')
  })
})
CommandDescription
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
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`)
})
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 })
})
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 })
  }
)
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)
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 })
})
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
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)
// 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 }[] }
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
  1. Use TypeScript generics for env bindings — Define Hono<{ Bindings: MyBindings }> to get type safety for environment variables, KV namespaces, and D1 databases.

  2. Validate all inputs with Zod — Use @hono/zod-validator for request body, query, and param validation. This gives runtime safety and TypeScript types in one step.

  3. Use the RPC client for frontend-backend — Export AppType from your server and use hc<AppType>() on the client for end-to-end type safety without code generation.

  4. Apply middleware with path patterns — Use app.use('/api/*', middleware) to scope middleware to specific routes rather than applying everything globally.

  5. Group routes with new Hono().basePath() — Organize related endpoints into separate Hono instances and mount them with app.route() for clean separation.

  6. Handle errors globally — Use app.onError() and app.notFound() to ensure every error returns a structured JSON response instead of stack traces.

  7. Use c.executionCtx.waitUntil() — For background work on Cloudflare Workers (analytics, logging), use waitUntil() to avoid blocking the response.

  8. Keep handlers thin — Extract business logic into separate functions/modules. Route handlers should parse input, call logic, and return responses.

  9. 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.

  10. 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.