Frameworks

Hono: The Ultrafast Web Framework for the Edge, Serverless, and Beyond

Hono: The Ultrafast Web Framework for the Edge, Serverless, and Beyond

If you’ve been building APIs or web applications with Node.js, you’ve likely encountered the familiar trade-off: developer-friendly frameworks that sacrifice raw performance, or blazing-fast runtimes that demand verbose boilerplate. Hono shatters that compromise. Born for the edge, optimized for every JavaScript runtime, and designed with an ergonomic API that feels instantly familiar, Hono has rapidly become one of the most exciting web frameworks in the modern JavaScript ecosystem.

In this guide, we’ll explore what makes Hono unique, walk through practical code examples including a full API with middleware and a Cloudflare Workers integration with D1 database, and examine why teams building performance-critical applications are increasingly choosing Hono over established alternatives.

What Is Hono?

Hono (meaning “flame” in Japanese) is an ultrafast, lightweight web framework built on Web Standards. Unlike traditional Node.js frameworks that rely on platform-specific APIs, Hono is built entirely on the Request and Response interfaces defined by the Web API standard. This architectural decision means Hono runs natively on Cloudflare Workers, Deno, Bun, Fastly Compute, AWS Lambda, and Node.js — all without modification.

Created by Yusuke Wada in 2022, Hono started as a Cloudflare Workers framework but quickly evolved into a truly universal solution. Its router is one of the fastest in the JavaScript ecosystem, consistently benchmarking at or near the top across all major runtimes. The framework weighs in at approximately 14KB (minified), making it one of the smallest full-featured web frameworks available.

If you’re new to server-side JavaScript, our guide to getting started with Node.js provides the foundational knowledge you’ll need before diving into Hono’s more advanced patterns.

Why Hono Stands Out

Multi-Runtime Support

The JavaScript runtime landscape has expanded dramatically. Beyond Node.js, developers now have Bun, Deno, and edge runtimes like Cloudflare Workers to choose from. Hono doesn’t force you to pick one. Write your application once and deploy it anywhere. This portability isn’t just theoretical — Hono’s adapter system handles the runtime-specific differences transparently.

Router Performance

Hono ships with multiple router implementations, each optimized for different use cases. The RegExpRouter pre-compiles all routes into a single regular expression at startup, achieving O(1) route matching regardless of how many routes you define. The TrieRouter offers a balanced approach for applications with dynamic route patterns. The SmartRouter (the default) automatically selects the optimal strategy based on your route definitions.

TypeScript-First Design

Hono was built with TypeScript from day one. Route parameters, query strings, request bodies, and middleware outputs are all fully typed. The framework’s type inference is remarkably sophisticated — when you define a validation schema, the parsed data is automatically typed throughout your handler chain. This eliminates entire categories of runtime errors and dramatically improves the developer experience in modern IDEs.

Built-In Middleware Ecosystem

Rather than requiring a constellation of third-party packages, Hono includes a comprehensive set of built-in middleware: CORS, ETag, compression, Bearer auth, JWT authentication, request logging, caching, rate limiting, and more. Each middleware follows the same composable pattern, and they’re all tree-shakeable — you only pay for what you import.

Getting Started with Hono

Setting up a Hono project is straightforward. The create-hono CLI scaffolds projects with templates for every supported runtime:

npm create hono@latest my-api
cd my-api
npm install
npm run dev

The CLI will prompt you to select your target runtime. Whether you choose Cloudflare Workers, Bun, Deno, or Node.js, you’ll get a properly configured project with the right adapter pre-installed. For those exploring edge computing with Cloudflare and Deno, Hono provides first-class support for both platforms.

Building a Full API with Middleware, Authentication, and Validation

Let’s build a practical REST API that demonstrates Hono’s middleware composition, JWT authentication, request validation, and error handling. This example creates a task management API — the kind of backend you might build for a project management tool like Taskee.

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { jwt, sign } from 'hono/jwt'
import { validator } from 'hono/validator'
import { HTTPException } from 'hono/http-exception'

// Type definitions
type Task = {
  id: string
  title: string
  description: string
  status: 'todo' | 'in_progress' | 'done'
  priority: 'low' | 'medium' | 'high'
  assignee: string | null
  createdAt: string
  updatedAt: string
}

type Variables = {
  userId: string
  role: string
}

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

// In-memory store (replace with database in production)
const tasks = new Map<string, Task>()
const JWT_SECRET = 'your-secret-key-change-in-production'

// ---- Global Middleware ----
app.use('*', logger())
app.use('*', prettyJSON())
app.use('*', cors({
  origin: ['http://localhost:3000', 'https://yourdomain.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,
}))

// ---- Auth Endpoints ----
app.post('/auth/login', validator('json', (value, c) => {
  const { email, password } = value
  if (!email || !password) {
    return c.json({ error: 'Email and password required' }, 400)
  }
  return { email: String(email), password: String(password) }
}), async (c) => {
  const { email, password } = c.req.valid('json')

  // Simplified auth check (use proper DB lookup in production)
  if (email === 'admin@example.com' && password === 'secure123') {
    const token = await sign(
      { sub: 'user-001', role: 'admin', exp: Math.floor(Date.now() / 1000) + 3600 },
      JWT_SECRET
    )
    return c.json({ token, expiresIn: 3600 })
  }
  throw new HTTPException(401, { message: 'Invalid credentials' })
})

// ---- Protected Routes (JWT Middleware) ----
const api = new Hono<{ Variables: Variables }>()

api.use('*', jwt({ secret: JWT_SECRET }))
api.use('*', async (c, next) => {
  const payload = c.get('jwtPayload')
  c.set('userId', payload.sub)
  c.set('role', payload.role)
  await next()
})

// Task validation schema
const taskValidator = validator('json', (value, c) => {
  const { title, description, priority } = value
  if (!title || typeof title !== 'string' || title.length < 3) {
    return c.json({ error: 'Title must be at least 3 characters' }, 400)
  }
  if (!description || typeof description !== 'string') {
    return c.json({ error: 'Description is required' }, 400)
  }
  const validPriorities = ['low', 'medium', 'high']
  if (priority && !validPriorities.includes(priority)) {
    return c.json({ error: `Priority must be: ${validPriorities.join(', ')}` }, 400)
  }
  return {
    title: String(title),
    description: String(description),
    priority: (priority || 'medium') as Task['priority'],
  }
})

// GET /api/tasks — List all tasks with filtering
api.get('/tasks', (c) => {
  const status = c.req.query('status')
  const priority = c.req.query('priority')
  let result = Array.from(tasks.values())

  if (status) result = result.filter(t => t.status === status)
  if (priority) result = result.filter(t => t.priority === priority)

  return c.json({
    tasks: result,
    total: result.length,
    requestedBy: c.get('userId'),
  })
})

// POST /api/tasks — Create a new task
api.post('/tasks', taskValidator, (c) => {
  const { title, description, priority } = c.req.valid('json')
  const id = crypto.randomUUID()
  const now = new Date().toISOString()

  const task: Task = {
    id, title, description, priority,
    status: 'todo',
    assignee: c.get('userId'),
    createdAt: now,
    updatedAt: now,
  }

  tasks.set(id, task)
  return c.json(task, 201)
})

// PATCH /api/tasks/:id/status — Update task status
api.patch('/tasks/:id/status', validator('json', (value, c) => {
  const validStatuses = ['todo', 'in_progress', 'done']
  if (!value.status || !validStatuses.includes(value.status)) {
    return c.json({ error: `Status must be: ${validStatuses.join(', ')}` }, 400)
  }
  return { status: value.status as Task['status'] }
}), (c) => {
  const task = tasks.get(c.req.param('id'))
  if (!task) throw new HTTPException(404, { message: 'Task not found' })

  task.status = c.req.valid('json').status
  task.updatedAt = new Date().toISOString()
  return c.json(task)
})

// DELETE /api/tasks/:id — Admin-only deletion
api.delete('/tasks/:id', (c) => {
  if (c.get('role') !== 'admin') {
    throw new HTTPException(403, { message: 'Admin access required' })
  }
  const deleted = tasks.delete(c.req.param('id'))
  if (!deleted) throw new HTTPException(404, { message: 'Task not found' })
  return c.json({ message: 'Task deleted' })
})

// Mount protected routes
app.route('/api', api)

// ---- Global Error Handler ----
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status)
  }
  console.error('Unhandled error:', err)
  return c.json({ error: 'Internal server error' }, 500)
})

export default app

This example demonstrates several of Hono’s strengths: composable middleware stacking, typed context variables, route grouping with the app.route() method, built-in JWT handling, and structured error management. For a deeper understanding of the design principles behind this API structure, check out our introduction to REST APIs.

Hono with Cloudflare Workers and D1 Database

One of Hono’s most compelling use cases is building APIs on serverless infrastructure. Cloudflare Workers combined with D1 (Cloudflare’s serverless SQLite database) creates a globally distributed, zero-cold-start backend. Here’s a complete example building a blog API with D1:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { cache } from 'hono/cache'

// Define the Bindings type for Cloudflare Workers environment
type Bindings = {
  DB: D1Database
  CACHE_KV: KVNamespace
  ENVIRONMENT: string
}

type Post = {
  id: number
  slug: string
  title: string
  content: string
  excerpt: string
  author: string
  published: boolean
  created_at: string
  updated_at: string
}

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

app.use('*', cors())

// ---- Database Initialization ----
// Run this once via wrangler: wrangler d1 execute blog-db --file=./schema.sql
// schema.sql:
// CREATE TABLE IF NOT EXISTS posts (
//   id INTEGER PRIMARY KEY AUTOINCREMENT,
//   slug TEXT UNIQUE NOT NULL,
//   title TEXT NOT NULL,
//   content TEXT NOT NULL,
//   excerpt TEXT DEFAULT '',
//   author TEXT NOT NULL,
//   published INTEGER DEFAULT 0,
//   created_at TEXT DEFAULT (datetime('now')),
//   updated_at TEXT DEFAULT (datetime('now'))
// );
// CREATE INDEX idx_posts_slug ON posts(slug);
// CREATE INDEX idx_posts_published ON posts(published);

// ---- Middleware: Cache published posts for 5 minutes ----
app.get('/posts', cache({
  cacheName: 'blog-posts',
  cacheControl: 'max-age=300, stale-while-revalidate=60',
}))

// GET /posts — List published posts with pagination
app.get('/posts', async (c) => {
  const page = Number(c.req.query('page') || '1')
  const limit = Math.min(Number(c.req.query('limit') || '10'), 50)
  const offset = (page - 1) * limit

  const { results: posts } = await c.env.DB.prepare(
    'SELECT id, slug, title, excerpt, author, created_at FROM posts WHERE published = 1 ORDER BY created_at DESC LIMIT ? OFFSET ?'
  ).bind(limit, offset).all<Post>()

  const { results: [{ total }] } = await c.env.DB.prepare(
    'SELECT COUNT(*) as total FROM posts WHERE published = 1'
  ).all<{ total: number }>()

  return c.json({
    posts,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit),
    },
  })
})

// GET /posts/:slug — Get single post by slug
app.get('/posts/:slug', async (c) => {
  const slug = c.req.param('slug')

  // Try KV cache first
  const cached = await c.env.CACHE_KV.get(`post:${slug}`, 'json')
  if (cached) {
    return c.json({ post: cached, source: 'cache' })
  }

  const post = await c.env.DB.prepare(
    'SELECT * FROM posts WHERE slug = ? AND published = 1'
  ).bind(slug).first<Post>()

  if (!post) {
    return c.json({ error: 'Post not found' }, 404)
  }

  // Cache in KV for 10 minutes
  await c.env.CACHE_KV.put(`post:${slug}`, JSON.stringify(post), {
    expirationTtl: 600,
  })

  return c.json({ post, source: 'database' })
})

// POST /posts — Create a new post
app.post('/posts', async (c) => {
  const body = await c.req.json<Omit<Post, 'id' | 'created_at' | 'updated_at'>>()

  if (!body.title || !body.content || !body.author) {
    return c.json({ error: 'Title, content, and author are required' }, 400)
  }

  const slug = body.slug || body.title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/(^-|-$)/g, '')

  try {
    const result = await c.env.DB.prepare(
      'INSERT INTO posts (slug, title, content, excerpt, author, published) VALUES (?, ?, ?, ?, ?, ?)'
    ).bind(
      slug,
      body.title,
      body.content,
      body.excerpt || body.content.substring(0, 200),
      body.author,
      body.published ? 1 : 0
    ).run()

    return c.json({
      message: 'Post created',
      id: result.meta.last_row_id,
      slug,
    }, 201)
  } catch (e: any) {
    if (e.message?.includes('UNIQUE constraint')) {
      return c.json({ error: 'A post with this slug already exists' }, 409)
    }
    throw e
  }
})

// PUT /posts/:slug — Update an existing post
app.put('/posts/:slug', async (c) => {
  const slug = c.req.param('slug')
  const body = await c.req.json<Partial<Post>>()

  const existing = await c.env.DB.prepare(
    'SELECT id FROM posts WHERE slug = ?'
  ).bind(slug).first()

  if (!existing) {
    return c.json({ error: 'Post not found' }, 404)
  }

  await c.env.DB.prepare(
    `UPDATE posts SET
      title = COALESCE(?, title),
      content = COALESCE(?, content),
      excerpt = COALESCE(?, excerpt),
      published = COALESCE(?, published),
      updated_at = datetime('now')
    WHERE slug = ?`
  ).bind(
    body.title || null,
    body.content || null,
    body.excerpt || null,
    body.published !== undefined ? (body.published ? 1 : 0) : null,
    slug
  ).run()

  // Invalidate cache
  await c.env.CACHE_KV.delete(`post:${slug}`)

  return c.json({ message: 'Post updated', slug })
})

// DELETE /posts/:slug — Delete a post
app.delete('/posts/:slug', async (c) => {
  const slug = c.req.param('slug')

  const result = await c.env.DB.prepare(
    'DELETE FROM posts WHERE slug = ?'
  ).bind(slug).run()

  if (result.meta.changes === 0) {
    return c.json({ error: 'Post not found' }, 404)
  }

  await c.env.CACHE_KV.delete(`post:${slug}`)
  return c.json({ message: 'Post deleted' })
})

// ---- Health Check ----
app.get('/health', async (c) => {
  try {
    await c.env.DB.prepare('SELECT 1').run()
    return c.json({
      status: 'healthy',
      runtime: 'Cloudflare Workers',
      database: 'D1 connected',
      environment: c.env.ENVIRONMENT,
    })
  } catch {
    return c.json({ status: 'unhealthy', database: 'D1 error' }, 503)
  }
})

export default app

This Workers + D1 example showcases Hono’s tight integration with the Cloudflare ecosystem. The typed Bindings interface ensures your environment variables and service bindings are properly typed. The D1 query API is clean and parameterized, preventing SQL injection. The KV caching layer demonstrates how to combine multiple Cloudflare services within a single Hono application for optimal performance.

Hono’s Middleware Architecture

Hono’s middleware system follows the onion model — each middleware wraps around the next, executing code before and after the handler. This is similar to Koa’s approach but with better TypeScript support and Web Standard compatibility.

The framework includes over 20 built-in middleware modules. Some of the most commonly used include:

  • Bearer Auth & JWT — Token-based authentication with minimal configuration
  • CORS — Cross-origin resource sharing with fine-grained control
  • Compress — Automatic gzip/brotli compression
  • ETag — Conditional requests for efficient caching
  • Secure Headers — Adds security headers (CSP, HSTS, X-Frame-Options)
  • Rate Limiter — Request throttling with configurable windows and limits
  • Request ID — Unique identifiers for request tracing
  • Timing — Server-Timing headers for performance monitoring

Custom middleware creation is equally straightforward. You define an async function that receives the context and a next function, following the same pattern used by the built-in modules. This consistency makes the middleware ecosystem highly predictable and composable.

Hono vs. Express, Fastify, and Elysia

Understanding where Hono fits in the framework landscape helps clarify when it’s the right choice. For a broader comparison, see our roundup of the best web frameworks in 2026.

Hono vs. Express

Express remains the most widely used Node.js framework, but it was designed for a different era. Express relies heavily on Node-specific APIs (req, res objects), doesn’t support TypeScript natively, and requires numerous third-party middleware packages for basic functionality. Hono addresses all of these limitations while maintaining an equally approachable API surface. In benchmarks, Hono consistently handles 3-5x more requests per second than Express on Node.js, and the gap widens dramatically on edge runtimes where Express simply cannot run.

Hono vs. Fastify

Fastify is the performance champion of the Node.js-only world. It’s fast, well-documented, and has a mature plugin ecosystem. However, Fastify is fundamentally tied to Node.js — it cannot run on Cloudflare Workers, Deno, or Bun’s native server. If your deployment target is exclusively Node.js and you need the deepest plugin ecosystem, Fastify remains an excellent choice. If you want runtime flexibility or edge deployment, Hono is the clear winner.

Hono vs. Elysia

Elysia is a Bun-native framework that achieves remarkable performance through tight Bun integration. However, this specialization is also its limitation — Elysia doesn’t run on other runtimes. Hono’s performance on Bun is competitive with Elysia while also supporting every other major runtime. For teams that might ever need to migrate or deploy across multiple platforms, Hono provides insurance that Elysia cannot.

RPC Mode and Client-Side Type Safety

One of Hono’s most innovative features is its RPC mode, which generates a fully typed client from your server routes. This creates end-to-end type safety without code generation, GraphQL schemas, or separate API contracts.

When you define routes with Hono’s validator middleware, the framework captures the input and output types. The hc (Hono Client) function then creates a typed HTTP client that mirrors your API structure. Change a route parameter or response shape on the server, and TypeScript immediately flags any mismatched client calls at compile time.

This approach is particularly powerful for full-stack TypeScript applications. Teams working on digital products — especially those using platforms like Toimi for project coordination — benefit from the reduced communication overhead that end-to-end type safety provides. Frontend and backend developers share a single source of truth, eliminating the API documentation drift that plagues larger projects.

Performance Benchmarks

Hono’s performance claims are backed by consistent benchmark results across runtimes. On Cloudflare Workers, Hono handles over 320,000 requests per second in simple routing benchmarks — significantly faster than alternatives like itty-router or worktop. On Bun, Hono achieves throughput comparable to Elysia while offering broader compatibility. On Node.js, Hono outperforms Express by 3-5x and matches Fastify in most scenarios.

The secret lies in Hono’s router architecture. The RegExpRouter compiles all registered routes into a single optimized regular expression at application startup. This means route matching is effectively O(1) regardless of how many routes your application defines — a significant advantage for large APIs with hundreds of endpoints.

Memory footprint is equally impressive. A minimal Hono application consumes roughly 2-3MB of memory on Node.js, compared to 8-12MB for an equivalent Express application. On edge runtimes where memory is constrained and billed, this efficiency translates directly to cost savings.

JSX and Server-Side Rendering

Beyond API development, Hono includes a built-in JSX renderer that works without React. You can write JSX components that render to HTML strings, enabling server-side rendering directly within your Hono application. This is particularly useful for building lightweight web pages, email templates, or HTML responses without the overhead of a full React setup.

The JSX support integrates seamlessly with Hono’s streaming capabilities. You can stream HTML responses as they’re rendered, improving Time to First Byte for content-heavy pages. Combined with edge deployment, this creates an architecture where HTML is rendered and delivered from the server closest to the user — a pattern that produces exceptionally fast page loads.

Production Deployment Patterns

Deploying Hono applications follows the conventions of your target runtime. For Cloudflare Workers, you use wrangler deploy. For Deno Deploy, you push to a connected Git repository. For Node.js, you run the compiled output with any process manager. For Bun, you use bun run directly.

A common production pattern is deploying the same Hono application to multiple runtimes for redundancy. Your primary deployment might target Cloudflare Workers for global edge distribution, with a Node.js fallback on a traditional cloud provider. Since the application code is identical, the only difference is the entry point adapter — a few lines of runtime-specific bootstrapping.

For monitoring and observability, Hono’s middleware architecture makes it straightforward to integrate with services like OpenTelemetry, Sentry, or Datadog. The Timing middleware adds Server-Timing headers automatically, enabling performance monitoring through standard browser developer tools.

When to Choose Hono

Hono is an excellent choice when you need any of the following: deployment on edge runtimes like Cloudflare Workers, Deno Deploy, or Vercel Edge Functions; a lightweight framework with minimal overhead; multi-runtime compatibility for future flexibility; end-to-end TypeScript type safety; a fast alternative to Express that doesn’t sacrifice developer experience.

Hono may not be the optimal choice when you need the vast Express middleware ecosystem for specialized integrations, when your team is deeply invested in a specific framework’s patterns and tooling, or when you’re building a traditional server-rendered application where a full-stack framework like Next.js or Nuxt would be more appropriate.

The Future of Hono

Hono’s trajectory points toward becoming the standard web framework for the multi-runtime JavaScript era. The Hono project continues to evolve rapidly with features like improved streaming support, enhanced RPC capabilities, and deeper integration with emerging runtime APIs. The community has grown substantially, with contributions from developers across all major runtime ecosystems.

As the JavaScript ecosystem continues to fragment across runtimes and deployment targets, frameworks that embrace Web Standards and runtime portability will define the next generation of web development. Hono is leading that charge with a framework that is fast, small, typed, and genuinely universal.

FAQ

Is Hono production-ready?

Yes. Hono has been used in production by companies of all sizes since 2023. Cloudflare uses Hono internally for several of their own products, and the framework has a stable API with semantic versioning. The ecosystem includes comprehensive documentation, active maintenance, and a growing community. Major versions follow a predictable release cycle with clear migration guides.

Can Hono replace Express in an existing project?

Hono can replace Express, but it’s not a drop-in replacement. The API is intentionally similar — both use app.get(), app.post(), and middleware chaining — but Hono uses Web Standard Request/Response objects instead of Node’s req/res. Express middleware is not directly compatible. However, the migration path is well-documented, and most Express patterns have direct Hono equivalents. For large applications, incremental migration using Hono’s Node.js adapter is a practical approach.

How does Hono handle WebSockets?

Hono provides WebSocket support through its helper utilities. On Cloudflare Workers, it integrates with the Durable Objects WebSocket API. On Bun, it uses Bun’s native WebSocket server. On Node.js, you can pair Hono with libraries like ws. The framework provides a consistent API wrapper, though the underlying implementation varies by runtime. For real-time applications, Hono’s WebSocket support is sufficient for most use cases including chat, notifications, and live updates.

What is the learning curve for Hono if I know Express?

If you’re familiar with Express, you can be productive with Hono within an hour. The routing syntax is nearly identical, middleware follows the same compose-and-chain pattern, and the concept of request/response handling is fundamentally the same. The main differences are learning Hono’s typed context system, the Web Standard Request/Response interface (which is simpler than Express’s API), and the validator middleware for type-safe input parsing. Most developers find Hono’s API more intuitive than Express once they understand the Web Standards approach.

Does Hono support file uploads and multipart form data?

Yes. Hono supports file uploads through the standard Web API FormData interface. You can access uploaded files via c.req.parseBody(), which returns form fields and files. On Cloudflare Workers, uploaded files can be stored in R2 (Cloudflare’s object storage). On Node.js or Bun, you can write files to the filesystem or forward them to cloud storage. Hono also supports streaming uploads for large files, avoiding memory issues that plague frameworks that buffer entire files in memory before processing.