Hono is the ultrafast, [Web Standards-based](https://hono.dev/docs/concepts/web-standard) JavaScript framework that runs unchanged on Node.js, Bun, Deno, Cloudflare Workers, AWS Lambda, Lambda@Edge, Vercel, Netlify, Fastly Compute, and any other runtime that speaks the Fetch API. Created by Yusuke Wada and currently shipping at **v4.12.16** (April 30, 2026), Hono is roughly 14KB minified, exports its own typed RPC client, includes JSX out of the box, and is used in production by Cloudflare (D1, Workers KV), Deno, Clerk, Unkey, OpenStatus, and cdnjs. Its name means "flame" in Japanese — both for its speed and its ambition to be *the* framework you reach for when you don't want to pick a runtime first. This guide covers Hono v4.12 specifically: the project structure, the validator and middleware ecosystem, JSX server-side rendering, the type-safe RPC client, the Cloudflare Workers and Node.js deployment paths, HonoX (the alpha meta-framework), and a complete, repeatable production pipeline using [DeployHQ's automated Git deployments](https://www.deployhq.com/features/automatic-deployments).

## Why Hono Matters in 2026

The JavaScript framework conversation has moved on from "Express vs Koa vs Fastify" to "what runtime are you targeting, and can your code follow you when that changes?" Hono answers that question convincingly:

- **Web Standards over runtime APIs.** Hono is built on `Request`, `Response`, `URL`, `Headers`, `ReadableStream` — the same primitives that ship in every modern JavaScript runtime under the [WinterCG interoperability spec](https://wintercg.org/). The same source file deploys to a Node.js VPS, a Cloudflare Worker, and an AWS Lambda function with no business-logic changes.
- **Performance benchmarks where it matters.** On Cloudflare Workers, Hono's `RegExpRouter` (the default) consistently outperforms every alternative — see the [router benchmarks in the Hono repository](https://github.com/honojs/hono#benchmarks). On Node.js via `@hono/node-server`, it handles measurably more concurrent connections than Express on identical hardware.
- **Type safety end to end.** The validator middleware integrates directly with [Zod](https://github.com/honojs/middleware/tree/main/packages/zod-validator), [Valibot](https://github.com/honojs/middleware/tree/main/packages/valibot-validator), [TypeBox](https://github.com/honojs/middleware/tree/main/packages/typebox-validator), and [ArkType](https://github.com/honojs/middleware/tree/main/packages/arktype-validator). The [RPC client](https://hono.dev/docs/guides/rpc) gives you Express-style routes with tRPC-style client typing, with no schema duplication and no extra build step.
- **Built-in features that aren't bolted on.** [JSX server-side rendering](https://hono.dev/docs/guides/jsx), Server-Sent Events, WebSockets, basic auth, bearer auth, JWT (HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512, EdDSA), CORS, CSRF, ETag, cache, compression, secure-headers, body-limit, IP restriction, request timing, and timeouts are all part of the core package — no plugin sprawl.
- **One framework across the stack.** [HonoX](https://github.com/honojs/honox) (currently v0.1.55, alpha) wraps Hono in a Vite-powered meta-framework with file-based routing, fast SSR, islands hydration, and BYOR (bring your own renderer) — the closest thing to Next.js or [SvelteKit](https://www.deployhq.com/guides/sveltekit) for teams who want to stay on Hono.



If you're choosing between Hono and the alternatives in 2026: pick [Express](https://www.deployhq.com/guides/express) if you need maximum ecosystem familiarity, [Fastify](https://www.deployhq.com/guides/fastify) if you need pure Node.js throughput with built-in JSON Schema, [Gin](https://www.deployhq.com/guides/gin) if you're on Go, or Hono if you want one routing layer that follows your code from a Node VPS to the edge. The DeployHQ team has written a deeper comparison in [Node.js Application Servers in 2026: Express, Fastify, Hono, and Modern Alternatives Compared](https://www.deployhq.com/blog/node-application-servers-in-2025-from-express-to-modern-solutions).

## Step 1: System Requirements

Hono itself is runtime-agnostic, but the toolchain depends on your target.

For the Node.js path used in this guide:

- **Node.js 20 LTS or later.** Hono v4 requires native Web Fetch API support, which is on by default from Node 18+ but is materially better in 20+ (stable `fetch`, stable `WebStreams`, stable `WebCrypto`). Node 22 LTS is the safest choice for production in 2026.
- **A modern package manager.** npm 10+, pnpm 9+, Bun 1.2+, or Yarn 4+ all work. The examples below use npm; Bun cuts cold-install time by roughly 5x if you'd rather use it.
- **TypeScript 5.4 or later.** Hono's type system relies on `const` type parameters and template literal inference. TS 5.4+ matters for accurate route-param typing.
- **A production server with SSH access** for the deployment section. Any Linux box with Node 22 and PM2 will do — Ubuntu 24.04 LTS is the reference target.

Verify your toolchain:

```bash
node --version    # v22.x.x
npm --version     # 10.x.x
tsc --version     # 5.4 or later
```

If you're targeting Cloudflare Workers, AWS Lambda, Vercel, Netlify, Bun, or Deno instead, the deployment section at the end of this guide covers each one.

## Step 2: Install Hono

### Using `create-hono` (recommended)

The official scaffolder picks the right adapter, runtime preset, `tsconfig.json`, and `package.json` scripts for every supported target:

```bash
npm create hono@latest my-hono-app
```

You'll be prompted for a target template. Pick **`nodejs`** for this guide, then:

```bash
cd my-hono-app
npm install
npm run dev
```

The other templates worth knowing about:

- **`cloudflare-workers`** — wires up Wrangler, `wrangler.toml`, and `@cloudflare/workers-types`
- **`bun`** — uses `Bun.serve` directly; no adapter needed
- **`deno`** — uses `Deno.serve`
- **`aws-lambda`** — uses `@hono/node-server/aws-lambda` for API Gateway / Function URLs
- **`vercel`** — Next.js-style `app/api/[...route]/route.ts`
- **`netlify`** — Edge Functions or regular Functions
- **`x-basic`** — bare HonoX template

### Manual setup

```bash
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
```

Add a `tsconfig.json`:

```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"]
}
```

The two non-obvious settings: `"moduleResolution": "bundler"` enables the package-export style Hono uses for subpath imports (`hono/jsx`, `hono/cors`, `hono/logger`, etc.), and `"jsxImportSource": "hono/jsx"` lets you write JSX without importing `h` or `Fragment` manually.

A minimal `src/index.ts`:

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

const app = new Hono()

app.get('/', (c) => c.json({ message: 'Hello from Hono!' }))
app.get('/health', (c) => c.json({ ok: true, version: '4.12.16' }))

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

Add scripts to `package.json`:

```json
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc --project tsconfig.json",
    "start": "node dist/index.js"
  }
}
```

`npm run dev` starts the server with hot reload via `tsx`. `npm run build && npm start` is the production path.

## Step 3: Project Structure

Below is the layout the production examples in this guide assume. It separates the Hono `app` (testable, importable) from the runtime adapter (Node, Bun, Workers, Lambda) — the single most useful structural decision when you anticipate moving runtimes later.

```
my-hono-app/
├── src/
│   ├── index.ts           # Adapter entry — Node-specific, swappable
│   ├── app.ts             # Hono app definition (runtime-agnostic, exported)
│   ├── routes/
│   │   ├── index.ts       # Route aggregator
│   │   ├── posts.ts       # Posts resource routes
│   │   └── health.ts      # Health and readiness checks
│   ├── middleware/
│   │   ├── auth.ts        # JWT verification
│   │   ├── error.ts       # Centralised error handler
│   │   └── logging.ts     # Request ID, structured logs
│   ├── validators/
│   │   └── posts.ts       # Zod schemas (shared with the RPC client)
│   ├── services/
│   │   └── posts.ts       # Business logic, runtime-independent
│   ├── lib/
│   │   └── db.ts          # DB client (Postgres, D1, Turso, etc.)
│   └── types/
│       └── index.ts       # Shared types — Variables, Bindings, Env
├── ecosystem.config.cjs   # PM2 config (Node-only deployments)
├── tsconfig.json
├── package.json
└── .env
```

### Splitting the app from the server

```typescript
// src/app.ts — runtime-agnostic
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { cors } from 'hono/cors'
import { routes } from './routes'
import { errorHandler } from './middleware/error'
import type { Variables } from './types'

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

app.use('*', logger())
app.use('*', secureHeaders())
app.use('/api/*', cors({ origin: ['https://yourdomain.com'] }))
app.onError(errorHandler)
app.route('/', routes)

export { app }
export type AppType = typeof app
```

```typescript
// src/index.ts — Node adapter
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}`)
})
```

The Cloudflare Workers entry would be `export default app` in a separate `src/worker.ts`. The Bun entry would be `Bun.serve({ fetch: app.fetch, port: 3000 })`. The same `app.ts` powers all of them.

## Step 4: Middleware — What Ships in the Box

Hono's [built-in middleware](https://hono.dev/docs/guides/middleware) covers the cross-cutting concerns most APIs need without external packages. Each is a subpath import:

| Middleware | Import | What it does |
|---|---|---|
| Logger | `hono/logger` | Per-request method, path, status, duration |
| Secure headers | `hono/secure-headers` | CSP, HSTS, X-Frame-Options, X-Content-Type-Options |
| CORS | `hono/cors` | Origin allow-listing, preflight handling |
| CSRF | `hono/csrf` | Origin-based CSRF protection |
| Compress | `hono/compress` | gzip / deflate / Brotli (Node only — Workers handles this at the edge) |
| Cache | `hono/cache` | Web Cache API integration (Workers, Deno, Bun) |
| ETag | `hono/etag` | Strong / weak ETags with conditional requests |
| Body limit | `hono/body-limit` | Reject oversized requests early |
| Timing | `hono/timing` | `Server-Timing` header for browser perf panels |
| Timeout | `hono/timeout` | Cancel slow requests |
| IP restriction | `hono/ip-restriction` | Allow / deny lists (CIDR ranges supported) |
| Basic auth | `hono/basic-auth` | RFC 7617 basic auth |
| Bearer auth | `hono/bearer-auth` | Static-token auth for service-to-service |
| JWT | `hono/jwt` | JWT verification — HS256/384/512, RS\*, PS\*, ES\*, EdDSA |
| Pretty JSON | `hono/pretty-json` | Add `?pretty` to format JSON output |
| JSX renderer | `hono/jsx-renderer` | SSR layout helper |
| Method override | `hono/method-override` | `_method` form fields → real HTTP verbs |

Hono v4.12.16 (April 30, 2026) shipped two important security fixes: missing JSX tag-name validation that allowed HTML injection in `jsx()` / `createElement()`, and a body-limit middleware bypass. **If you're on any v4.12.x below 16, upgrade now.** Earlier in April, v4.12.12 patched a path-traversal issue in `serveStatic` plus a cookie-handling bug — also worth flagging if you're still pinned to an older minor.

A representative middleware stack for a public API:

```typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { cors } from 'hono/cors'
import { csrf } from 'hono/csrf'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { bodyLimit } from 'hono/body-limit'
import { timeout } from 'hono/timeout'
import { timing } from 'hono/timing'

const app = new Hono()

app.use('*', timing())
app.use('*', logger())
app.use('*', secureHeaders())
app.use('*', etag())

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

app.use('/api/*', csrf({ origin: 'yourdomain.com' }))
app.use('/api/*', bodyLimit({ maxSize: 1024 * 1024 }))   // 1 MB
app.use('/api/*', timeout(15_000))                        // 15s
app.use('/api/*', compress())                             // Node only
```

For anything not in core, the [third-party middleware repo](https://hono.dev/docs/middleware/third-party) covers Sentry, Clerk, Auth.js, OpenAPI / Swagger, GraphQL Yoga, Prometheus metrics, OAuth providers, BullMQ, Sessions, Lucia, the validators above, and more.

## Step 5: Routing, Validation, and Type-Safe Handlers

### Routing primitives

```typescript
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.patch('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
app.delete('/posts/:id', (c) => c.json({ deleted: true }))

// Wildcard, optional, and regex parameters all work
app.get('/files/*', (c) => c.text(c.req.path))
app.get('/posts/:id{[0-9]+}', (c) => c.json({ id: c.req.param('id') }))
```

Hono picks the [`RegExpRouter`](https://hono.dev/docs/api/routing) by default — a precompiled trie that's roughly 4-5x faster than the linear scan in older Express versions. For Workers' cold-start budget there's also `SmartRouter`, `TrieRouter`, and `LinearRouter`; `RegExpRouter` is the default for a reason.

### Validators with Zod

The `@hono/zod-validator` package gives you parsing, validation, and inferred types in one middleware call. Install it:

```bash
npm install zod @hono/zod-validator
```

```typescript
// src/validators/posts.ts
import { z } from 'zod'

export 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()).max(10).optional()
})

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

```typescript
// src/routes/posts.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { createPostSchema, listPostsQuery } from '../validators/posts'

export const posts = new Hono()
  .get('/', zValidator('query', listPostsQuery), async (c) => {
    const { page, limit, tag } = c.req.valid('query')
    const data = await listPosts({ page, limit, tag })
    return c.json(data)
  })
  .post('/', zValidator('json', createPostSchema), async (c) => {
    const body = c.req.valid('json')
    const post = await createPost(body)
    return c.json(post, 201)
  })
```

`c.req.valid('json')` and `c.req.valid('query')` are fully typed — you'll get autocomplete and type errors for any field that doesn't match the schema. This is the same source of truth the RPC client uses below, which means client and server can never disagree on the request shape.

### Other validator backends

If you'd rather not pull in Zod (it's around 13KB minified+gzipped, which matters at the edge), the official validator wrappers cover lighter alternatives:

- **[`@hono/valibot-validator`](https://github.com/honojs/middleware/tree/main/packages/valibot-validator)** — Valibot is around 1.5KB and tree-shakes per validator. Best for Workers.
- **[`@hono/typebox-validator`](https://github.com/honojs/middleware/tree/main/packages/typebox-validator)** — TypeBox gives you JSON Schema you can hand to OpenAPI directly.
- **[`@hono/arktype-validator`](https://github.com/honojs/middleware/tree/main/packages/arktype-validator)** — ArkType uses TypeScript-like syntax for runtime validation.

All four expose the same `c.req.valid('json' | 'query' | 'param' | 'header' | 'cookie' | 'form')` API.

## Step 6: JSX Server-Side Rendering

Hono ships its own JSX runtime — no React, no Preact, no extra bundle — that compiles to a Web Standard `Response` directly. It's purpose-built for SSR and email templates:

```tsx
// src/views/Layout.tsx
import type { FC } from 'hono/jsx'

export const Layout: FC<{ title: string }> = ({ title, children }) => (
  <html lang="en">
    <head>
      <meta charSet="utf-8" />
      <title>{title}</title>
      <link rel="stylesheet" href="/static/app.css" />
    </head>
    <body>{children}</body>
  </html>
)
```

```tsx
// src/routes/index.ts
import { app } from '../app'
import { Layout } from '../views/Layout'

app.get('/', (c) => {
  return c.html(
    <Layout title="Home">
      <h1>Welcome</h1>
      <p>Rendered server-side, with no React in sight.</p>
    </Layout>
  )
})
```

For larger apps, `hono/jsx-renderer` lets you set a layout once and call `c.render(...)` from each route. Hono's JSX also has streaming support via `<Suspense />`-like async components and a small client-side runtime if you need hydration. For full-stack islands and file-based routing, jump to **HonoX** below.

## Step 7: The RPC Client — End-to-End Type Safety

This is the feature that earns Hono its place in modern stacks. The route definitions you already wrote double as a typed API contract that the client picks up automatically — no codegen, no OpenAPI step, no separate schema file.

```typescript
// src/app.ts (server)
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { createPostSchema } from './validators/posts'

const app = new Hono()
  .get('/api/posts', (c) => c.json({ posts: [] as { id: number; title: string }[] }))
  .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
export { app }
```

```typescript
// client/api.ts (frontend)
import { hc } from 'hono/client'
import type { AppType } from '../server/src/app'

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

// GET — return type inferred from the handler's c.json() argument
const res = await client.api.posts.$get()
const { posts } = await res.json()
//        ^? { id: number; title: string }[]

// POST — body type inferred from the Zod schema
const created = await client.api.posts.$post({
  json: {
    title: 'My first post',
    content: 'Hello',
    published: true
  }
})
```

If the server schema changes — a field is renamed, a string becomes a number, a route is removed — the TypeScript compiler fails on the frontend at build time. That's the same guarantee tRPC gives you, but with REST-style routes, no procedure builder, and no protocol of its own. Read [the RPC guide](https://hono.dev/docs/guides/rpc) for the full pattern, including streaming, file uploads, and `$url()` for type-safe URL construction.

## Step 8: Streaming, SSE, and WebSockets

Hono's streaming helpers are Web Standards-native, so they work identically on Node, Bun, Deno, and Workers:

```typescript
import { streamSSE, streamText } from 'hono/streaming'

// Server-Sent Events — perfect for LLM token streaming, progress updates, live counters
app.get('/events', (c) => {
  return streamSSE(c, async (stream) => {
    let id = 0
    while (!stream.aborted) {
      await stream.writeSSE({
        data: JSON.stringify({ count: id++, ts: Date.now() }),
        event: 'tick',
        id: String(id)
      })
      await stream.sleep(1000)
    }
  })
})

// Plain text streaming — chunked transfer, low latency
app.get('/log', (c) => {
  return streamText(c, async (stream) => {
    for (const line of await fetchLogLines()) {
      await stream.writeln(line)
    }
  })
})
```

For WebSockets, the upgrade path is runtime-specific — `@hono/node-ws` for Node, `Bun.serve({ websocket })` for Bun, the `WebSocketPair` API on Workers — but Hono's [WebSocket helpers](https://hono.dev/docs/helpers/websocket) abstract the registration so the handler signature stays the same.

## Step 9: Testing — `app.request()` Without a Server

Hono's testing story is its quietest superpower. Because `app` is just a function `(req: Request) => Promise<Response>`, you can call it directly in any test runner — no Supertest, no fixture server, no port allocation:

```typescript
// tests/posts.test.ts
import { describe, it, expect } from 'vitest'
import { app } from '../src/app'

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

  it('rejects an invalid body with 400', async () => {
    const res = await app.request('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: '' })   // missing content
    })
    expect(res.status).toBe(400)
  })
})
```

`app.request(path, init)` runs the full middleware stack, including the validator, exactly the way it would in production. A typical Hono test suite of 200 cases finishes in under two seconds because there's no I/O at the boundary. See the [testing helper docs](https://hono.dev/docs/guides/testing) for `testClient()`, which gives you the same RPC-style API in tests as on the frontend.

## Step 10: Best Practices

### Typed `Variables` and `Bindings`

```typescript
// src/types/index.ts
export type Variables = {
  user: { id: string; email: string; role: 'admin' | 'editor' | 'viewer' }
  requestId: string
}

// For Cloudflare Workers
export type Bindings = {
  DB: D1Database
  KV: KVNamespace
  POSTS_BUCKET: R2Bucket
  ANTHROPIC_API_KEY: string
}
```

```typescript
const app = new Hono<{
  Variables: Variables
  Bindings: Bindings   // Workers only — omit on Node
}>()
```

`c.get('user')` is now typed. So is `c.env.DB`. So is every middleware that reads or writes those values.

### Centralised error handling

```typescript
// src/middleware/error.ts
import type { ErrorHandler } from 'hono'
import { HTTPException } from 'hono/http-exception'
import { ZodError } from 'zod'

export const errorHandler: ErrorHandler = (err, c) => {
  if (err instanceof HTTPException) return err.getResponse()

  if (err instanceof ZodError) {
    return c.json({ error: 'Validation failed', issues: err.issues }, 400)
  }

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

### Middleware factories

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

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

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

### Chain mounts for typed sub-apps

If you want the RPC client to see the full route tree, *chain* `.route()` calls and let TypeScript flow the types:

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

export type AppType = typeof app
```

Mounting via `app.route(...)` on its own line breaks the type chain — every chained route is a returned `Hono` instance, so the assignment back to `app` matters.

---

*Building APIs with Hono? DeployHQ connects your Git repo to any server — VPS, dedicated, [deploy from GitHub](https://www.deployhq.com/deploy-from-github), [deploy from GitLab](https://www.deployhq.com/deploy-from-gitlab) — and ships [zero-downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments) every time you push.*

---

## Step 11: Deploy with DeployHQ (Node.js + VPS)

The Hono → Node → VPS → DeployHQ path is the one most teams want first: predictable cost, no cold starts, full control. The same build pipeline works for any [Akamai Cloud (Linode)](https://www.deployhq.com/guides/linode), DigitalOcean, Hetzner, or AWS EC2 box.

### Prepare the server

```bash
# Install Node 22 LTS via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
nvm alias default 22

# PM2 process manager
npm install -g pm2

# Application directory
sudo mkdir -p /var/www/my-hono-app
sudo chown $USER:$USER /var/www/my-hono-app
```

### `package.json` build scripts

```json
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc --project tsconfig.json",
    "start": "node dist/index.js"
  }
}
```

### `ecosystem.config.cjs` for PM2

```javascript
module.exports = {
  apps: [{
    name: 'my-hono-app',
    script: 'dist/index.js',
    instances: 'max',                  // one worker per CPU core
    exec_mode: 'cluster',              // load-balanced cluster mode
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    max_memory_restart: '500M',
    error_file: 'logs/err.log',
    out_file: 'logs/out.log',
    time: true,
    listen_timeout: 5000,
    kill_timeout: 3000
  }]
}
```

`exec_mode: 'cluster'` is the key line for zero-downtime reloads — `pm2 reload` cycles workers one at a time so requests in flight aren't dropped.

### Set up the DeployHQ project

1. Create a project at DeployHQ and connect your Git repository — GitHub, GitLab, Bitbucket, or self-hosted Git are all supported.
2. Add your server with SSH credentials (key recommended over password).
3. Set the deployment branch (`main`) and target path (`/var/www/my-hono-app`).
4. Configure file exclusions — `.env`, `node_modules/`, `logs/`, `*.log` should never deploy.

**Build commands** (run in the DeployHQ build environment, before transfer):

```bash
npm ci
npm run build
```

DeployHQ caches `node_modules/` between builds, so `npm ci` is fast on subsequent runs.

**Post-deployment commands** (run on your server, after transfer):

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

The `||` fallback handles the very first deploy, where there's no running process to reload yet. Every subsequent deploy is a [zero-downtime cluster reload](https://www.deployhq.com/blog/zero-downtime-deployments-with-deployhq).

### Environment variables

Use DeployHQ's Configuration Files feature to manage `.env` per server. The config file is held outside Git, encrypted at rest, and templated into `/var/www/my-hono-app/current/.env` on every deploy. Rotating a database password becomes a one-click change with no commit history.

### Reverse proxy with Nginx

Hono's Node adapter listens on a local port; Nginx terminates TLS and forwards requests:

```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;
        proxy_read_timeout 60s;
    }

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

The `Upgrade` and `Connection` headers are mandatory if you're running Hono's WebSocket helpers — without them, the upgrade handshake silently fails and the connection drops at the LB.

If a deploy ever produces a bad build, DeployHQ's one-click rollback reverts the symlinked `current` release to the previous build in under a second; PM2 reloads against the older `dist/index.js` and you're back online.

## Step 12: Other Runtimes

The `app.ts` you wrote in Step 3 deploys unchanged to every supported runtime. Only the entry file differs.

### Cloudflare Workers

The deployment target with the lowest latency and the cheapest free tier — and the runtime where Hono's design pays off most. Follow the [Cloudflare Workers guide](https://hono.dev/docs/getting-started/cloudflare-workers) and pair it with [DeployHQ's Cloudflare Workers guide](https://www.deployhq.com/guides/cloudflare-workers).

```typescript
// src/worker.ts
import { app } from './app'

export default app
```

`wrangler.toml`:

```toml
name = "my-hono-app"
main = "src/worker.ts"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]

[vars]
ENV = "production"
```

Deploy with `npx wrangler deploy`. DeployHQ can wrap that in a build pipeline that runs `wrangler deploy --env production` on every push to `main`, so the same Git-push-to-deploy contract holds.

### Bun

Bun's HTTP server speaks the Fetch API natively, so no adapter is needed — see the [DeployHQ Bun guide](https://www.deployhq.com/guides/bun) for full setup:

```typescript
// src/index.ts
import { app } from './app'

export default {
  port: 3000,
  fetch: app.fetch
}
```

`bun run src/index.ts` and you're serving. Production deploys can use Bun's native `bun build --compile` to ship a single-binary executable, which DeployHQ transfers and PM2 supervises.

### Deno

The [DeployHQ Deno guide](https://www.deployhq.com/guides/deno) covers the deployment side; the entry is one line:

```typescript
// src/main.ts
import { app } from './app.ts'

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

### AWS Lambda

```typescript
// src/lambda.ts
import { handle } from 'hono/aws-lambda'
import { app } from './app'

export const handler = handle(app)
```

Wire it up with API Gateway, an Application Load Balancer, or a Function URL. The [Hono AWS Lambda guide](https://hono.dev/docs/getting-started/aws-lambda) covers each.

### Vercel

```typescript
// app/api/[...route]/route.ts
import { handle } from 'hono/vercel'
import { app } from '@/server/app'

export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const DELETE = handle(app)
```

This drops Hono into a Next.js project at `/api/*` without disturbing the rest of the app.

### Netlify

Netlify Edge Functions or regular Functions both work — see the [Hono Netlify guide](https://hono.dev/docs/getting-started/netlify). The DeployHQ blog post on [serverless deployments](https://www.deployhq.com/blog/serverless-deployments-with-deployhq-aws-lambda-vercel-and-netlify) covers the trade-offs.

## Step 13: HonoX — When You Want a Full-Stack Meta-Framework

[HonoX](https://github.com/honojs/honox) is Hono's own answer to Next.js, SvelteKit, and Astro — built on Hono + Vite, currently at **v0.1.55** (March 10, 2026, still alpha). Worth knowing about if you want one framework for both API and frontend:

- **File-based routing** — `app/routes/posts/[id].tsx` becomes `/posts/:id`
- **Fast SSR** powered by Hono's runtime
- **Islands hydration** — JavaScript ships only for interactive components
- **BYOR (bring your own renderer)** — Hono JSX, React, Preact, or Vue
- **Same Hono middleware stack** — everything in this guide carries over

The alpha caveat matters: minor versions can still introduce breaking changes. For production today, Hono + a separate frontend (Astro, SvelteKit, or [Nuxt 3](https://www.deployhq.com/guides/nuxt)) is the safer bet; HonoX is one to track and revisit when it stabilises.

## Step 14: Troubleshooting

**`ERR_REQUIRE_ESM` or module-resolution errors at runtime.** Hono and `@hono/node-server` are ESM-only. Either set `"type": "module"` in `package.json` and keep `"module": "ESNext"` in `tsconfig.json`, or build to `"module": "CommonJS"` and use the CJS-compatible Node entry (`require('hono')` is supported as of v4.5+ via the dual-publish, but ESM is the maintained path).

**Validator errors return 500 instead of 400.** The Zod validator throws a `ZodError`; if your `onError` handler doesn't recognise it, it'll fall through to the generic 500. Check for `ZodError` first, then `HTTPException`, and only then catch-all 500. The error-handler example in Step 10 is the pattern.

**Cloudflare Workers: `Error: hono/node-server is not supported`.** You imported the Node adapter into a Workers entry. The Workers entry must `export default app` — no `serve()`, no `@hono/node-server`. Check that your `wrangler.toml` `main` field points to the Workers entry, not the Node entry.

**WebSockets disconnect immediately behind Nginx.** Missing `Upgrade` / `Connection` headers in the proxy config. The Step 11 Nginx block has the correct values — copy it verbatim.

**PM2 `cluster` mode + WebSockets dropping connections on reload.** Workers don't share state by default. Use a sticky-session header in front of PM2 (Nginx `ip_hash`, or HAProxy `balance source`), or move WebSocket-heavy workloads to a Worker / Durable Object setup where state is naturally shared.

**RPC client returns `unknown` for response types.** The most common cause is mounting routes via `app.route(...)` on a separate line instead of chaining: `const app = new Hono().route(...).route(...)`. Without chaining, TypeScript can't see the full route tree.

**`npm ci` fails on the build server with `EACCES`.** Build cache permissions left over from a previous run. Clear DeployHQ's build cache from the project settings and re-run.

**The Hono package size grew unexpectedly.** You're importing `hono` instead of `hono/tiny`. The `tiny` preset uses `SmartRouter` instead of `RegExpRouter` and trims the bundle to ~6KB minified — at the cost of slightly slower routing on cold start. For Workers it's usually a wash; for Node it's not worth it.

## Conclusion

Hono in 2026 occupies a specific, defensible niche: an Express-style routing API on top of Web Standards, with first-class TypeScript types, an RPC client that beats schema duplication, JSX SSR built in, and a runtime adapter list that covers every JavaScript target a team is realistically going to ship to. It's small enough to live happily on Cloudflare Workers, fast enough to replace Express on a Node VPS, and consistent enough that the same `app.ts` you wrote on day one moves to AWS Lambda, Bun, or Deno without a rewrite.

For developers who pair Hono with a deployment platform, DeployHQ handles the production side of the equation: connect a Git repository, set up [build pipelines](https://www.deployhq.com/features/build-pipelines), ship with zero-downtime cluster reloads on every push to `main`, and keep [one-click rollback](https://www.deployhq.com/features/one-click-rollback) one button away. The pattern is the same whether you're deploying a Node VPS, a Cloudflare Worker, or a Bun binary.

[Sign up for a free DeployHQ account](https://www.deployhq.com/signup) and get a Hono app from `git push` to production in an afternoon.

---

If you have questions about deploying Hono with DeployHQ, reach out at [support@deployhq.com](mailto:support@deployhq.com) or find us on [Twitter/X at @deployhq](https://x.com/deployhq).
