Edge Framework

Hono

Hono is an ultrafast web framework built on the standard Request and Response objects. Write once, run on Cloudflare Workers, Deno, Bun or Node, with end-to-end types through RPC.

intermediate14 min readUpdated Sep 16, 2026
index.ts
ts
// index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";

type Bindings = { KV: KVNamespace; JWT_SECRET: string };

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

app.use("*", logger());
app.use("/api/*", cors());

app.get("/", (c) => c.text("Hello from the edge"));

app.get("/api/users/:id", async (c) => {
  const id = c.req.param("id");
  const user = await c.env.KV.get(`user:${id}`, "json");
  if (!user) return c.json({ error: "not_found" }, 404);
  return c.json(user);
});

export default app;
Released
2021
Runs on
Workers, Deno, Bun, Node
Style
Minimal, web-standard
Core idea
Request and Response
Language
TypeScript
Version
4.x

Why it matters

Why edge runtimes love Hono

Ultrafast on any runtime

A tiny router with no Node dependencies starts in milliseconds and adds almost nothing to a cold start, which matters on serverless and the edge.

One codebase, many platforms

The same app runs on Cloudflare Workers, Deno Deploy, Bun, Vercel and Node. Portability is the design goal, not an afterthought.

Types end to end with RPC

Export a route type and the client infers every path, parameter and response. No code generation and no hand-written API types.

The big picture

The three ideas behind Hono

Speak the web platform's Request and Response, compose behaviour with middleware, and infer client types straight from the server routes.

Web standards

Portable

Handlers receive a standard Request and return a Response, so the framework adds routing and helpers without inventing new primitives.

Middleware

Compose

Small async functions run before or after the handler and can read and write the context.

RPC

Infer

The route tree is a type, so the client and server share one source of truth for shapes and status codes.

At a glance

What ships in the box

Routing

Express-style paths with params, wildcards and route groups.

Middleware

app.use chains async functions with a shared context object.

Context

c.req reads input and c.json, c.text and c.html write output.

Built-in middleware

cors, logger, bearerAuth, cache, etag and secureHeaders ship with the core.

Validators

First-party zod and valibot validators type the parsed body.

Adapters

Serve the same app on Workers, Node, Deno, Bun and more.

The complete guide

Hono: Everything you need to know

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:

  • cors for cross-origin headers.
  • logger for request logging.
  • bearerAuth and basicAuth for authentication.
  • cache for edge response caching.
  • etag, compress and secureHeaders for HTTP hygiene.
  • csrf for 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 Hono generics so c.env and c.get are safe.
  • Validate every input with zValidator and keep the schema next to the route.
  • Compose middleware with createMiddleware and scope it with a path pattern.
  • Export AppType and use hc on 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 cors after 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 AppType drift 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.

In practice

Routes, middleware, validation and RPC

A Hono service from four angles: a router, a reusable middleware, a validated handler and a typed client.

routes/users.ts
import { Hono } from "hono";

const users = new Hono();

users.get("/", (c) => c.json({ users: [] }));

users.get("/:id", async (c) => {
  const id = c.req.param("id");
  const user = await getUser(id);
  if (!user) return c.json({ error: "not_found" }, 404);
  return c.json(user);
});

export default users;

Trade-offs

Is Hono the right framework for you?

Hono is optimised for standards, portability and cold starts. That shape is ideal on the edge and less relevant on a long-lived Node server.

Strengths

  • Truly portable

    The same code runs unchanged across Workers, Deno, Bun, Node and several platforms, which keeps your options open as hosting evolves.

  • Tiny and fast

    The core is a few kilobytes with no Node built-in dependencies, so it bundles cleanly and starts instantly.

  • Types without codegen

    RPC mode gives the client full knowledge of routes and responses directly from the server's types, catching API drift at compile time.

Trade-offs

  • The ecosystem is younger

    Hono has a growing middleware library, but nothing like Express's decades of packages. You will write more glue yourself.

  • Edge storage is different

    Workers have no local filesystem and limited CPU time. You must design around KV, D1 or R2 rather than a traditional database connection.

  • Not every Node library works

    Code that depends on Node built-ins may not run in the Workers runtime. On Node via the adapter it is fine, but portability is not automatic.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Hono?

Our interactive tutorial walks you through Hono step by step — with quizzes and real code you can run in the browser.