hono review
Hono 4 is an HTTP framework built around Web `Request`, `Response`, `Headers`, and `fetch` instead of Node's native request and response classes. One route tree can target Cloudflare Workers, Bun, Deno, Lambda, Node, and other adapters, while TypeScript route definitions can feed Hono's typed client. Version 4.13.5 fixes query parsing after URL fragments, an SSG path traversal gap, and memory exhaustion through deeply nested dot-notation form fields. Core Hono does not choose your database, job runner, or application structure, and Node needs a separate server adapter.
Hono 4.13.3 installed as 1 package in 0.8 seconds and its full import bundled to 7.6 KB gzipped in our sandbox, with 0 audit findings on 2026-08-22. Choose current 4.13.5 for typed Web API handlers across runtimes, but keep Express or Fastify when Node-specific middleware is the harder dependency to replace.
We installed it
| Install | ✓ · 0.8s | 1 package on disk · 4 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 7.6 KB | gzipped (18.3 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does hono install cleanly?
Yes. In a fresh container with an empty cache, npm install hono finished in 0.8s, leaving 1 package and 4 MB on disk. npm audit reported no known vulnerabilities.
How much does hono add to a browser bundle?
7.6 KB gzipped (18.3 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does hono work with both ESM and CommonJS?
Yes. Both import 'hono' and require('hono') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does hono include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
hono or fastify: which should you use?
fastify: Use it for a Node-first API that benefits from compiled schemas and a mature plugin contract. Hono 4.13.3 installed as 1 package in 0.8 seconds and its full import bundled to 7.6 KB gzipped in our sandbox, with 0 audit findings on 2026-08-22.
When should you not use hono?
The application already depends on Express middleware that mutates req or res. Those objects and conventions do not map directly to Hono's Web API context.
Discussed on
Use it if
- HTTP handlers may move between a Worker, Node, Bun, Deno, or a serverless adapter and should keep Web API types.
- A TypeScript backend should expose route input and output types to a generated client without a second handwritten interface.
- Cold-started deployments benefit from a core package with no direct or peer dependencies.
- The team wants routing and middleware without adopting a framework-wide ORM, dependency container, or module system.
- The application already depends on Express middleware that mutates `req` or `res`. Those objects and conventions do not map directly to Hono's Web API context.
- You want built-in dependency injection, database conventions, background jobs, and code generators. Hono leaves those decisions to the application and other packages.
- Node is the only target and a mature Fastify or Express plugin already solves a niche protocol or vendor integration. Portability may not repay the integration work.
- The team expects body parsing to validate input automatically. `c.req.json()` only parses; schema checking needs validator middleware and another schema package.
- You cannot upgrade past 4.13.3 while using cache middleware behind a proxy, `toSSG()`, or `parseBody({ dot: true })`. Release 4.13.5 fixes security issues in those paths.
Setup reality
We installed hono 4.13.3 in 0.8 seconds in a fresh Node 22 container. That run left 1 package and 4 MB on disk. The package had 0 direct dependencies, 0 peers, 3,664 KB unpacked, bundled TypeScript declarations, and an MIT license. npm audit reported 0 known vulnerabilities on 2026-08-22. ESM import and CommonJS require() both worked. Our full-package browser build measured 18.3 KB minified and 7.6 KB gzipped.
The registry now serves 4.13.5, so those install figures belong to 4.13.3 rather than today's tarball. Upgrade matters: 4.13.5 stops query parsing at URL fragments, closes a remaining toSSG() output-directory escape, and bounds dot-notation nesting in parseBody(). The package declares Node 16.9.0 or newer. Core Hono does not listen on a Node port; install @hono/node-server and pass app.fetch to serve().
No credentials or mandatory config file come with Hono. Runtime bindings do vary. A Worker commonly exposes secrets through c.env, while a Node service may read process.env. Put that boundary in an adapter layer if route code must move. Parsing is separate from validation: add middleware such as @hono/zod-validator before reading c.req.valid('json'). Middleware registration order decides which routes it wraps.
Hosts still control streaming and network metadata. A proxy can buffer streamSSE() output, and a CDN can replace cache headers on a Web Response. Forwarded addresses are trustworthy only when the deployment owns the proxy path. Code written around res.write(), Node streams, or Express response mutation needs translation to Web Streams and returned Response objects. Test mounted wildcards, preflight responses, caching, and streamed output on the actual adapter, since those are where recent patch releases have concentrated fixes.
Patterns
Return text and JSON define-routes
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.text('hello'));
app.get('/health', (c) => c.json({ ok: true }));
export default app;This export works on fetch-style runtimes; Node still needs an adapter that opens a listening socket.
Listen with the Node adapter serve-on-node
import { serve } from '@hono/node-server';
import app from './app.js';
serve({ fetch: app.fetch, port: 3000 });Install `@hono/node-server` separately because the `hono` package does not start a Node server.
Read path and query values read-request-input
app.get('/users/:id', (c) => {
const id = c.req.param('id');
const page = Number(c.req.query('page') ?? '1');
return c.json({ id, page });
});Query values are strings or undefined; `Number()` can produce `NaN` and does not enforce an allowed range.
Validate a JSON body with Zod validate-json
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';
const input = z.object({ name: z.string().min(1) });
app.post('/users', zValidator('json', input), (c) => {
const body = c.req.valid('json');
return c.json(body, 201);
});Install Zod and the validator package; `c.req.json()` by itself parses data without checking its shape.
Measure handler duration write-middleware
app.use(async (c, next) => {
const started = performance.now();
await next();
c.header('Server-Timing', `app;dur=${performance.now() - started}`);
});Register middleware before its target routes; statements after `await next()` run as the response unwinds.
Apply CORS to an API prefix configure-cors
import { cors } from 'hono/cors';
app.use('/api/*', cors({
origin: 'https://app.example.com',
allowMethods: ['GET', 'POST'],
}));Test preflight through the deployed CDN because caches must respect `Vary: Origin` for shared responses.
Return typed HTTP failures handle-errors
import { HTTPException } from 'hono/http-exception';
app.get('/private', () => {
throw new HTTPException(403, { message: 'forbidden' });
});
app.onError((error, c) => {
if (error instanceof HTTPException) return error.getResponse();
console.error(error);
return c.json({ error: 'internal_error' }, 500);
});`HTTPException` carries its status and headers; log unexpected exceptions before returning a generic response.
Infer a client from chained routes generate-typed-client
const routes = app
.get('/hello', (c) => c.json({ message: 'hi' }))
.post('/hello', (c) => c.json({ saved: true }));
export type AppType = typeof routes;
import { hc } from 'hono/client';
const client = hc<AppType>('https://api.example.com');
const response = await client.hello.$get();Keep route calls chained when exporting the app type, or TypeScript may lose information needed by `hc`.
Write a server-sent event stream-sse
import { streamSSE } from 'hono/streaming';
app.get('/events', (c) => streamSSE(c, async (stream) => {
await stream.writeSSE({
event: 'ready',
data: JSON.stringify({ ok: true }),
});
}));A reverse proxy may buffer this response; verify that one event arrives before the connection closes.
Mount a route group mount-subapp
const users = new Hono();
users.get('/', (c) => c.json([]));
users.get('/:id', (c) => c.json({ id: c.req.param('id') }));
app.route('/users', users);Exercise wildcard and suffix cases around mounted prefixes after upgrades because router fixes often land in patch releases.
Type an environment binding type-bindings
type Bindings = { API_TOKEN: string };
const app = new Hono<{ Bindings: Bindings }>();
app.get('/token-check', (c) => {
return c.json({ configured: Boolean(c.env.API_TOKEN) });
});The runtime adapter populates `c.env`; a Node deployment must provide its own mapping from process environment values.
Call a route without a port test-route
import { expect, it } from 'vitest';
import app from './app';
it('returns health', async () => {
const response = await app.request('/health');
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true });
});`app.request()` stays inside the process; adapter, proxy, and socket behavior require a deployed integration test.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fastify | npm | Use it for a Node-first API that benefits from compiled schemas and a mature plugin contract. |
| express | npm | Use it when an existing Node codebase depends on Express request objects and its broad middleware catalog. |
| elysia | npm | Use it for a Bun-centered service that prefers Elysia's integrated schema and plugin model. |
More web backend guides
urllib3 · requests · ws · anyio · undici · httpx · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

