Learn Hono: Tutorial, Install & Best Practices for Fast APIs
Hono is the ultrafast, Web Standards-based 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.
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. 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. 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, Valibot, TypeBox, and ArkType. The RPC client 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, 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 (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 for teams who want to stay on Hono.
If you're choosing between Hono and the alternatives in 2026: pick Express if you need maximum ecosystem familiarity, Fastify if you need pure Node.js throughput with built-in JSON Schema, 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.
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, stableWebStreams, stableWebCrypto). 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
consttype 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:
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:
npm create hono@latest my-hono-app
You'll be prompted for a target template. Pick nodejs for this guide, then:
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-typesbun— usesBun.servedirectly; no adapter neededdeno— usesDeno.serveaws-lambda— uses@hono/node-server/aws-lambdafor API Gateway / Function URLsvercel— Next.js-styleapp/api/[...route]/route.tsnetlify— Edge Functions or regular Functionsx-basic— bare HonoX template
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
Add 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"]
}
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:
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:
{
"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
// 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
// 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 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:
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 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
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 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:
npm install zod @hono/zod-validator
// 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()
})
// 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— Valibot is around 1.5KB and tree-shakes per validator. Best for Workers.@hono/typebox-validator— TypeBox gives you JSON Schema you can hand to OpenAPI directly.@hono/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:
// 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>
)
// 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.
// 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 }
// 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 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:
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 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:
// 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 for testClient(), which gives you the same RPC-style API in tests as on the frontend.
Step 10: Best Practices
Typed Variables and Bindings
// 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
}
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
// 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
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:
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, deploy from GitLab — and ships 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), DigitalOcean, Hetzner, or AWS EC2 box.
Prepare the server
# 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
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node dist/index.js"
}
}
ecosystem.config.cjs for PM2
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
- Create a project at DeployHQ and connect your Git repository — GitHub, GitLab, Bitbucket, or self-hosted Git are all supported.
- Add your server with SSH credentials (key recommended over password).
- Set the deployment branch (
main) and target path (/var/www/my-hono-app). - Configure file exclusions —
.env,node_modules/,logs/,*.logshould never deploy.
Build commands (run in the DeployHQ build environment, before transfer):
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):
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.
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:
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 and pair it with DeployHQ's Cloudflare Workers guide.
// src/worker.ts
import { app } from './app'
export default app
wrangler.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 for full setup:
// 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 covers the deployment side; the entry is one line:
// src/main.ts
import { app } from './app.ts'
Deno.serve({ port: 3000 }, app.fetch)
AWS Lambda
// 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 covers each.
Vercel
// 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. The DeployHQ blog post on serverless deployments covers the trade-offs.
Step 13: HonoX — When You Want a Full-Stack Meta-Framework
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].tsxbecomes/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) 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, ship with zero-downtime cluster reloads on every push to main, and keep 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 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 or find us on Twitter/X at @deployhq.