What is Hono?
Hono is a small, fast web framework built on the Web Platform. Instead of inventing its own request and response objects, it uses the standard Request and Response classes that browsers, Cloudflare Workers, Deno and Bun all provide. On top of that foundation it adds routing, middleware and a context object, and nothing else you do not ask for.
The name means “flame” in Japanese, and the project leans into speed: a tiny core, no Node built-in dependencies and a router designed for cold starts. That combination made Hono the default choice for edge APIs, where every millisecond of startup is paid on every request. Because the runtime contract is the web standard, the same application also runs on Node, so you are never locked in.
Routing on web standards
Routing looks deliberately familiar. A method and a path map to a handler, parameters use :name, and wildcards use *.
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.text("Hello"));
app.get("/posts", (c) => c.json([]));
app.get("/posts/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/posts", (c) => c.json({ created: true }, 201));
Routers can be split into sub-apps and mounted, which is how larger services stay organised:
import { Hono } from "hono";
const api = new Hono();
api.get("/users", listUsers);
api.get("/users/:id", getUser);
app.route("/api", api);
Every handler returns a Response. The context helpers — c.json, c.text, c.html, c.redirect, c.body — build the correct response with headers and status, and you can always return a raw new Response(...) when you need full control.
The context object
The handler’s only argument is the context, conventionally named c. It holds the request, the response helpers and a place to store values for the current request.
app.post("/posts", async (c) => {
const id = c.req.param("id"); // path parameter
const page = c.req.query("page"); // query string
const body = await c.req.json(); // parsed body
const token = c.req.header("authorization");
c.set("requestId", crypto.randomUUID()); // per-request store
return c.json({ id, page, body, token });
});
c.env exposes the runtime bindings: environment variables, KV namespaces, D1 databases and R2 buckets on Workers. c.set and c.get share values between middleware and handlers, and c.var gives typed access to them. Everything you need for one request lives in one object, which makes middleware composition straightforward.
Middleware
Middleware is an async function that receives the context and next. Call await next() to continue the chain, or return early to short-circuit.
import { createMiddleware } from "hono/factory";
export const timing = createMiddleware(async (c, next) => {
const start = performance.now();
await next();
c.header("Server-Timing", `app;dur=${performance.now() - start}`);
});
app.use("*", timing);
The createMiddleware helper adds type inference, and app.use accepts a path pattern so middleware only runs where it is needed. Registration order is execution order, exactly as in Express.
Hono ships a useful set of middleware in the core:
corsfor cross-origin headers.loggerfor request logging.bearerAuthandbasicAuthfor authentication.cachefor edge response caching.etag,compressandsecureHeadersfor HTTP hygiene.csrffor cross-site request forgery protection.
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { bearerAuth } from "hono/bearer-auth";
app.use("*", logger());
app.use("/api/*", cors());
app.use("/admin/*", bearerAuth({ token: c.env.ADMIN_TOKEN }));
Because these are plain middleware, they compose with your own and with anything else in the ecosystem.
Validation and typed responses
Hono has first-party validators for Zod and Valibot. They parse a target (json, query, param, form), reject invalid input with a 400, and give the handler a typed value.
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const CreatePost = z.object({
title: z.string().min(1),
body: z.string(),
});
app.post("/posts", zValidator("json", CreatePost), (c) => {
const post = c.req.valid("json");
return c.json({ id: crypto.randomUUID(), ...post }, 201);
});
The parsed value is typed, so c.req.valid("json") is exactly the shape of the schema. You can customise the failure response with a hook, which is how you keep error bodies consistent with the rest of your API.
RPC: end-to-end types without codegen
RPC mode is Hono’s standout feature. If you export the type of your app, the client can infer every route and response directly from it.
// server.ts
const route = app
.get("/api/users", (c) => c.json([{ id: "1", name: "Ada" }]))
.post(
"/api/users",
zValidator("json", CreateUser),
(c) => c.json({ id: crypto.randomUUID(), ...c.req.valid("json") }, 201),
);
export type AppType = typeof route;
// client.ts
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("https://api.example.com");
const res = await client.api.users.$post({
json: { name: "Ada", email: "[email protected]" },
});
if (res.ok) {
const user = await res.json(); // typed from the server handler
}
There is no schema file to keep in sync and no generated client. Change a route on the server and the client fails to compile until it is updated, which turns API drift into a build error. This works best in a monorepo or a shared package where both sides can import the type.
Rendering HTML and JSX
Hono can serve HTML directly, either as strings or with its own JSX runtime. The JSX is designed for server rendering: no virtual DOM, no hydration, just string output.
import { Hono } from "hono";
import { html } from "hono/html";
const app = new Hono();
app.get("/", (c) => {
return c.html(
html`<!doctype html>
<html>
<body><h1>Hello from ${c.req.query("name") ?? "the edge"}</h1></body>
</html>`,
);
});
For a fuller templating experience, hono/jsx provides components and layouts, and helpers such as html handle escaping. It is a good fit for server-rendered pages and for HTML fragments returned by an edge API.
One codebase, many runtimes
Portability is the point. The application code never imports a runtime-specific module; only the entry point changes.
// Node
import { serve } from "@hono/node-server";
import app from "./app";
serve({ fetch: app.fetch, port: 3000 });
// Cloudflare Workers
export default app;
// Bun
export default { port: 3000, fetch: app.fetch };
The same app object serves all three. That means a service prototyped on Node can move to Workers for cost or latency reasons without a rewrite, and a Workers app can run in a local test suite through the Node adapter.
Deploying to the edge
On Cloudflare Workers, deployment is a Wrangler command and a small config file.
pnpm add -D wrangler
npx wrangler deploy
# wrangler.toml
name = "api"
main = "src/index.ts"
compatibility_date = "2026-09-01"
[[kv_namespaces]]
binding = "KV"
id = "xxxxxxxxxxxxxxxx"
Bindings defined here appear on c.env, fully typed if you pass them as the Bindings generic to new Hono<{ Bindings: Bindings }>(). Vercel, Deno Deploy and Netlify each have a documented adapter, and the same app usually deploys with a one-line entry change. Remember the runtime constraints: cap CPU-heavy work, avoid long-lived database connections, and use edge-native storage.
Best practices
- Type bindings and variables through the
Honogenerics soc.envandc.getare safe. - Validate every input with
zValidatorand keep the schema next to the route. - Compose middleware with
createMiddlewareand scope it with a path pattern. - Export
AppTypeand usehcon the client instead of hand-written types. - Keep handlers thin; move reusable logic into plain functions or services.
- Return the right status codes with
c.json(body, status)rather than always 200. - Design for the runtime: no filesystem, bounded CPU, edge-native storage.
- Test with
app.request()so tests need no server or network.
test("GET /posts", async () => {
const res = await app.request("/posts");
expect(res.status).toBe(200);
});
Common mistakes
- Assuming Node libraries work on Workers; built-ins and native addons often do not.
- Holding open database connections in an edge runtime instead of using HTTP or bindings.
- Forgetting to return the response, so the handler resolves to
undefined. - Registering
corsafter the routes it should apply to. - Reading
c.req.json()more than once, which fails because the body stream is consumed. - Skipping validation and trusting
c.req.query()strings. - Letting
AppTypedrift by hand-writing client types instead of importing it. - Doing CPU-heavy work in a request and hitting the runtime’s time limit.
Where to go next
Hono brings the routing and middleware model you already know from Express to runtimes that did not exist when Express was written. If you need a long-lived Node server with schema-first validation, compare it with Fastify. To understand the runtimes Hono targets, read the Node.js basics guide, and when you are ready to ship, the backend roadmap covers cloud deployment. Then build one small API, deploy it to Workers, and call it with a typed RPC client.