Last updated on 24th February 2026

Hono: The Complete Guide to the Ultrafast Web Framework

Hono is an ultrafast, lightweight web framework designed to run on any JavaScript runtime — Node.js, Bun, Deno, Cloudflare Workers, AWS Lambda Edge, and more. Created by Yusuke Wada, its name means "flame" in Japanese, which reflects both its speed and its ambition. Unlike traditional frameworks that were built for a single runtime, Hono was architected from the ground up to be runtime-agnostic, making it an ideal choice for developers who want to write once and deploy anywhere. With a bundle size of roughly 14KB and routing performance that consistently benchmarks faster than Express, Koa, and Fastify, Hono is quickly becoming the framework of choice for teams building modern APIs and server-side applications.

Why Hono Matters for Web Developers

The JavaScript ecosystem has long been dominated by Express.js, a framework that has served millions of developers well but was designed in 2010 — before TypeScript, before edge computing, and before the explosion of JavaScript runtimes. Hono fills the gap between Express's familiar API and the demands of modern, type-safe, multi-runtime development. You get the intuitive route-handler pattern you already know, combined with first-class TypeScript support, built-in middleware, and a runtime adapter system that means your code is not locked to a single deployment target.

Performance is a first-class concern in Hono's design. The framework uses a highly optimized trie-based router called RegExpRouter that avoids the performance pitfalls of linear route scanning. In benchmarks comparing raw requests-per-second on Cloudflare Workers, Hono consistently outperforms every alternative framework. On Node.js via the @hono/node-server adapter, it handles significantly more concurrent connections than Express under the same hardware conditions.

The developer experience is where Hono truly differentiates itself. Its validator middleware integrates directly with Zod, giving you automatic request parsing, schema validation, and TypeScript type inference from a single source of truth. Its RPC client — a built-in feature, not a third-party add-on — lets you call your API routes from your frontend with full end-to-end type safety, eliminating an entire class of integration bugs without introducing the complexity of tRPC. JSX support means you can render server-side HTML using familiar component syntax without pulling in a full SSR framework.

Hono's runtime portability is its most strategically valuable feature. Teams frequently start a project on a traditional VPS, then want to move a subset of endpoints to Cloudflare Workers for global edge performance, then add a Lambda function for a webhook handler. With Express, each of these moves requires rewriting routing logic. With Hono, you swap the adapter import and everything else stays identical. This guide focuses on deploying Hono with the Node.js adapter to a VPS using DeployHQ.

Step 1: System Requirements

  • Node.js 18 or later — Hono requires a runtime that supports the Web Fetch API natively
  • npm 9+, pnpm 8+, or Bun — any modern package manager works
  • TypeScript 5.0 or later — Hono's type system is designed around TypeScript generics

Check your installed versions:

node --version
npm --version
tsc --version

On your production server, you need Node.js 18+, a process manager (PM2), and SSH access for DeployHQ.

Step 2: Install Hono

npm create hono@latest my-hono-app

Choose nodejs as the target runtime, then:

cd my-hono-app
npm install

Manual Setup

mkdir my-hono-app && cd my-hono-app
npm init -y
npm install hono @hono/node-server
npm install --save-dev typescript tsx @types/node

Create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "dist",
    "rootDir": "src",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Create your entry point at src/index.ts:

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

const app = new Hono()

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

serve({
  fetch: app.fetch,
  port: 3000
}, (info) => {
  console.log(`Server running on http://localhost:${info.port}`)
})

Step 3: Project Structure

my-hono-app/
├── src/
│   ├── index.ts          # Entry point — mounts adapter, starts server
│   ├── app.ts            # Hono app definition (exported for testing)
│   ├── routes/
│   │   ├── index.ts      # Route aggregator
│   │   ├── posts.ts      # Posts resource routes
│   │   └── health.ts     # Health check endpoint
│   ├── middleware/
│   │   ├── auth.ts       # Authentication middleware
│   │   └── error.ts      # Global error handler
│   ├── validators/
│   │   └── posts.ts      # Zod schemas for post endpoints
│   └── types/
│       └── index.ts      # Shared TypeScript types
├── dist/                 # Compiled output (git-ignored)
├── tsconfig.json
├── package.json
└── .env

Separating the App from the Server

// src/app.ts
import { Hono } from 'hono'
import { routes } from './routes'
import { errorHandler } from './middleware/error'

const app = new Hono()
app.onError(errorHandler)
app.route('/', routes)

export { app }
// src/index.ts
import { serve } from '@hono/node-server'
import { app } from './app'

const port = parseInt(process.env.PORT || '3000', 10)

serve({ fetch: app.fetch, port }, (info) => {
  console.log(`Server listening on port ${info.port}`)
})

Step 4: Configure Hono for Your Workflow

Middleware Stack

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { compress } from 'hono/compress'
import { secureHeaders } from 'hono/secure-headers'

const app = new Hono()

app.use('*', logger())
app.use('*', secureHeaders())
app.use('*', compress())

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

Step 5: Core Features

Routing

app.get('/posts', (c) => c.json({ posts: [] }))
app.post('/posts', (c) => c.json({ created: true }, 201))
app.put('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
app.delete('/posts/:id', (c) => c.json({ deleted: true }))

Validators with Zod

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

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

app.post(
  '/api/posts',
  zValidator('json', createPostSchema),
  async (c) => {
    const body = c.req.valid('json')
    const post = await db.posts.create({ data: body })
    return c.json(post, 201)
  }
)

JSX for Server-Side Rendering

import type { FC } from 'hono/jsx'

const Layout: FC<{ title: string }> = ({ title, children }) => {
  return (
    <html lang="en">
      <head><title>{title}</title></head>
      <body>{children}</body>
    </html>
  )
}

app.get('/', (c) => {
  return c.html(
    <Layout title="Home">
      <h1>Welcome</h1>
    </Layout>
  )
})

Building APIs with Hono? DeployHQ connects your Git repo to any server and deploys automatically when you push — SFTP, SSH, or cloud. Try it free.


Step 6: Advanced Features

RPC Client for End-to-End Type Safety

// Server: export the app type
const app = new Hono()
  .get('/api/posts', (c) => c.json({ posts: [] }))
  .post('/api/posts', zValidator('json', createPostSchema), async (c) => {
    const body = c.req.valid('json')
    return c.json({ id: 1, ...body }, 201)
  })

export type AppType = typeof app
// Client: fully typed API calls
import { hc } from 'hono/client'
import type { AppType } from '../server/src/app'

const client = hc<AppType>('http://localhost:3000')
const response = await client.api.posts.$get()
const data = await response.json()

Streaming and SSE

import { streamSSE } from 'hono/streaming'

app.get('/events', (c) => {
  return streamSSE(c, async (stream) => {
    let id = 0
    while (true) {
      await stream.writeSSE({
        data: JSON.stringify({ count: id++ }),
        event: 'update',
        id: String(id)
      })
      await stream.sleep(1000)
    }
  })
})

Testing Helpers

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

describe('POST /api/posts', () => {
  it('creates a post and returns 201', async () => {
    const response = await app.request('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: 'Test Post',
        content: 'Test content',
        published: false
      })
    })
    expect(response.status).toBe(201)
  })
})

Step 7: Best Practices

Typed Context Variables

export type Variables = {
  user: { id: string; email: string; role: string }
  requestId: string
}

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

Centralised Error Handling

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

export const errorHandler: ErrorHandler = (err, c) => {
  if (err instanceof HTTPException) return err.getResponse()
  console.error('Unhandled error:', err)
  return c.json({ error: 'Internal server error' }, 500)
}

Middleware Composition

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

export const requireAuth = createMiddleware(async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
  if (!token) throw new HTTPException(401, { message: 'Authentication required' })
  const user = await verifyJwt(token)
  c.set('user', user)
  await next()
})

export const requireRole = (role: string) =>
  createMiddleware(async (c, next) => {
    const user = c.get('user')
    if (user.role !== role) throw new HTTPException(403, { message: 'Forbidden' })
    await next()
  })

Step 8: Deploy with DeployHQ

Prepare the Server

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 18
npm install -g pm2
mkdir -p /var/www/my-hono-app

Configure the Build

{
  "scripts": {
    "build": "tsc --project tsconfig.json",
    "start": "node dist/index.js"
  }
}

Set Up DeployHQ

  1. Create a project at DeployHQ and connect your Git repository
  2. Add your server with SSH access
  3. Set the deployment branch and path (/var/www/my-hono-app)

Build commands:

npm ci
npm run build

Post-deployment commands:

cd /var/www/my-hono-app
npm ci --omit=dev
pm2 reload ecosystem.config.cjs --update-env || pm2 start ecosystem.config.cjs
pm2 save

Environment Variables

Use DeployHQ's Configuration Files feature to manage .env on the server, keeping secrets out of your repository.

Reverse Proxy with Nginx

server {
    server_name api.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
}

Step 9: Troubleshooting

Application fails to start after deployment

Check PM2 logs: pm2 logs my-hono-app --lines 50. Common causes: missing environment variables or failed npm ci.

ERR_REQUIRE_ESM or module resolution errors

Ensure package.json includes "type": "module" or set tsconfig.json module target to "CommonJS".

Zod validation errors returning 500 instead of 400

Verify the validator middleware is applied before the route handler and that your error handler checks for ZodError.

WebSocket connections dropping immediately

Confirm your Nginx config includes proxy_set_header Upgrade $http_upgrade and proxy_set_header Connection 'upgrade'.

PM2 not persisting across server reboots

Run pm2 startup and follow the printed instructions, then pm2 save.

Conclusion

Hono offers a compelling combination that few frameworks match: the familiar Express-like routing API, TypeScript-first design with powerful type inference, genuinely fast performance, runtime portability across every major JavaScript environment, and built-in features — validators, RPC client, JSX, streaming, WebSockets — that would require multiple third-party packages in any other framework.

Deploying Hono with the Node.js adapter to a VPS via DeployHQ gives you a reliable, repeatable pipeline: push to your Git branch, DeployHQ compiles and transfers the build, PM2 performs a zero-downtime reload, and your updated application is serving traffic within seconds.


If you have questions about deploying your Hono projects, reach out to us at support@deployhq.com or find us on Twitter/X.