cheat sheet

hono

Package-level reference for Hono — minimal routing, typed handlers, middleware, RPC client, and deployment to Cloudflare Workers, Bun, Deno, Node, and Vercel Edge.

hono

What it is

hono is a tiny web framework (Japanese for "flame") built on Web standards (Request, Response, URL). It was designed for edge runtimes — Cloudflare Workers, Deno Deploy, Vercel Edge, Bun, and Lambda — where startup time and bundle size matter. The same code runs unchanged on Node via a built-in adapter.

Reach for Hono when deploying to edge / serverless and you want Express-shaped ergonomics with a Web-standards API surface. Reach for fastify if you need a Node-first server with the deepest ecosystem of plugins; reach for elysia if you are 100% Bun; reach for raw Request / Response handlers if you want zero abstraction.

Install

bash
npm install hono

Output: added hono to dependencies

bash
pnpm add hono

Output: added 1 package, linked from store

bash
yarn add hono

Output: added hono

bash
bun add hono

Output: installed hono

Many people start from a runtime-specific template:

bash
npm create hono@latest my-api

Output: interactive prompts (Cloudflare Workers, Bun, Node, Deno, Vercel, Netlify); scaffolds a minimal API.

Versioning & Node support

The current line is hono@4.x. Hono moves quickly — minor releases ship every few weeks with additive features and occasional breaking changes flagged in the changelog.

  • Runs on any Web-standards runtime: Cloudflare Workers, Deno, Bun, Vercel Edge, Netlify Edge, AWS Lambda (with an adapter), Node 18+ (via @hono/node-server).
  • Pure ESM. CJS users need a bundler.
  • TypeScript types bundled.
  • Semver observed; consult the changelog for 4.x minor bumps before upgrading.

Package metadata

  • Maintainer: Yusuke Wada + core team
  • Project home: github.com/honojs/hono
  • Docs: hono.dev
  • npm: npmjs.com/package/hono
  • License: MIT
  • First released: 2022
  • Downloads: rapidly growing — millions of weekly downloads as the standard edge framework.

Peer dependencies & extras

hono itself has no peer deps; the runtime-specific extras live in sibling packages.

  • @hono/node-server — adapter to host Hono on Node's HTTP server
  • @hono/zod-validator, @hono/valibot-validator, @hono/typebox-validator — request validation middleware
  • @hono/swagger-ui, @hono/zod-openapi — OpenAPI integration
  • @hono/jwt, @hono/oidc-auth — auth middleware
  • @hono/sentry — error reporting
  • hono-rate-limiter — rate limiting

Alternatives

PackageTrade-off
expressMature Node ecosystem; CJS-first; not designed for edge runtimes.
fastifyHigh-performance Node-first; rich plugin system. Less edge-focused.
elysiaBun-native, end-to-end typed; not portable to Workers/Edge.
koaSlim Node middleware; predates the Web standards revival.
itty-routerMinimal Workers-only router. Tinier than Hono but fewer features.
oakDeno-flavoured koa-like; Deno-first.
Native fetch handlerZero deps; you re-implement routing, parsing, error handling.

Real-world recipes

Minimal API

typescript
import { Hono } from "hono";

const app = new Hono();
app.get("/", (c) => c.text("Hello!"));
app.get("/json", (c) => c.json({ ok: true }));

export default app;

Output: GET / returns Hello!; GET /json returns {"ok":true}; works as-is on Workers, Bun, and Deno.

Typed dynamic route

typescript
app.get("/users/:id", (c) => {
  const id = c.req.param("id");
  return c.json({ id, name: "Alice" });
});

Output: GET /users/42 returns {"id":"42","name":"Alice"}; param is type-inferred.

Logger + CORS middleware

typescript
import { Hono } from "hono";
import { logger } from "hono/logger";
import { cors } from "hono/cors";

const app = new Hono();
app.use(logger());
app.use("/api/*", cors({ origin: "https://example.com" }));

app.get("/api/health", (c) => c.json({ ok: true }));

export default app;

Output: each request is logged; /api/* accepts CORS preflight from the configured origin.

Request validation with Zod

typescript
import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";

const app = new Hono();
const PostSchema = z.object({ title: z.string().min(1) });

app.post("/posts", zValidator("json", PostSchema), (c) => {
  const body = c.req.valid("json");
  return c.json({ created: body });
});

export default app;

Output: invalid JSON returns 400 with the issue list; valid JSON reaches the handler with body typed as { title: string }.

Streaming response

typescript
import { stream } from "hono/streaming";

app.get("/feed", (c) =>
  stream(c, async (s) => {
    for (let i = 0; i < 5; i++) {
      await s.write(`chunk ${i}\n`);
      await s.sleep(500);
    }
  }),
);

Output: five chunks streamed at 500 ms intervals; client sees the body progressively.

Hono RPC client

hc produces a typed client from the server's app type.

typescript
// server.ts
import { Hono } from "hono";
const app = new Hono().get("/users/:id", (c) =>
  c.json({ id: c.req.param("id"), name: "Alice" }),
);
export type AppType = typeof app;
export default app;
typescript
// client.ts
import { hc } from "hono/client";
import type { AppType } from "./server";

const client = hc<AppType>("https://api.example.com");
const res = await client.users[":id"].$get({ param: { id: "42" } });
const data = await res.json();
console.log(data.name);

Output: end-to-end type safety — data.name is string because the server returns that shape.

Cloudflare Workers deployment

typescript
// src/index.ts
import { Hono } from "hono";

type Bindings = { DB: D1Database };

const app = new Hono<{ Bindings: Bindings }>();
app.get("/users", async (c) => {
  const rows = await c.env.DB.prepare("SELECT * FROM users").all();
  return c.json(rows.results);
});

export default app;
toml
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2025-10-01"

[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "..."
bash
wrangler deploy

Output: deploys to https://my-api.<account>.workers.dev; c.env.DB is the D1 binding.

Production deployment

Hono runs unchanged on every major Web-standards runtime. The deployment harness is what changes.

  • Cloudflare Workers / Pages. wrangler deploy after npm run build. Free tier covers many small APIs; D1, KV, R2, Queues are first-class.
  • Bun. bun src/index.ts — Bun calls the default export's fetch directly.
  • Node. Wrap with @hono/node-server:
typescript
import { serve } from "@hono/node-server";
import app from "./app";
serve({ fetch: app.fetch, port: 3000 });

Output: Node listens on port 3000 and dispatches to Hono.

  • Vercel Edge. Export runtime: 'edge' and the Hono app from the API route.
  • AWS Lambda. Use hono/aws-lambda adapter; deploy via SAM or SST.
  • Deno. Deno.serve(app.fetch) runs without an adapter.

Performance tuning

Hono is already among the fastest JavaScript frameworks. The knobs are around routing and middleware ordering.

  • Use RegExpRouter (default in 4.x) for dense route trees. TrieRouter is the fallback when patterns include features RegExpRouter cannot express.
  • Avoid per-request allocations in middleware. Build static objects at module scope.
  • c.text is faster than c.json for plain strings — skips a JSON.stringify call.
  • Mount middleware narrowly. app.use('/api/*', heavy) instead of app.use('*', heavy) so static asset requests skip it.
  • Cache headers. c.header('Cache-Control', 'public, max-age=60') for cacheable responses — at the edge, this is the biggest single performance win.
  • Async work in the request critical path. c.executionCtx.waitUntil(promise) (on Workers) returns the response immediately while finishing background work.

Version migration guide

Hono iterates fast. The headline jumps:

FromToKey changes
hono@3hono@4Adapter packages moved out of core (hono/node-server@hono/node-server). Validator middlewares moved to @hono/*-validator. New app.basePath semantics.

Before (3.x):

typescript
import { Hono } from "hono";
import { zValidator } from "hono/zod-validator";
import { serve } from "hono/node-server";

After (4.x):

typescript
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { serve } from "@hono/node-server";

Output: install the moved packages explicitly; behaviour is unchanged.

Within 4.x minors: check the changelog before bumping — minor releases sometimes adjust middleware option shapes.

Security considerations

  • CORS defaults are restrictive — verify the origin list. cors({ origin: "*" }) with credentials is dangerous; specify exact origins.
  • csrf() middleware is required for cookie-based auth endpoints; without it, a cross-site form can POST to your API.
  • Validate every input. Do not trust c.req.json() until it has been schema-validated; @hono/zod-validator (or valibot) is the standard.
  • Rate limit. hono-rate-limiter or KV/Durable Object-backed limiters for Workers.
  • JWT verification. @hono/jwt verifies signatures but does not check audience/issuer by default — pass audience and issuer explicitly.
  • HTML escape if rendering HTML. Hono's c.html() doesn't sanitise — use hono/jsx or escape manually.
  • Don't leak request bodies in error responses. A validation failure that echoes the body to the client can leak sensitive data.

Testing & CI integration

Unit test with Vitest

typescript
// app.test.ts
import { describe, it, expect } from "vitest";
import app from "./app";

describe("API", () => {
  it("GET / returns hello", async () => {
    const res = await app.request("/");
    expect(res.status).toBe(200);
    expect(await res.text()).toBe("Hello!");
  });

  it("POST /posts validates input", async () => {
    const res = await app.request("/posts", {
      method: "POST",
      body: JSON.stringify({}),
      headers: { "Content-Type": "application/json" },
    });
    expect(res.status).toBe(400);
  });
});

Output: app.request simulates a fetch against the app in memory — no port binding required.

Type-check the RPC client

bash
tsc --noEmit

Output: any mismatch between client call shape and server route surfaces as a TypeScript error.

Ecosystem integrations

PackageRole
@hono/node-serverNode HTTP server adapter
@hono/zod-validator / @hono/valibot-validatorSchema validation middleware
@hono/zod-openapiOpenAPI spec generation
@hono/swagger-uiSwagger UI mounted at a route
@hono/jwtJWT auth middleware
@hono/sentrySentry error reporting
hono-rate-limiterRate limiting
drizzle-ormDB layer; common pairing
@cloudflare/workers-typesTypeScript types for Workers bindings

Troubleshooting common errors

TypeError: c.req.valid is not a functionzValidator middleware not mounted on that route; or you forgot to import { zValidator } from @hono/zod-validator (4.x package move).

Cannot find module 'hono/node-server' — Hono 4.x moved it; install @hono/node-server instead.

Error: No matching route at runtime even though the route exists — middleware mounted before the route shortcircuited. Reorder app.use and app.get calls.

Body has already been consumed — you called c.req.json() twice. Cache the parsed body in middleware and read once.

c.env is undefined on Workers — running locally without wrangler dev or with an incorrect binding name. wrangler.toml and the Bindings type must match.

Type 'Response' is not assignable to type 'TypedResponse<...>' — Hono types the response when you use c.json / c.text. Returning a raw Response loses the RPC type. Use the c.* helpers.

Hot reload breaks state — modules cached in dev; restart wrangler dev or bun --hot when changing top-level singletons.

When NOT to use this

  • You need the Node-specific plugin ecosystem. Fastify and Express have older, larger plugin catalogues.
  • You need Express-compatible middleware. Hono uses its own middleware signature; bring-your-own-Express-middleware doesn't work directly.
  • Long-lived stateful connections. WebSocket / SSE is supported, but Node + Fastify (or Bun + Elysia) may be more ergonomic for long-running socket servers.
  • You need a runtime-agnostic ORM that wraps Workers and Node identically. Drizzle works on both, but the ecosystem here is younger.

See also