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

Hono.js Complete Guide 2026: Build Ultra-Fast APIs with TypeScript

Learn Hono.js from scratch in 2026. Complete guide covering routing, middleware, Zod validation, and deploying to Cloudflare Workers and Bun. Includes Express.js performance comparison.

honotypescriptapiedgebuncloudflare-workers

Hono.js Complete Guide 2026: Build Ultra-Fast APIs with TypeScript

Hono.js has become the go-to framework for edge-first TypeScript APIs. If you’re still reaching for Express.js for new projects, you’re leaving serious performance on the table. This guide covers everything you need to build production-ready APIs with Hono in 2026.

What is Hono.js?

Hono (η‚Ž β€” β€œflame” in Japanese) is an ultrafast web framework designed for edge runtimes. It runs natively on:

  • Cloudflare Workers
  • Bun
  • Deno
  • Node.js
  • AWS Lambda
  • Vercel Edge Functions
  • Fastly Compute

The same Hono code works across all these runtimes β€” a genuine write-once, deploy-anywhere experience.

Performance: Hono vs Express.js vs Fastify

Real-world benchmarks on equivalent hardware (Node.js 22, simple JSON response endpoint):

FrameworkRequests/secLatency (p99)Bundle Size
Hono (Bun)~210,0000.8ms14 KB
Hono (Node.js)~95,0001.2ms14 KB
Fastify~85,0001.8ms78 KB
Express.js~22,0006.5ms210 KB
Koa~28,0005.1ms45 KB

Hono on Cloudflare Workers is even faster β€” sub-millisecond cold starts because Workers run on V8 isolates, not containers.

The performance gap comes from Hono’s design choices: no dependency, tree-shakable, uses the standard Request/Response Web API, and has a hyper-optimized router (RegExpRouter for dynamic routes, TrieRouter as fallback).


Installation

# For Cloudflare Workers (recommended starting point)
npm create cloudflare@latest my-api -- --template hono

# For Bun
bun create hono my-api

# For Node.js
npm install hono @hono/node-server

For Node.js adapter, your entry point:

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

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

serve({ fetch: app.fetch, port: 3000 })

Core Concepts: Routing

Hono’s routing API is familiar to Express developers, but typed end-to-end:

import { Hono } from 'hono'

const app = new Hono()

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

// Named parameters
app.get('/posts/:id/comments/:commentId', (c) => {
  const { id, commentId } = c.req.param()
  return c.json({ postId: id, commentId })
})

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

Route Groups with Hono Instances

import { Hono } from 'hono'

const app = new Hono()

// Users sub-app
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) => {
  const data = await c.req.json()
  return c.json(data, 201)
})

// Posts sub-app
const posts = new Hono()
posts.get('/', (c) => c.json({ posts: [] }))
posts.get('/:slug', (c) => c.json({ slug: c.req.param('slug') }))

// Mount under base paths
app.route('/api/users', users)
app.route('/api/posts', posts)

export default app

Middleware

Hono middleware follows the same pattern as Express but uses async/await natively:

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
import { timing } from 'hono/timing'

const app = new Hono()

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

// Only pretty JSON in development
if (process.env.NODE_ENV === 'development') {
  app.use('*', prettyJSON())
}

Custom Middleware

import { createMiddleware } from 'hono/factory'

// Rate limiting middleware
const rateLimit = createMiddleware(async (c, next) => {
  const ip = c.req.header('cf-connecting-ip') ?? 'unknown'
  const key = `rate:${ip}`

  // Example with Cloudflare KV
  const current = parseInt(await c.env.KV.get(key) ?? '0')

  if (current >= 100) {
    return c.json({ error: 'Rate limit exceeded' }, 429)
  }

  await c.env.KV.put(key, String(current + 1), { expirationTtl: 60 })
  await next()
})

// Auth middleware
const authMiddleware = createMiddleware(async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')

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

  // Verify token (example with JWT)
  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)
  }
})

// Apply to specific routes
app.use('/api/protected/*', authMiddleware)
app.use('/api/*', rateLimit)

Validation with Zod

Hono integrates with Zod through @hono/zod-validator:

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

const app = new Hono()

// Schema definitions
const CreateUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(13).max(120).optional(),
  role: z.enum(['admin', 'user', 'moderator']).default('user'),
})

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

// Validate request body
app.post(
  '/users',
  zValidator('json', CreateUserSchema),
  async (c) => {
    const data = c.req.valid('json') // Fully typed
    // data.name: string
    // data.email: string
    // data.role: 'admin' | 'user' | 'moderator'

    const user = await db.createUser(data)
    return c.json(user, 201)
  }
)

// Validate query params
app.get(
  '/users',
  zValidator('query', QuerySchema),
  async (c) => {
    const { page, limit, search } = c.req.valid('query')
    const users = await db.findUsers({ page, limit, search })
    return c.json({ users, page, limit })
  }
)

// Validate path params
app.get(
  '/users/:id',
  zValidator('param', z.object({ id: z.string().uuid() })),
  async (c) => {
    const { id } = c.req.valid('param')
    const user = await db.findUserById(id)
    if (!user) return c.json({ error: 'Not found' }, 404)
    return c.json(user)
  }
)

Custom Error Handler for Validation

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

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

// Custom Zod error response
app.post(
  '/users',
  zValidator('json', CreateUserSchema, (result, c) => {
    if (!result.success) {
      return c.json({
        error: 'Validation failed',
        issues: result.error.issues.map(i => ({
          field: i.path.join('.'),
          message: i.message,
        })),
      }, 422)
    }
  }),
  async (c) => {
    const data = c.req.valid('json')
    return c.json(await createUser(data), 201)
  }
)

Deployment

Cloudflare Workers

npm create cloudflare@latest -- --template hono

Your wrangler.toml:

name = "my-hono-api"
main = "src/index.ts"
compatibility_date = "2026-03-01"

[[kv_namespaces]]
binding = "SESSIONS"
id = "your-kv-namespace-id"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"

[vars]
ENVIRONMENT = "production"

Your src/index.ts:

import { Hono } from 'hono'

type Bindings = {
  DB: D1Database
  SESSIONS: KVNamespace
  JWT_SECRET: string
  ENVIRONMENT: string
}

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

app.get('/health', (c) => c.json({
  status: 'ok',
  env: c.env.ENVIRONMENT,
}))

// Access D1 database
app.get('/users', async (c) => {
  const { results } = await c.env.DB.prepare('SELECT * FROM users LIMIT 20').all()
  return c.json(results)
})

export default app

Deploy:

wrangler deploy

Bun

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

const app = new Hono()

app.get('/', (c) => c.text('Running on Bun!'))

export default {
  port: 3000,
  fetch: app.fetch,
}
bun run src/index.ts
# Or for production:
bun build src/index.ts --outfile dist/index.js
bun dist/index.js

Bun’s native HTTP server handles the fetch interface directly β€” no adapter needed.


Hono vs Express.js: When to Choose Which

Choose Hono when:

  • Building a new API that will run on edge (Cloudflare Workers, Deno Deploy, Vercel Edge)
  • Performance is critical (high-traffic APIs, serverless with cold start concerns)
  • You want excellent TypeScript support out of the box
  • Targeting Bun as your runtime

Stick with Express.js when:

  • You have an existing Express codebase (migration rarely worth it)
  • You depend on Express-specific middleware with no Hono equivalent
  • Your team is deeply familiar with Express patterns and the project is internal/low-traffic
  • You need mature session/cookie management libraries that aren’t ported yet

The migration from Express to Hono is straightforward for simple apps, but the ecosystem difference matters for complex applications. Check the Hono middleware directory before migrating.


Complete Example: REST API

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

const app = new Hono()

// Global middleware
app.use('*', logger())
app.use('/api/*', cors())

// Public routes
app.get('/health', (c) => c.json({ status: 'ok', timestamp: Date.now() }))

// Protected API
const api = app.basePath('/api')

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

const PostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
  published: z.boolean().default(false),
})

const posts: Array<{ id: string } & z.infer<typeof PostSchema>> = []

api.get('/posts', (c) => {
  const { published } = c.req.query()
  const filtered = published !== undefined
    ? posts.filter(p => p.published === (published === 'true'))
    : posts
  return c.json(filtered)
})

api.post('/posts', zValidator('json', PostSchema), async (c) => {
  const data = c.req.valid('json')
  const post = { id: crypto.randomUUID(), ...data }
  posts.push(post)
  return c.json(post, 201)
})

api.get('/posts/:id', (c) => {
  const post = posts.find(p => p.id === c.req.param('id'))
  if (!post) return c.json({ error: 'Not found' }, 404)
  return c.json(post)
})

api.delete('/posts/:id', (c) => {
  const idx = posts.findIndex(p => p.id === c.req.param('id'))
  if (idx === -1) return c.json({ error: 'Not found' }, 404)
  posts.splice(idx, 1)
  return c.body(null, 204)
})

export default app

Summary

Hono.js in 2026 is the clear choice for new edge-deployed TypeScript APIs. Its performance advantage over Express is not marginal β€” it’s an order of magnitude. The framework is stable, the TypeScript support is excellent, and the multi-runtime story (Workers, Bun, Node, Deno) is genuinely useful.

If you’re building something new and performance or edge deployment matters, use Hono. The learning curve for Express developers is roughly one afternoon.


Want more developer tools and guides? Check out DevPlaybook β€” 500+ curated tools and articles for developers.

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.