Hono in 2026: The Web Framework That Runs Everywhere

14KB. Zero dependencies. Runs on Cloudflare Workers, Bun, Deno, AWS Lambda, and Node.js — same code, no rewriting. Hono is quietly becoming the default choice for TypeScript APIs.


Express.js was released in 2010. It was built for Node.js, before TypeScript existed, before edge computing, and before the proliferation of JavaScript runtimes we have in 2026. It’s still widely used, but it was never designed for Cloudflare Workers, never designed for edge deployments, and never designed to infer types from your routes.

Hono (炎 — “flame” in Japanese) was. Created by Yusuke Wada in December 2021, it’s built from the ground up on Web Standard APIs — the same Request, Response, and fetch that run natively in every modern JavaScript environment.

The result: one codebase that deploys identically to Cloudflare Workers, Bun, Deno, AWS Lambda, and Node.js. Sub-14KB. Zero dependencies. TypeScript-native end-to-end type safety without code generation.

In 2026, it’s quietly become the framework of choice for TypeScript API developers.


Why Hono Over Express?

Before diving into features, it’s worth understanding the core difference.

Express was designed in 2010 around Node.js-specific APIs. It uses req and res objects tied to the Node.js http module. It cannot run on Cloudflare Workers (which don’t have those APIs). It has no TypeScript support by design — @types/express is a community shim. Every validator, every JWT library, every piece of middleware ships with its own type definitions that don’t compose.

Hono uses the Web Standards Request and Response APIs that every modern runtime implements natively. It was written in TypeScript from day one, and its type system threads through your entire application — from route definitions to validation schemas to response types to the RPC client. Change a field name on the server and your editor immediately highlights the client call that needs updating.

// Express — Node.js-specific APIs, no type inference
app.get('/users/:id', async (req, res) => {
    const id = req.params.id  // string, but Express doesn't know what params exist
    const user = await getUser(id)
    res.json(user)             // response type unknown to caller
})

// Hono — Web Standard APIs, full type inference
app.get('/users/:id', async (c) => {
    const id = c.req.param('id')  // typed to the route pattern
    const user = await getUser(id)
    return c.json(user)           // response type flows to RPC client
})

Getting Started

# Start a new project for your target runtime
npm create hono@latest my-api
# Select: cloudflare-workers | nodejs | bun | deno | aws-lambda | vercel

# Or add to an existing project
npm install hono
// src/index.ts — works on every runtime
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json({ message: 'Hello Hono!' }))

export default app  // Cloudflare Workers / Bun
// or: serve({ fetch: app.fetch, port: 3000 }) for Node.js

To change runtime, swap the adapter import — that’s literally it. The application code doesn’t change.


The Context Object

Hono passes a single c (context) object to every handler. It provides typed access to everything you need:

app.post('/posts', async (c) => {
    // Request
    const body = await c.req.json()           // parsed JSON body
    const id = c.req.param('id')              // URL parameter
    const q = c.req.query('search')           // query string
    const token = c.req.header('Authorization') // request header

    // Response helpers
    return c.json({ post: body }, 201)        // JSON response
    return c.text('Not found', 404)           // plain text
    return c.html('<h1>Hello</h1>')           // HTML
    return c.redirect('/new-url', 301)        // redirect
    return c.notFound()                        // 404 shorthand

    // Environment (Cloudflare Workers bindings, etc.)
    const db = c.env.DATABASE                  // typed environment
})

Validation with Zod — One Source of Truth

Hono integrates with Zod (and Valibot, ArkType via Standard Schema) to validate incoming data. The same schema provides both runtime validation and TypeScript types — no duplication:

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

const CreatePostSchema = z.object({
    title: z.string().min(1).max(100),
    body: z.string().min(10),
    tags: z.array(z.string()).optional(),
})

app.post(
    '/posts',
    zValidator('json', CreatePostSchema),
    async (c) => {
        // c.req.valid('json') returns the validated, typed data
        const { title, body, tags } = c.req.valid('json')
        // TypeScript knows: title is string, body is string, tags is string[] | undefined
        const post = await db.posts.create({ title, body, tags })
        return c.json(post, 201)
    }
)

You can validate json, form, query, param, header, and cookie — all with the same pattern:

app.get(
    '/posts',
    zValidator('query', z.object({
        page: z.coerce.number().min(1).default(1),
        limit: z.coerce.number().min(1).max(100).default(20),
        search: z.string().optional(),
    })),
    async (c) => {
        const { page, limit, search } = c.req.valid('query')
        // page and limit are numbers (coerced from query string)
        const posts = await db.posts.findMany({ page, limit, search })
        return c.json({ posts, page, limit })
    }
)

The RPC Client — End-to-End Type Safety Without Code Generation

This is Hono’s killer feature. Export your app type from the server, import it on the client, and get full type safety across the network boundary — no OpenAPI spec, no codegen, no schema duplication:

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

const app = new Hono()

const routes = app
    .get('/posts', async (c) => {
        const posts = await db.posts.findMany()
        return c.json({ posts })
    })
    .post(
        '/posts',
        zValidator('json', z.object({
            title: z.string(),
            body: z.string(),
        })),
        async (c) => {
            const data = c.req.valid('json')
            const post = await db.posts.create(data)
            return c.json(post, 201)
        }
    )
    .get('/posts/:id', async (c) => {
        const id = c.req.param('id')
        const post = await db.posts.findById(id)
        if (!post) return c.json({ error: 'Not found' }, 404)
        return c.json(post, 200)
    })

// Export the type — this is the "contract"
export type AppType = typeof routes
export default app
// client.ts (React, Vue, another server — anywhere TypeScript runs)
import { hc } from 'hono/client'
import type { AppType } from './server'

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

// Fully typed — autocomplete, parameter types, response types
const postsRes = await client.posts.$get()
const { posts } = await postsRes.json()
// posts is typed to match the server response

const newPostRes = await client.posts.$post({
    json: { title: 'Hello', body: 'World' }
    // TypeScript error if required fields are missing or wrong type
})
const newPost = await newPostRes.json()
// newPost is typed as the 201 response body

// Status-conditional types
const postRes = await client.posts[':id'].$get({ param: { id: '123' } })
if (postRes.status === 404) {
    const { error } = await postRes.json()  // typed as { error: string }
}
if (postRes.ok) {
    const post = await postRes.json()  // typed as Post
}

This is end-to-end type safety without tRPC’s procedure syntax, without GraphQL’s schema language, and without OpenAPI’s code generation step. Just TypeScript.


Middleware — Built-In and Custom

Hono ships a comprehensive set of built-in middleware:

import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'
import { rateLimiter } from 'hono/rate-limiter'
import { bearerAuth } from 'hono/bearer-auth'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { secureHeaders } from 'hono/secure-headers'
import { timeout } from 'hono/timeout'

const app = new Hono()

// Global middleware
app.use('*', logger())
app.use('*', cors({ origin: 'https://myapp.com' }))
app.use('*', secureHeaders())
app.use('*', compress())

// Route-specific middleware
app.use('/api/*', timeout(10_000))
app.use('/api/v1/*', jwt({ secret: process.env.JWT_SECRET! }))

Custom middleware with typed context variables:

import { createMiddleware } from 'hono/factory'

// Define the variables this middleware sets on context
type Variables = {
    user: { id: string; email: string; role: string }
}

const authMiddleware = createMiddleware<{ Variables: Variables }>(
    async (c, next) => {
        const token = c.req.header('Authorization')?.replace('Bearer ', '')
        if (!token) return c.json({ error: 'Unauthorized' }, 401)

        const user = await verifyToken(token)
        if (!user) return c.json({ error: 'Invalid token' }, 401)

        c.set('user', user)  // typed — c.get('user') returns the user type
        await next()
    }
)

app.get('/profile', authMiddleware, (c) => {
    const user = c.get('user')  // { id: string; email: string; role: string }
    return c.json(user)
})

Route Organization — app.route() and Subapps

Scale your Hono app with routers:

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

export const postsRouter = new Hono()
    .get('/', (c) => c.json({ posts: [] }))
    .post('/', zValidator('json', z.object({ title: z.string() })), async (c) => {
        const { title } = c.req.valid('json')
        return c.json({ title }, 201)
    })
    .delete('/:id', async (c) => {
        const id = c.req.param('id')
        await deletePost(id)
        return c.body(null, 204)
    })

export type PostsRouter = typeof postsRouter

// routes/users.ts
export const usersRouter = new Hono()
    .get('/', (c) => c.json({ users: [] }))
    .get('/:id', (c) => c.json({ id: c.req.param('id') }))

export type UsersRouter = typeof usersRouter
// index.ts
import { Hono } from 'hono'
import { postsRouter } from './routes/posts'
import { usersRouter } from './routes/users'

const app = new Hono()
    .route('/posts', postsRouter)
    .route('/users', usersRouter)

// Export the full type for the RPC client
export type AppType = typeof app
export default app

Error Handling

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

// Throw typed HTTP errors from anywhere
app.get('/posts/:id', async (c) => {
    const post = await db.posts.findById(c.req.param('id'))
    if (!post) {
        throw new HTTPException(404, { message: 'Post not found' })
    }
    return c.json(post)
})

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

// 404 handler
app.notFound((c) => c.json({ error: 'Route not found' }, 404))

Multi-Runtime Deployment

The same Hono application deploys to any runtime by changing the entry point:

// For Cloudflare Workers (wrangler.toml setup)
export default app  // that's it

// For Node.js
import { serve } from '@hono/node-server'
serve({ fetch: app.fetch, port: 3000 })

// For Bun
export default { port: 3000, fetch: app.fetch }

// For Deno
Deno.serve(app.fetch)

// For AWS Lambda
import { handle } from 'hono/aws-lambda'
export const handler = handle(app)

// For Vercel Edge Functions
export const config = { runtime: 'edge' }
export default app

When your team decides to move from Node.js to Cloudflare Workers, or add a Lambda function alongside your main app, the routing logic doesn’t change at all.


When to Use Hono

Hono is the right choice for:

  • APIs deployed to Cloudflare Workers, AWS Lambda, or edge runtimes
  • TypeScript teams that want end-to-end type safety without code generation
  • Any new TypeScript API — the DX is better than Express even on Node.js
  • Microservices that need to run in multiple environments
  • CLI tools or libraries with a built-in HTTP server
  • Full-stack apps alongside a Hono RPC client in Vue/React

Consider Express or Fastify if:

  • You’re maintaining a large existing Express codebase (migration cost > benefit)
  • You need C++ native addons that only work with Node.js
  • Your team has deep existing investment in the Express ecosystem

The honest benchmark take: Hono is genuinely faster than Express in synthetic benchmarks (≈3× in hello-world, less in real apps). But its real advantage isn’t raw throughput — it’s the TypeScript story, the runtime portability, and the minimal footprint that matters on cold-start-sensitive environments.


Final Thoughts

Hono is the framework that Express should have been if it were designed in 2021. Built on Web Standards, TypeScript-native from day one, lightweight enough for edge environments, and versatile enough for traditional Node.js deployments.

The RPC client is the feature that will keep you coming back. Writing an API with Zod validation, exporting the type, and getting full autocompletion on every API call from your frontend — with TypeScript catching mismatches at compile time rather than runtime — is a genuinely better way to build full-stack TypeScript applications.

For new projects in 2026, Hono is the default recommendation. For existing Express projects, the migration is incremental — start with one route file, export the type, wire up the RPC client, and see how it feels.

One framework, every runtime, zero compromises on type safety.

Leave a Reply

Your email address will not be published. Required fields are marked *