Skip to main content
← All Articles · · 14 min read

Hono.js Full Stack Guide 2026: Ultra-Fast Web Framework for Every Runtime

Master Hono.js in 2026 — middleware patterns, RPC client, JSX SSR, and deployment to Cloudflare Workers, Deno Deploy, and Bun. Complete full-stack guide with code examples.

honotypescriptcloudflare-workersbundenobackendfull-stackedge

Hono.js Full Stack Guide 2026: Ultra-Fast Web Framework for Every Runtime

The web framework landscape has fragmented. You’re no longer building only for Node.js — your code might run on Cloudflare Workers, Deno Deploy, Bun, or AWS Lambda Edge, often across the same project. Hono.js was built for exactly this world.

Hono (炎, Japanese for “flame”) is a small, fast, edge-compatible web framework with zero dependencies, a TypeScript-first API, and a runtime-agnostic design that lets you deploy the same codebase anywhere JavaScript runs. In 2026, it’s the go-to choice for teams building APIs, edge functions, and full-stack apps that need to move fast without runtime lock-in.

This guide covers everything: routing, middleware, the RPC client, JSX SSR, and production deployment patterns across the major runtimes.


Why Hono in 2026?

The numbers tell the story. In the TechEmpower benchmarks, Hono consistently outperforms Express by 5–10x on JSON serialization workloads. Its bundle size sits around 14KB with zero dependencies. But raw speed is only part of the story.

FrameworkBundle SizeRuntimesTypeScriptRPC Client
Hono~14KBAll majorNativeYes
Express~200KB+Node onlyVia typesNo
Fastify~300KBNode/BunVia pluginNo
Elysia~50KBBunNativeYes

Hono’s real advantage is uniformity. You write one application and deploy it to whichever runtime fits your infrastructure. No adapter rewrites, no platform-specific abstractions to fight.


Installation and Quick Start

# Node.js
npm create hono@latest my-app
cd my-app && npm install

# Bun
bun create hono my-app
cd my-app && bun install

# Deno
deno run npm:create-hono@latest my-app

The CLI scaffolds runtime-specific entry points while keeping your application code identical. Here’s the simplest possible Hono app:

import { Hono } from 'hono'

const app = new Hono()

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

export default app

The c context object is the heart of Hono. It provides access to the request, response helpers, environment variables, and any values you attach during middleware execution.


Routing

Hono’s router is its fastest component — built on a RegExp-based router (RegExpRouter) for simple cases and a more sophisticated linear router (SmartRouter) for complex applications. You get all standard HTTP methods plus convenient patterns:

import { Hono } from 'hono'

const app = new Hono()

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

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

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

// Query parameters
app.get('/search', (c) => {
  const q = c.req.query('q') ?? ''
  const page = Number(c.req.query('page') ?? 1)
  return c.json({ q, page })
})

// Multiple methods
app.on(['GET', 'POST'], '/items', (c) => {
  return c.json({ method: c.req.method })
})

export default app

Route Groups and Nesting

For larger applications, group related routes using app.route():

import { Hono } from 'hono'

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

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

v1.get('/posts', (c) => c.json({ posts: [] }))

api.route('/v1', v1)
app.route('/', api)

export default app

This produces clean URLs: GET /api/v1/users, POST /api/v1/users, GET /api/v1/posts.


Middleware

Hono’s middleware system is composable and explicit. Middleware runs in the order it’s registered, and you can scope it to specific routes or apply it globally.

Built-in Middleware

Hono ships a comprehensive set of middleware out of the box:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { secureHeaders } from 'hono/secure-headers'
import { rateLimiter } from 'hono/rate-limiter'

const app = new Hono()

// Global middleware
app.use('*', logger())
app.use('*', compress())
app.use('*', etag())
app.use('*', secureHeaders())

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

// JWT protection on specific routes
app.use('/api/protected/*', jwt({
  secret: 'your-secret-key',
}))

// Rate limiting
app.use('/api/*', rateLimiter({
  windowMs: 15 * 60 * 1000,
  limit: 100,
}))

export default app

Custom Middleware

Writing your own middleware follows the same pattern:

import { Hono, MiddlewareHandler } from 'hono'

// Timing middleware
const timing: MiddlewareHandler = async (c, next) => {
  const start = Date.now()
  await next()
  const elapsed = Date.now() - start
  c.res.headers.set('X-Response-Time', `${elapsed}ms`)
}

// Context injection middleware
const requestId: MiddlewareHandler = async (c, next) => {
  const id = crypto.randomUUID()
  c.set('requestId', id)
  c.res.headers.set('X-Request-Id', id)
  await next()
}

// Auth middleware with typed context
type Variables = {
  user: { id: string; role: string }
}

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

  // Verify token and attach user
  const user = await verifyToken(token)
  c.set('user', user)
  await next()
}

const app = new Hono<{ Variables: Variables }>()
app.use('*', timing)
app.use('*', requestId)
app.use('/protected/*', authMiddleware)

app.get('/protected/profile', (c) => {
  const user = c.get('user') // Fully typed
  return c.json({ user })
})

The RPC Client: Type-Safe API Calls

One of Hono’s most powerful features is the RPC client — it generates a fully typed client from your route definitions, eliminating the need for manual API client code or a separate GraphQL/tRPC layer.

Setting Up RPC

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

const createUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
})

const users = new Hono()
  .get('/', (c) => {
    return c.json({ users: [{ id: '1', name: 'Alice' }] })
  })
  .post('/', zValidator('json', createUserSchema), async (c) => {
    const data = c.req.valid('json') // Fully typed
    const user = await createUser(data)
    return c.json({ user }, 201)
  })
  .get('/:id', (c) => {
    const id = c.req.param('id')
    return c.json({ user: { id, name: 'Alice' } })
  })

export default users
export type UsersRoute = typeof users
// server/index.ts
import { Hono } from 'hono'
import users from './routes/users'

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

export default app
export type AppType = typeof app

Using the Client

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

const client = hc<AppType>('http://localhost:3000')

// Fully typed — your IDE knows the shape of every request and response
const res = await client.users.$get()
const data = await res.json()
// data.users is typed as { id: string; name: string }[]

const newUser = await client.users.$post({
  json: { name: 'Bob', email: '[email protected]' },
})
const created = await newUser.json()
// created.user is fully typed

The RPC client works across all runtimes and integrates seamlessly with React, Svelte, or any frontend framework. Use the API Request Builder tool to prototype API calls before wiring up the client.


JSX Server-Side Rendering

Hono supports JSX out of the box for server-side rendering without React. This is useful for building lightweight server-rendered UIs, email templates, or documentation pages.

// tsconfig.json: add "jsxImportSource": "hono/jsx"
import { Hono } from 'hono'
import { html } from 'hono/html'

const app = new Hono()

// Simple HTML template
app.get('/', (c) => {
  return c.html(
    <html>
      <head>
        <title>Hono App</title>
      </head>
      <body>
        <h1>Hello from Hono JSX!</h1>
      </body>
    </html>
  )
})

// With Streaming
import { streamSSE } from 'hono/streaming'

app.get('/stream', (c) => {
  return streamSSE(c, async (stream) => {
    let count = 0
    const interval = setInterval(async () => {
      await stream.writeSSE({
        data: `Message ${count}`,
        event: 'message',
        id: String(count),
      })
      count++
      if (count >= 10) {
        clearInterval(interval)
        stream.close()
      }
    }, 1000)
  })
})

export default app

Deployment Guide

Cloudflare Workers

Cloudflare Workers is the most popular deployment target for Hono. Your code runs at the edge in 300+ locations globally.

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

type Bindings = {
  MY_KV: KVNamespace
  DB: D1Database
  MY_BUCKET: R2Bucket
}

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

app.get('/kv/:key', async (c) => {
  const value = await c.env.MY_KV.get(c.req.param('key'))
  return c.json({ value })
})

app.get('/db/users', async (c) => {
  const stmt = c.env.DB.prepare('SELECT * FROM users LIMIT 10')
  const result = await stmt.all()
  return c.json({ users: result.results })
})

export default app
# wrangler.toml
name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2026-01-01"

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

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-db-id"
npx wrangler deploy

Use the Cloudflare Workers KV Simulator to test KV operations locally before deploying.

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.text('Running on Deno!'))

Deno.serve(app.fetch)

Deploy with deployctl deploy --project=my-app main.ts.

Bun

// 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 index.ts

Bun’s HTTP server is currently the fastest JavaScript runtime for Hono, achieving over 200,000 requests per second on typical hardware.

Node.js with Adapter

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

const app = new Hono()
app.get('/', (c) => c.text('Running on Node!'))

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

Validation with Zod

Input validation is critical for any production API. Hono integrates cleanly with Zod via the @hono/zod-validator package:

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

const app = new Hono()

const querySchema = z.object({
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(20),
})

const bodySchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).max(5).optional(),
})

app.get('/posts', zValidator('query', querySchema), (c) => {
  const { page, limit } = c.req.valid('query')
  return c.json({ page, limit })
})

app.post('/posts', zValidator('json', bodySchema), async (c) => {
  const body = c.req.valid('json') // Typed and validated
  // body.title, body.content, body.tags — all typed correctly
  return c.json({ post: body }, 201)
})

Validation errors are returned automatically with a 400 status. Use the JWT Debugger and CORS Policy Builder tools when debugging auth and CORS issues in your Hono API.


Error Handling

Hono provides a clean error handling API that works across all runtimes:

import { Hono, HTTPException } from 'hono'

const app = new Hono()

// Custom error handler
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse()
  }

  console.error('Unhandled error:', err)
  return c.json({ error: 'Internal Server Error' }, 500)
})

// 404 handler
app.notFound((c) => {
  return c.json({ error: `Not Found: ${c.req.path}` }, 404)
})

// Throw typed HTTP errors anywhere
app.get('/admin', (c) => {
  const role = c.get('user')?.role
  if (role !== 'admin') {
    throw new HTTPException(403, { message: 'Forbidden' })
  }
  return c.json({ admin: true })
})

Testing

Hono apps are trivial to test because app.fetch is a standard fetch handler:

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

describe('API', () => {
  it('GET / returns 200', async () => {
    const res = await app.request('/')
    expect(res.status).toBe(200)
  })

  it('POST /users creates a user', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Alice', email: '[email protected]' }),
    })
    expect(res.status).toBe(201)
    const data = await res.json()
    expect(data.user.name).toBe('Alice')
  })
})

No server to start, no port conflicts — app.request processes requests in memory. This makes CI fast and test setup minimal.


When to Choose Hono

Hono is the right choice when:

  • You’re targeting Cloudflare Workers, Deno Deploy, or Bun
  • You need a type-safe API with zero dependency overhead
  • You want the RPC client to replace a REST client or avoid a full tRPC setup
  • You’re building edge functions that need to stay small and fast
  • You’re unsure which runtime you’ll ultimately deploy to

Consider alternatives when:

  • You need the full Node.js ecosystem and heavy ORM integrations (Fastify or NestJS)
  • You’re all-in on Bun and want maximum Bun-specific DX (Elysia)
  • You need a full-stack React framework (Next.js, Remix)

Summary

Hono.js in 2026 is a mature, production-ready framework for teams that need speed, type safety, and runtime flexibility. Its zero-dependency design, TypeScript-first API, built-in middleware suite, and RPC client cover the full stack without the bloat of more opinionated frameworks.

Start with npm create hono@latest, pick your runtime at deploy time, and use the RPC client to eliminate manual API client maintenance. Whether you deploy to Cloudflare, Deno, Bun, or Node, the same application code runs everywhere.

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.