Skip to main content
← All Articles Β· Β· 11 min read

Hono.js Complete Guide 2026: Ultra-Fast Edge Web Framework

Complete guide to Hono.js in 2026. Routing, middleware, TypeScript, deployment to Cloudflare Workers, Deno, and Bun. Build blazing-fast edge APIs with real code examples.

honocloudflare-workersedge-computingtypescriptweb-framework

Hono.js has emerged as one of the most compelling web frameworks of the past two years. Originally designed for Cloudflare Workers, it has grown into a universal edge-first framework that runs on Bun, Deno, Node.js, Fastly, and AWS Lambda β€” all from a single codebase. If you have not looked at Hono seriously yet, 2026 is the year to start.

This guide walks through everything you need to know: the architecture that makes Hono fast, routing and middleware, TypeScript integration, JSX support, RPC mode, and step-by-step deployment to the three most popular runtimes.

What Is Hono.js

Hono (η‚Ž, meaning β€œflame” in Japanese) is a small, fast, and multi-runtime web framework built on Web Standard APIs β€” specifically the Fetch API and the Request/Response primitives. Because it relies only on APIs that exist natively in every modern JavaScript runtime, Hono code needs almost no adaptation to run in different environments.

The key design decisions that define Hono:

  • Zero dependencies β€” the core package ships no third-party runtime dependencies.
  • Tiny bundle size β€” the core is around 14 kB minified. This matters enormously on edge platforms where cold starts are measured in milliseconds.
  • Web Standards first β€” every handler receives a standard Request and returns a standard Response. No proprietary req/res objects to learn.
  • TypeScript native β€” the entire framework is written in TypeScript and ships full type definitions, including route-level type inference.

Why Hono Is Fast

Speed in web frameworks comes from several places: routing algorithm efficiency, middleware overhead, serialization cost, and runtime startup time.

Hono uses a Trie-based RegExpRouter as its default router. Route matching is O(log n) in the average case and avoids the linear scan that older frameworks perform. For applications with many routes, this difference compounds quickly.

The framework avoids class instantiation overhead by leaning on closures and plain functions wherever possible. Middleware is composed as a flat async chain rather than a deeply nested callback tree, which reduces call stack depth and makes stack traces readable.

On edge runtimes like Cloudflare Workers, the cold start advantage is decisive. A Hono application initializes in under 1 ms. An equivalent Express application running on Node.js in a Lambda function can take 300-800 ms to cold start. For user-facing APIs, that gap is the difference between a fast product and a slow one.

Installation and Basic Setup

Installing Hono depends on your runtime. For Node.js and Bun, use npm:

npm install hono
# or
bun add hono

For Cloudflare Workers with Wrangler:

npm create cloudflare@latest my-api -- --template hono

A minimal Hono application looks like this:

import { Hono } from 'hono'

const app = new Hono()

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

export default app

The c parameter is a Context object. It wraps the incoming Request and provides helper methods for building responses. You will use c.req, c.res, c.json(), c.text(), c.html(), and c.redirect() constantly.

Routing API

Hono’s routing API is clean and consistent. Every HTTP method has a matching method on the app instance:

import { Hono } from 'hono'

const app = new Hono()

// Basic methods
app.get('/users', (c) => c.json({ users: [] }))
app.post('/users', async (c) => {
  const body = await c.req.json()
  return c.json({ created: body }, 201)
})
app.put('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ updated: id })
})
app.delete('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ deleted: id })
})

// Wildcard route
app.get('/files/*', (c) => {
  const path = c.req.param('*')
  return c.text(`Serving: ${path}`)
})

// Match any method
app.all('/health', (c) => c.json({ status: 'ok' }))

export default app

Route Groups

Hono supports route grouping, which is essential for organizing larger APIs:

import { Hono } from 'hono'

const app = new Hono()

// Create a sub-application for /api/v1
const v1 = new Hono()

v1.get('/products', (c) => c.json({ products: [] }))
v1.get('/products/:id', (c) => c.json({ id: c.req.param('id') }))
v1.post('/products', async (c) => {
  const data = await c.req.json()
  return c.json({ created: data }, 201)
})

// Mount at prefix
app.route('/api/v1', v1)

export default app

This pattern scales cleanly. Each sub-application is independently testable and can define its own middleware chain.

Middleware System

Middleware in Hono follows the same signature as route handlers β€” a function that receives a Context and a next function. Calling await next() passes control to the next handler in the chain.

import { Hono } from 'hono'

const app = new Hono()

// Global middleware β€” runs on every request
app.use('*', async (c, next) => {
  console.log(`${c.req.method} ${c.req.url}`)
  const start = Date.now()
  await next()
  const elapsed = Date.now() - start
  c.res.headers.set('X-Response-Time', `${elapsed}ms`)
})

// Route-specific middleware
const requireAuth = async (c: any, next: any) => {
  const token = c.req.header('Authorization')
  if (!token || !token.startsWith('Bearer ')) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  await next()
}

app.get('/protected', requireAuth, (c) => c.json({ secret: 'data' }))

export default app

Built-in Middleware

Hono ships a solid set of built-in middleware packages. They are opt-in via separate imports, so you only pay the bundle cost for what you use:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { compress } from 'hono/compress'
import { bearerAuth } from 'hono/bearer-auth'
import { rateLimiter } from 'hono/rate-limiter'

const app = new Hono()

app.use(logger())
app.use(compress())
app.use(prettyJSON())

app.use('/api/*', cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
}))

app.use('/api/*', bearerAuth({ token: process.env.API_SECRET! }))

export default app

TypeScript Integration

Hono’s TypeScript support goes well beyond basic type definitions. It provides route-level type inference, which means you can define the expected shape of route parameters, query strings, and JSON bodies, and the framework enforces them at compile time.

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

// Define typed environment for bindings (Cloudflare Workers)
type Env = {
  Bindings: {
    DATABASE_URL: string
    API_KEY: string
  }
  Variables: {
    userId: string
  }
}

const app = new Hono<Env>()

// Zod validation with automatic type inference
const createUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'viewer']),
})

app.post(
  '/users',
  zValidator('json', createUserSchema),
  async (c) => {
    // data is fully typed as { name: string; email: string; role: 'admin' | 'user' | 'viewer' }
    const data = c.req.valid('json')

    // Access typed environment bindings
    const dbUrl = c.env.DATABASE_URL

    return c.json({ created: data }, 201)
  }
)

// Query parameter validation
const listUsersSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  role: z.enum(['admin', 'user', 'viewer']).optional(),
})

app.get('/users', zValidator('query', listUsersSchema), (c) => {
  const { page, limit, role } = c.req.valid('query')
  return c.json({ page, limit, role, users: [] })
})

export default app

The @hono/zod-validator adapter is the standard approach for runtime validation. It integrates with Zod’s inference so validated data is immediately typed without additional type assertions.

JSX Support

Hono supports JSX for server-side rendering. You do not need React β€” Hono ships its own JSX runtime that produces HTML strings with zero client-side JavaScript overhead:

// index.tsx
import { Hono } from 'hono'
import { html } from 'hono/html'

const app = new Hono()

// Using JSX
const Layout = ({ children, title }: { children: any; title: string }) => (
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>{title}</title>
    </head>
    <body>{children}</body>
  </html>
)

const ProductCard = ({ name, price }: { name: string; price: number }) => (
  <div class="card">
    <h2>{name}</h2>
    <p>${price.toFixed(2)}</p>
  </div>
)

app.get('/products', (c) => {
  const products = [
    { name: 'Widget Pro', price: 29.99 },
    { name: 'Gadget Plus', price: 49.99 },
  ]

  return c.html(
    <Layout title="Products">
      <h1>Our Products</h1>
      {products.map((p) => (
        <ProductCard name={p.name} price={p.price} />
      ))}
    </Layout>
  )
})

export default app

Set "jsx": "react-jsx" and "jsxImportSource": "hono/jsx" in your tsconfig.json to enable JSX compilation.

RPC Mode

RPC mode is one of Hono’s most powerful and distinctive features. It generates a fully type-safe client from your server route definitions β€” no code generation step required, no schema files to maintain.

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

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

export type AppType = typeof app
export default app
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'

// The client is fully typed β€” autocomplete works on routes, params, and responses
const client = hc<AppType>('https://api.example.com')

// Fetch all posts β€” response type is inferred automatically
const res = await client.posts.$get()
const { posts } = await res.json()

// Create a post β€” TypeScript enforces the request body shape
const createRes = await client.posts.$post({
  json: { title: 'New Post', body: 'Content here' },
})
const created = await createRes.json()

// Route parameters are type-checked
const postRes = await client.posts[':id'].$get({ param: { id: '1' } })
const post = await postRes.json()

RPC mode eliminates an entire category of client-server type drift bugs. If you change a route’s response shape on the server, TypeScript immediately flags every client call that assumes the old shape.

Practical REST API Tutorial

Here is a complete REST API for a bookmark manager, demonstrating all the patterns covered so far:

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

// In-memory store for demonstration
const bookmarks: { id: number; url: string; title: string; tags: string[] }[] = []
let nextId = 1

const bookmarkSchema = z.object({
  url: z.string().url(),
  title: z.string().min(1).max(200),
  tags: z.array(z.string()).default([]),
})

const app = new Hono()

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

// List bookmarks with optional tag filter
app.get('/bookmarks', (c) => {
  const tag = c.req.query('tag')
  const results = tag
    ? bookmarks.filter((b) => b.tags.includes(tag))
    : bookmarks
  return c.json({ bookmarks: results, total: results.length })
})

// Get single bookmark
app.get('/bookmarks/:id', (c) => {
  const id = Number(c.req.param('id'))
  const bookmark = bookmarks.find((b) => b.id === id)
  if (!bookmark) {
    return c.json({ error: 'Bookmark not found' }, 404)
  }
  return c.json(bookmark)
})

// Create bookmark
app.post('/bookmarks', zValidator('json', bookmarkSchema), (c) => {
  const data = c.req.valid('json')
  const bookmark = { id: nextId++, ...data }
  bookmarks.push(bookmark)
  return c.json(bookmark, 201)
})

// Update bookmark
app.put(
  '/bookmarks/:id',
  zValidator('json', bookmarkSchema.partial()),
  (c) => {
    const id = Number(c.req.param('id'))
    const index = bookmarks.findIndex((b) => b.id === id)
    if (index === -1) {
      return c.json({ error: 'Bookmark not found' }, 404)
    }
    const updates = c.req.valid('json')
    bookmarks[index] = { ...bookmarks[index], ...updates }
    return c.json(bookmarks[index])
  }
)

// Delete bookmark
app.delete('/bookmarks/:id', (c) => {
  const id = Number(c.req.param('id'))
  const index = bookmarks.findIndex((b) => b.id === id)
  if (index === -1) {
    return c.json({ error: 'Bookmark not found' }, 404)
  }
  bookmarks.splice(index, 1)
  return c.body(null, 204)
})

export default app

Deployment Targets

Cloudflare Workers

Cloudflare Workers is Hono’s home runtime. Deploy with Wrangler:

# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"

[vars]
ENVIRONMENT = "production"
// src/index.ts
import { Hono } from 'hono'

type Bindings = {
  KV: KVNamespace
  DB: D1Database
  ENVIRONMENT: string
}

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

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

export default app
wrangler deploy

Deno Deploy

// main.ts
import { Hono } from 'npm:hono'
import { serve } from 'https://deno.land/std/http/server.ts'

const app = new Hono()

app.get('/', (c) => c.json({ runtime: 'Deno', version: Deno.version.deno }))
app.get('/env/:key', (c) => {
  const key = c.req.param('key')
  const value = Deno.env.get(key) ?? null
  return c.json({ key, value })
})

serve(app.fetch)

Deploy with the Deno CLI:

deployctl deploy --project=my-api main.ts

Bun Server

// server.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json({ runtime: 'Bun', version: Bun.version }))
app.get('/file/:name', async (c) => {
  const name = c.req.param('name')
  const file = Bun.file(`./public/${name}`)
  const exists = await file.exists()
  if (!exists) return c.json({ error: 'File not found' }, 404)
  return new Response(file)
})

export default {
  port: 3000,
  fetch: app.fetch,
}
bun run server.ts

Hono vs Express vs Fastify

Choosing a framework depends on your context. Here is an honest comparison across the dimensions that matter most in 2026:

CriterionHonoExpressFastify
Runtime supportEdge, Bun, Deno, NodeNode onlyNode, Bun
Bundle size~14 kB~200 kB~120 kB
Requests/sec (benchmark)~500k+~80k~350k
TypeScriptFirst-class, built-inExternal types onlyGood, improving
Ecosystem maturityGrowing fastMassive, 15+ yearsLarge, stable
Middleware compatibilityHono-specificMassive npm ecosystemFastify plugin system
RPC / type-safe clientYes (built-in)NoVia external libs
JSX / SSRYes (built-in)NoNo
Learning curveLowVery lowMedium

Use Hono when you are targeting edge runtimes, need a type-safe client-server contract, want multi-runtime portability, or are building a new project from scratch.

Use Express when you need maximum npm middleware compatibility, are extending an existing Express codebase, or have team members already familiar with it. Express is not fast, but it is predictable and well-documented.

Use Fastify when you are running on Node.js and need higher throughput than Express without migrating to edge runtimes. Fastify’s schema-based validation with JSON Schema is excellent for JSON-heavy APIs.

The honest summary: for new projects in 2026, Hono is the default choice unless you have a specific reason to reach for one of the others. Its performance advantage on edge runtimes is substantial, and the TypeScript integration β€” especially RPC mode β€” eliminates real categories of bugs.

Testing Hono Applications

Hono’s adherence to Web Standards makes testing straightforward. The app.request() method accepts a URL string or a full Request object and returns a Response β€” no HTTP server needed:

import { describe, it, expect } from 'vitest'
import app from './server'

describe('Bookmarks API', () => {
  it('returns empty list initially', async () => {
    const res = await app.request('/bookmarks')
    expect(res.status).toBe(200)
    const body = await res.json()
    expect(body.bookmarks).toEqual([])
  })

  it('creates a bookmark', async () => {
    const res = await app.request('/bookmarks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        url: 'https://hono.dev',
        title: 'Hono Documentation',
        tags: ['framework', 'edge'],
      }),
    })
    expect(res.status).toBe(201)
    const body = await res.json()
    expect(body.url).toBe('https://hono.dev')
    expect(body.id).toBeDefined()
  })

  it('rejects invalid URLs', async () => {
    const res = await app.request('/bookmarks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: 'not-a-url', title: 'Bad' }),
    })
    expect(res.status).toBe(400)
  })
})

Run tests with Vitest:

npx vitest run

No test server setup, no port conflicts, no async teardown issues. Tests are fast because they never touch the network stack.

Error Handling

Hono provides a global error handler and a not-found handler:

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

const app = new Hono()

// Custom not-found response
app.notFound((c) => {
  return c.json({ error: `Route ${c.req.path} not found` }, 404)
})

// 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)
})

// Throw HTTPException in any handler
app.get('/admin', (c) => {
  const role = c.req.header('X-User-Role')
  if (role !== 'admin') {
    throw new HTTPException(403, { message: 'Admin access required' })
  }
  return c.json({ admin: true })
})

export default app

HTTPException carries a status code and optional message. The global error handler catches it and formats a consistent response. This pattern keeps error handling out of individual route handlers while giving you full control over response format.

What to Build With Hono in 2026

Hono is a strong fit for several categories of projects:

Edge API gateways β€” authentication, rate limiting, request routing, and response caching at the CDN edge, before traffic reaches your origin.

Backend for Frontend (BFF) layers β€” aggregate multiple microservice calls into optimized responses for specific frontend views, running at the edge for low latency.

Full-stack Cloudflare applications β€” combine Hono with Cloudflare Workers, D1 (SQLite), KV, R2 (object storage), and Queues to build complete applications with no traditional server.

Type-safe internal APIs β€” use RPC mode to build internal services where the TypeScript types flow automatically from server to client, eliminating manual API documentation for team-internal services.

Server-side rendering β€” use Hono’s JSX runtime for lightweight SSR without shipping a React bundle to the client. Combine with htmx for dynamic interactions.

Summary

Hono.js has earned its place as the leading edge web framework in 2026. Its advantages β€” tiny footprint, Web Standards foundation, first-class TypeScript, RPC mode, and multi-runtime support β€” address real problems that developers face when building modern APIs and web services.

The framework is mature enough for production use (major companies run it in production today) while still being small enough to understand completely. The source code is readable, the documentation is thorough, and the community is active.

If you are starting a new API project today, the practical choice is to start with Hono, deploy to Cloudflare Workers, and use RPC mode for any TypeScript clients. That combination gives you excellent performance, strong type safety, and low operational complexity.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.