Skip to content

Web Development · Backend Frameworks

Hono in 2026: The Web Framework Built for the Edge

Hono started as a tiny router for Cloudflare Workers and became the default for edge APIs. What it does well, and why "runs everywhere" actually holds.

Anurag Verma

Anurag Verma

7 min read

Hono in 2026: The Web Framework Built for the Edge

Sponsored

Share

When Cloudflare Workers first became mainstream, the available framework options were thin. Express didn’t work because it depended on Node.js APIs. Fastify had the same problem. You were writing routing logic by hand, matching path strings with regex and tediously parsing request bodies.

Hono came out of that constraint. It was built from scratch to run in V8-based runtimes without any Node.js dependencies. In 2026, it runs on Cloudflare Workers, Deno Deploy, Bun, Node.js, Vercel Edge Functions, AWS Lambda, Netlify Edge Functions, and the browser. The same application code works across all of them with minimal configuration changes.

That’s a real claim. Here’s what it actually means in practice.

The Core API

Hono’s routing API will be immediately familiar if you’ve used Express or Fastify:

import { Hono } from 'hono'

const app = new Hono()

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

app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  const user = await db.users.findOne(id)

  if (!user) return c.json({ error: 'Not found' }, 404)
  return c.json(user)
})

app.post('/users', async (c) => {
  const body = await c.req.json()
  const user = await db.users.create(body)
  return c.json(user, 201)
})

export default app

The context object c is the main interface. It provides request parsing, response helpers, cookies, headers, and environment access. The API is small enough to learn in an hour.

The runtime-specific entry points look like this:

// Cloudflare Workers — export default is enough
export default app

// Node.js
import { serve } from '@hono/node-server'
serve(app)

// Bun
Bun.serve(app)

// Deno
Deno.serve(app.fetch)

The app.fetch method is a standard Request => Response function, the same interface used by all edge runtimes. That’s the portability mechanism.

Middleware

Hono ships built-in middleware for the things you need on every API:

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

const app = new Hono()

// Apply globally
app.use('*', logger())
app.use('*', cors({
  origin: ['https://app.example.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))

// Apply to specific routes
app.use('/api/*', bearerAuth({ token: process.env.API_TOKEN }))
app.use('/api/*', prettyJSON())

The built-in list covers: logger, CORS, bearer auth, basic auth, JWT, compress, ETag, cache, rate limiting, CSRF, body limit, and several more. Most small-to-medium APIs don’t need third-party middleware packages.

Custom middleware follows the same pattern as any middleware system:

app.use('/admin/*', async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')

  if (!token) return c.json({ error: 'Unauthorized' }, 401)

  try {
    const payload = await verifyJWT(token, c.env.JWT_SECRET)
    c.set('userId', payload.sub)
    await next()
  } catch {
    return c.json({ error: 'Invalid token' }, 401)
  }
})

// Downstream handlers can read from context
app.get('/admin/dashboard', (c) => {
  const userId = c.get('userId')
  return c.json({ userId, dashboard: '...' })
})

TypeScript Support

Hono’s TypeScript integration is well-designed. You can define environment bindings (for Cloudflare Workers), variables, and custom context values with full type safety:

type Bindings = {
  DATABASE_URL: string
  JWT_SECRET: string
  KV_STORE: KVNamespace // Cloudflare KV type
}

type Variables = {
  userId: string
}

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

app.use('/protected/*', async (c, next) => {
  // c.env.DATABASE_URL is typed as string
  // c.env.KV_STORE is typed as KVNamespace
  c.set('userId', 'user_123') // type-checked
  await next()
})

app.get('/protected/profile', (c) => {
  const userId = c.get('userId') // typed as string
  return c.json({ userId })
})

RPC is where the TypeScript goes furthest. Hono can generate a fully-typed client from your route definitions:

// server.ts
const routes = app
  .get('/users/:id', (c) => {
    return c.json({ id: c.req.param('id'), name: 'Alice' })
  })
  .post('/users', async (c) => {
    const body = await c.req.json<{ name: string }>()
    return c.json({ id: 'new', ...body }, 201)
  })

export type AppType = typeof routes

// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('https://api.example.com')

// Fully typed — parameter names, response shapes, everything
const user = await client.users[':id'].$get({ param: { id: '123' } })
const json = await user.json()
// json is typed as { id: string, name: string }

The RPC client is the closest thing in the JavaScript ecosystem to tRPC without requiring a specific framework. If your frontend and backend are in the same repo, this removes almost all manual type duplication.

Routing

Hono supports grouped routes, which makes larger APIs manageable:

import { Hono } from 'hono'

const api = new Hono().basePath('/api')

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

const products = new Hono()
products.get('/', (c) => c.json({ products: [] }))
products.post('/', async (c) => c.json(await c.req.json(), 201))

api.route('/users', users)
api.route('/products', products)

export default api

This keeps each resource in its own file and composes cleanly. The router is a trie-based implementation, which keeps performance predictable as route counts grow.

Performance

Hono is fast. On Cloudflare Workers, where cold start time matters, it adds essentially no overhead. The bundle size is around 14KB (uncompressed) for the core, which matters on edge runtimes where bundle size affects startup time.

The benchmark comparisons are Hono’s homepage selling point, and the numbers are real for edge runtimes. On Node.js, the differences between Hono and Fastify or Elysia are smaller and mostly irrelevant for typical API workloads. The performance case for Hono on Node.js is about consistency with your edge deployments, not about raw throughput.

When Hono Is the Right Choice

Building a Cloudflare Workers API. This is the original use case and still the strongest one. Hono was designed for Workers and the integration is frictionless. Environment bindings, Durable Objects, KV, R2, D1, all work naturally through the context.

An edge-deployed backend that needs to run locally too. If you’re deploying to Vercel Edge Functions or Netlify Edge, Hono’s portability means your local dev server runs the same code without emulation layers.

An API that needs to be small and dependency-light. Serverless cold starts are sensitive to bundle size. A Hono API with a few middleware packages is far smaller than an equivalent Express/Fastify setup with the same ecosystem dependencies.

A team already using TypeScript that wants end-to-end type safety. The RPC client pairs well with React apps and makes type-safe API calls without code generation steps.

Where It Doesn’t Fit

For a conventional Node.js server-rendered app (not edge), Hono’s advantages are less clear. Fastify is faster on raw Node.js benchmarks, has a larger plugin ecosystem, and has more production track record on that runtime. Neither is a strong argument against Hono, but if your stack is firmly Node.js, the “runs everywhere” advantage doesn’t apply.

For apps that need heavy request processing (file parsing, streaming, complex multipart handling) Hono’s streaming support exists but is less developed than mature Node.js frameworks. These are fixable gaps, not architectural limitations.

Getting Started

# Cloudflare Workers
npm create cloudflare@latest my-api -- --template hono

# Node.js
npm create hono@latest my-api
# Select Node.js runtime

# Bun
bun create hono my-api

The scaffold includes the runtime-specific entry point, basic routing, and TypeScript configuration. From there, it’s just adding routes.

Hono filled a specific gap (a fast, minimal, TypeScript-first router that didn’t need Node.js), and it filled it well. In 2026 the edge runtime ecosystem has matured significantly, and Hono is the default framework choice for anything running outside a traditional Node.js server.

Frequently asked questions

Does "runs everywhere" actually hold in practice?
Yes, more than most such claims. Because Hono targets the standard Request and Response objects rather than Node's http module, the application code genuinely is the same across runtimes; what changes is a small runtime-specific entry point that hands app.fetch to the platform. Where it gets less clean is anything platform-bound, Cloudflare KV or D1 bindings for example, which naturally will not move to Lambda unchanged.
Should I use Hono instead of Fastify on Node.js?
Only if you are also deploying to the edge and want one codebase. On Node alone, Fastify is faster on raw benchmarks, has a much larger plugin ecosystem, and has far more production mileage on that runtime. The honest case for Hono on Node is consistency with your edge deployments, not throughput.
How does Hono RPC compare to tRPC?
The idea is the same, end-to-end types without a code generation step, but the coupling is different. tRPC needs its own router definition that both ends import. Hono derives the client type from the routes you already wrote, so there is nothing extra to maintain. The tradeoff is that your client is typed against the server's actual implementation rather than an agreed contract, which is fine in a monorepo and less so across team boundaries.
Where does Hono fall short?
Heavy request processing. File parsing, complex multipart handling, and long-lived streaming are supported but less mature than in established Node frameworks. These read as gaps that will close rather than architectural limits, but if your service spends most of its time shovelling large request bodies around, test before committing.
Why does bundle size matter so much on edge runtimes?
Because on a cold start the runtime has to fetch, parse and initialise your bundle before it can answer the first request, and edge platforms create isolates far more often than a long-lived Node process restarts. A 14KB core plus a couple of middleware is in a different class from an Express or Fastify setup with its ecosystem dependencies, and that difference shows up directly in p99 latency.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored