DevToolBoxGRATIS
Blogg

Hono Complete Guide: Ultra-Fast Web Framework for Edge and Beyond

20 min readby DevToolBox Team

TL;DR

Hono is an ultra-fast, multi-runtime web framework (2.5x faster than Express) with built-in TypeScript support, middleware, validation via Zod, JWT auth, CORS, OpenAPI docs, and RPC mode for end-to-end type safety. It runs everywhere: Cloudflare Workers, Deno, Bun, Node.js, and AWS Lambda.

Key Takeaways

  • Runs on every major JS runtime with zero code changes — true write-once, deploy-anywhere.
  • Built-in middleware for JWT, CORS, ETag, logging, and compression eliminates external packages.
  • Zod-based validation provides runtime type checking with automatic TypeScript inference.
  • RPC mode enables end-to-end type safety between server and client without code generation.
  • OpenAPI integration auto-generates Swagger docs from your route definitions.
  • Handles 130K+ req/s on Bun, outperforming Express by 2.5x or more.

Hono is a small, simple, and ultra-fast web framework designed for the edge. With first-class TypeScript support and zero dependencies, Hono runs on virtually every JavaScript runtime: Cloudflare Workers, Deno, Bun, Node.js, Fastly, Netlify, AWS Lambda, and more. It delivers Express-like ergonomics with dramatically better performance and full type safety.

What Is Hono?

Hono (meaning "flame" in Japanese) is a lightweight web framework created by Yusuke Wada in 2022. Originally designed for Cloudflare Workers, it now supports every major JavaScript runtime. Hono uses Web Standard APIs (Request, Response, fetch) as its foundation, making it portable across runtimes without polyfills.

  • Ultra-fast: RegExpRouter benchmarks at 130K+ req/s on Bun
  • Tiny: core ~14KB minified, zero external dependencies
  • Multi-runtime: Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, Fastly
  • Type-safe: first-class TypeScript with inferred route types and RPC mode
  • Rich middleware: JWT, CORS, ETag, logger, compress, basicAuth, secureHeaders built-in
  • Web Standards: built on Request/Response, no proprietary abstractions

Installation and Setup

Hono provides starter templates for every runtime. Use the create-hono CLI to scaffold a new project.

# Create a new Hono project
npm create hono@latest my-app

# Select your target runtime:
#   cloudflare-workers / deno / bun / nodejs / aws-lambda

cd my-app && npm install

# Or install manually
npm install hono

# Project structure
# my-app/
# ├── src/index.ts    # Entry point
# ├── package.json
# ├── tsconfig.json
# └── wrangler.toml   # (Cloudflare only)

Routing

Hono provides a powerful routing system with path parameters, wildcards, regex constraints, and optional segments.

Basic Routes

import { Hono } from 'hono'
const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

app.post('/users', async (c) => {
  const body = await c.req.json()
  return c.json({ id: 1, ...body }, 201)
})

app.put('/users/:id', async (c) => {
  const id = c.req.param('id')
  const body = await c.req.json()
  return c.json({ id, ...body })
})

app.delete('/users/:id', (c) => {
  return c.json({ deleted: c.req.param('id') })
})

app.on(['PUT', 'PATCH'], '/items/:id', (c) => {
  return c.json({ updated: true })
})

app.all('/api/*', (c) => c.json({ method: c.req.method }))

export default app

Grouped and Nested Routes

Use app.route() to organize routes into logical groups, keeping large applications maintainable.

import { Hono } from 'hono'

// routes/users.ts
const users = new Hono()
users.get('/', (c) => c.json([{ id: 1, name: 'Alice' }]))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', async (c) => c.json(await c.req.json(), 201))

// routes/posts.ts
const posts = new Hono()
posts.get('/', (c) => c.json([{ id: 1, title: 'Hello' }]))
posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))

// Main app — mount route groups
const app = new Hono()
app.route('/api/users', users)
app.route('/api/posts', posts)
export default app

Route Parameters and Wildcards

// Named parameters
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))

// Multiple parameters
app.get('/orgs/:orgId/repos/:repoId', (c) => {
  const { orgId, repoId } = c.req.param()
  return c.json({ orgId, repoId })
})

// Optional parameter
app.get('/articles/:slug/:format?', (c) => {
  const format = c.req.param('format') || 'html'
  return c.json({ slug: c.req.param('slug'), format })
})

// Wildcard
app.get('/files/*', (c) => c.text('File: ' + c.req.path))

// Regex constraint
app.get('/posts/:id{[0-9]+}', (c) => {
  return c.json({ id: Number(c.req.param('id')) })
})

Middleware

Middleware in Hono follows the onion model. Hono ships with rich built-in middleware and makes custom ones simple to create.

Built-in Middleware

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { etag } from 'hono/etag'
import { compress } from 'hono/compress'
import { secureHeaders } from 'hono/secure-headers'
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { prettyJSON } from 'hono/pretty-json'

const app = new Hono()
app.use('*', logger())           // Log method, path, status
app.use('/api/*', cors())         // Cross-origin requests
app.use('*', etag())             // Caching headers
app.use('*', compress())         // Gzip/brotli
app.use('*', secureHeaders())    // CSP, X-Frame-Options
app.use('*', prettyJSON())       // ?pretty for formatted output

app.use('/admin/*', basicAuth({
  username: 'admin', password: 'secret',
}))
app.use('/api/private/*', bearerAuth({
  token: 'my-secret-token',
}))

Custom Middleware

import { createMiddleware } from 'hono/factory'

// Timing middleware
const timer = createMiddleware(async (c, next) => {
  const start = Date.now()
  await next()
  c.header('X-Response-Time', (Date.now() - start) + 'ms')
})

// Typed auth middleware
type Env = { Variables: { user: { id: string; role: string } } }

const auth = createMiddleware<Env>(async (c, next) => {
  const token = c.req.header('Authorization')
  if (!token) return c.json({ error: 'Unauthorized' }, 401)
  c.set('user', { id: '123', role: 'admin' })
  await next()
})

const app = new Hono<Env>()
app.use('*', timer)
app.use('/protected/*', auth)
app.get('/protected/profile', (c) => {
  const user = c.get('user')  // fully typed!
  return c.json({ user })
})

Request Handling

The Hono context object (c) provides convenient methods to access headers, query params, body, and more.

app.post('/upload', async (c) => {
  // Headers
  const contentType = c.req.header('Content-Type')

  // Query parameters
  const page = c.req.query('page')       // single
  const tags = c.req.queries('tag')       // array

  // JSON body
  const json = await c.req.json()

  // Form data
  const form = await c.req.formData()
  const file = form.get('file') as File

  // URL info
  const url = c.req.url       // full URL
  const path = c.req.path     // pathname
  const method = c.req.method // HTTP method

  // Path parameters
  const id = c.req.param('id')

  // Raw body
  const text = await c.req.text()
  const buffer = await c.req.arrayBuffer()

  return c.json({ received: true })
})

Response Helpers

Hono provides ergonomic response helpers for JSON, HTML, text, redirects, streaming, and more.

// JSON, text, HTML responses
app.get('/json', (c) => c.json({ message: 'hello' }))
app.get('/error', (c) => c.json({ error: 'not found' }, 404))
app.get('/text', (c) => c.text('Hello World'))
app.get('/page', (c) => c.html('<h1>Hello</h1>'))

// Redirects
app.get('/old', (c) => c.redirect('/new'))
app.get('/moved', (c) => c.redirect('/permanent', 301))

// Headers and status
app.get('/custom', (c) => {
  c.header('X-Custom', 'value')
  c.header('Cache-Control', 'max-age=3600')
  c.status(201)
  return c.json({ created: true })
})

// Streaming
app.get('/stream', (c) => {
  return c.streamText(async (stream) => {
    for (let i = 0; i < 5; i++) {
      await stream.writeln('Chunk ' + i)
      await stream.sleep(1000)
    }
  })
})

Validation with Zod

Hono integrates with Zod through @hono/zod-validator, providing runtime validation with automatic TypeScript type inference.

import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

// Validate JSON body
const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
})

app.post('/users', zValidator('json', userSchema), (c) => {
  const body = c.req.valid('json')  // fully typed
  return c.json({ user: body }, 201)
})

// Validate query parameters
const listSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().max(100).default(20),
  search: z.string().optional(),
})

app.get('/users', zValidator('query', listSchema), (c) => {
  const { page, limit, search } = c.req.valid('query')
  return c.json({ page, limit, search })
})

// Validate path params + custom error handling
app.get('/users/:id',
  zValidator('param', z.object({ id: z.coerce.number().positive() }),
    (result, c) => {
      if (!result.success) {
        return c.json({ errors: result.error.flatten() }, 422)
      }
    }
  ),
  (c) => c.json({ id: c.req.valid('param').id })
)

The Context Object

The context object (c) is the core of every Hono handler — it provides access to request, response helpers, variables, and execution context.

type AppEnv = {
  Bindings: { DATABASE_URL: string; API_KEY: string }
  Variables: { requestId: string }
}

const app = new Hono<AppEnv>()

app.use('*', async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  await next()
})

app.get('/demo', (c) => {
  const url = c.req.url          // request URL
  const dbUrl = c.env.DATABASE_URL // env bindings
  const reqId = c.get('requestId') // typed variables
  c.header('X-Request-Id', reqId)  // set headers
  return c.json({ requestId: reqId, url })
})

// waitUntil (Cloudflare Workers)
app.post('/webhook', (c) => {
  c.executionCtx.waitUntil(
    fetch('https://analytics.example.com/track', {
      method: 'POST',
      body: JSON.stringify({ event: 'webhook' }),
    })
  )
  return c.json({ ok: true })
})

Runtime Adapters

One of Hono's greatest strengths is multi-runtime support. The same code runs across different runtimes with minimal adapter changes.

Cloudflare Workers

// src/index.ts — Cloudflare Workers
import { Hono } from 'hono'

type Bindings = {
  MY_KV: KVNamespace
  MY_DB: D1Database
  MY_BUCKET: R2Bucket
  API_KEY: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.get('/kv/:key', async (c) => {
  const val = await c.env.MY_KV.get(c.req.param('key'))
  return val ? c.json({ val }) : c.json({ error: 'Not found' }, 404)
})

app.get('/db/users', async (c) => {
  const { results } = await c.env.MY_DB
    .prepare('SELECT * FROM users LIMIT 10')
    .all()
  return c.json(results)
})

export default app

Deno

// main.ts — Deno
import { Hono } from 'https://deno.land/x/hono/mod.ts'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Deno!'))
app.get('/env', (c) => {
  return c.json({ runtime: 'deno', port: Deno.env.get('PORT') })
})

Deno.serve(app.fetch)
// Run: deno run --allow-net --allow-env main.ts

Bun

// index.ts — Bun (fastest runtime for Hono)
import { Hono } from 'hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Bun!'))
app.get('/file', async (c) => {
  const data = await Bun.file('./data.json').json()
  return c.json(data)
})

export default { port: 3000, fetch: app.fetch }
// Run: bun run index.ts

Node.js

// index.ts — Node.js
import { Hono } from 'hono'
import { serve } from '@hono/node-server'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Node.js!'))
app.get('/health', (c) => {
  return c.json({ status: 'ok', version: process.version })
})

serve({ fetch: app.fetch, port: 3000 })
// Install: npm install hono @hono/node-server
// Run: npx tsx index.ts

AWS Lambda

// lambda.ts — AWS Lambda
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Lambda!'))
app.get('/items', (c) => {
  return c.json([{ id: 1, name: 'Item A' }])
})

// Export handler for API Gateway or Lambda URL
export const handler = handle(app)

Error Handling

Hono provides structured error handling through app.onError() and the HTTPException class for clean, consistent API error responses.

import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'

const app = new Hono()

// Global error handler
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json(
      { error: err.message, status: err.status },
      err.status
    )
  }
  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)
})

// Throw HTTPException in handlers
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 with response
app.get('/protected', (c) => {
  throw new HTTPException(401, {
    message: 'Authentication required',
    res: new Response('Unauthorized', {
      status: 401,
      headers: { 'WWW-Authenticate': 'Bearer' },
    }),
  })
})

Testing

Hono provides a built-in test client via app.request() — no need to spin up a server.

// app.test.ts — using Vitest
import { describe, it, expect } from 'vitest'
import app from './app'

describe('API', () => {
  it('GET / returns OK', async () => {
    const res = await app.request('/')
    expect(res.status).toBe(200)
    expect(await res.text()).toBe('OK')
  })

  it('GET /users/:id returns user', async () => {
    const res = await app.request('/users/42')
    const data = await res.json()
    expect(data.id).toBe('42')
  })

  it('POST /users creates user', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Alice' }),
    })
    expect(res.status).toBe(201)
    const data = await res.json()
    expect(data.name).toBe('Alice')
  })

  it('returns 404 for unknown routes', async () => {
    const res = await app.request('/nonexistent')
    expect(res.status).toBe(404)
  })

  it('handles auth with Bearer token', async () => {
    const res = await app.request('/api/me', {
      headers: { Authorization: 'Bearer my-token' },
    })
    expect(res.status).toBe(200)
  })
})

JWT Authentication

Hono includes built-in JWT middleware for token verification, enabling complete authentication flows.

import { Hono } from 'hono'
import { jwt, sign } from 'hono/jwt'

const app = new Hono()
const SECRET = 'my-jwt-secret-key'

// Login — generate token
app.post('/auth/login', async (c) => {
  const { email, password } = await c.req.json()
  if (email !== 'admin@test.com') {
    return c.json({ error: 'Invalid credentials' }, 401)
  }
  const token = await sign({
    sub: 'user-123', email, role: 'admin',
    exp: Math.floor(Date.now() / 1000) + 3600,
  }, SECRET)
  return c.json({ token })
})

// Protect routes
app.use('/api/*', jwt({ secret: SECRET }))

app.get('/api/me', (c) => {
  const payload = c.get('jwtPayload')
  return c.json({ userId: payload.sub, role: payload.role })
})

// Role-based access control
const requireRole = (role: string) => async (c: any, next: any) => {
  if (c.get('jwtPayload').role !== role) {
    return c.json({ error: 'Forbidden' }, 403)
  }
  await next()
}

app.delete('/api/admin/users/:id', requireRole('admin'),
  (c) => c.json({ deleted: c.req.param('id') })
)

CORS Configuration

The built-in CORS middleware handles cross-origin requests with fine-grained control over origins, methods, and headers.

import { cors } from 'hono/cors'

// Allow all origins (development)
app.use('/api/*', cors())

// Restrict to specific origins (production)
app.use('/api/*', cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  exposeHeaders: ['X-Total-Count'],
  maxAge: 86400,
  credentials: true,
}))

// Dynamic origin
app.use('/api/*', cors({
  origin: (origin) => {
    return origin.endsWith('.myapp.com') ? origin : 'https://myapp.com'
  },
}))

OpenAPI Integration

The @hono/zod-openapi package defines routes with OpenAPI schemas and auto-generates Swagger documentation.

import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'

const app = new OpenAPIHono()

const getUserRoute = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: {
    params: z.object({
      id: z.string().openapi({ example: '123' }),
    }),
  },
  responses: {
    200: {
      content: { 'application/json': {
        schema: z.object({
          id: z.string(), name: z.string(), email: z.string().email(),
        }),
      }},
      description: 'User found',
    },
  },
})

app.openapi(getUserRoute, (c) => {
  const { id } = c.req.valid('param')
  return c.json({ id, name: 'Alice', email: 'a@b.com' })
})

// Serve OpenAPI spec + Swagger UI
app.doc('/doc', {
  openapi: '3.0.0',
  info: { title: 'My API', version: '1.0.0' },
})
app.get('/ui', swaggerUI({ url: '/doc' }))

RPC Mode

Hono RPC provides end-to-end type safety between server and client without code generation. The client infers types directly from route definitions.

// ---- Server (server.ts) ----
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const app = new Hono()
  .get('/api/posts', (c) => {
    return c.json([{ id: 1, title: 'Hello Hono' }])
  })
  .post('/api/posts',
    zValidator('json', z.object({
      title: z.string(), content: z.string(),
    })),
    (c) => c.json({ id: 2, ...c.req.valid('json') }, 201)
  )

export type AppType = typeof app
export default app

// ---- Client (client.ts) ----
import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:3000')

// Fully typed — IDE autocomplete + compile-time checks
const res = await client.api.posts.$get()
const data = await res.json() // typed as { id: number; title: string }[]

const newPost = await client.api.posts.$post({
  json: { title: 'New', content: 'Hello!' },
}) // TypeScript error if shape is wrong

Performance Benchmarks

Hono consistently outperforms Express, Fastify, and Koa in benchmarks.

Benchmark Results (simple JSON, 10s)

  • Hono (Bun): ~130,000 req/s
  • Fastify (Node.js): ~75,000 req/s
  • Express (Node.js): ~50,000 req/s
  • Hono (Deno): ~95,000 req/s
  • Hono (Workers): sub-ms cold start, ~80,000 req/s
// Performance tips
// 1. RegExpRouter (default) — fastest for static routes
const app = new Hono()  // uses RegExpRouter

// 2. LinearRouter — better for dynamic route registration
import { LinearRouter } from 'hono/router/linear-router'
const app = new Hono({ router: new LinearRouter() })

// 3. Use c.json() over new Response()
app.get('/fast', (c) => c.json({ ok: true }))

// 4. Stream large responses
app.get('/large', (c) => {
  return c.streamText(async (stream) => {
    for (const chunk of data) await stream.write(chunk)
  })
})

// 5. Cache with ETag
import { etag } from 'hono/etag'
app.use('/api/*', etag())

Frequently Asked Questions

What is Hono and how is it different from Express?

Hono is an ultra-fast, lightweight web framework built on Web Standard APIs. Unlike Express which only runs on Node.js, Hono runs on Cloudflare Workers, Deno, Bun, Node.js, and AWS Lambda. It is 2-3x faster, has first-class TypeScript support, zero dependencies, and built-in middleware for JWT, CORS, and validation.

Can Hono replace Express in production?

Yes. Hono is production-ready and used by Cloudflare, Vercel, and many startups. It provides all Express features plus type safety, multi-runtime support, and significantly better performance.

Which JavaScript runtime is fastest for Hono?

Bun delivers the highest throughput at 130K+ req/s. Deno is second at ~95K req/s. Cloudflare Workers provide the lowest latency due to edge deployment. On Node.js, Hono still outperforms Express by 2x+.

How does Hono handle validation?

Hono integrates with Zod through @hono/zod-validator. You define schemas for body, query, headers, and params. The middleware validates automatically, returning 400 errors for invalid data with full TypeScript inference.

Does Hono support WebSockets?

Yes. Hono supports WebSocket connections through hono/websocket on runtimes that support it (Cloudflare Workers with Durable Objects, Deno, Bun) with full type safety.

How do I deploy Hono to Cloudflare Workers?

Create a project with "npm create hono@latest" and select cloudflare-workers. Write routes in src/index.ts, configure wrangler.toml, then deploy with "wrangler deploy". Hono has first-class Cloudflare support with typed bindings.

What is Hono RPC mode?

RPC mode provides end-to-end type safety between server and client without code generation. Export the app type from server, use hc() to create a typed client that infers all route types and response shapes at compile time.

Can Hono generate OpenAPI documentation automatically?

Yes. The @hono/zod-openapi package auto-generates a complete OpenAPI 3.0 spec from route definitions. You can serve Swagger UI directly from your Hono app, eliminating separate documentation maintenance.

𝕏 Twitterin LinkedIn
Var dette nyttig?

Hold deg oppdatert

Få ukentlige dev-tips og nye verktøy.

Ingen spam. Avslutt når som helst.

Try These Related Tools

{ }JSON FormatterJSON Validator

Related Articles

tRPC Complete Guide: End-to-End Type-Safe APIs for TypeScript

Master tRPC with routers, procedures, Zod validation, middleware, React Query integration, subscriptions, error handling, and testing strategies.

Deno: The Complete Guide to the Secure JavaScript Runtime

Master Deno runtime with security permissions, TypeScript support, standard library, HTTP server, testing, npm compatibility, and Deno Deploy.

Bun: The Complete Guide to the All-in-One JavaScript Runtime

Master Bun runtime with package manager, bundler, test runner, HTTP server, SQLite, shell scripting, and Node.js/Deno comparison.