Skip to content

Hono Reference: Routing, Middleware, Validation, Cloudflare Workers & RPC Mode

Hono is a web framework built on the Web Standards API (Request/Response/URL) — the same interface used by Cloudflare Workers, Deno, Bun, and Node.js 18+. One codebase runs everywhere. It’s fast, has a very small bundle (~14kb), and has first-class TypeScript support. The key differentiator from Express: Hono is edge-native. No Node.js stream APIs, no req.body magic — everything is standard Request/Response.

1. Routing & Context

Route handlers, path params, query strings, c.req / c.res, and method chaining
// npm install hono

import { Hono } from "hono";

const app = new Hono();

// Basic routes:
app.get("/",         (c) => c.text("Hello Hono!"));
app.get("/json",     (c) => c.json({ status: "ok" }));
app.post("/users",   async (c) => {
  const body = await c.req.json();
  return c.json({ created: body }, 201);
});

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

// Multiple params:
app.get("/orgs/:org/repos/:repo", (c) => {
  const { org, repo } = c.req.param();
  return c.json({ org, repo });
});

// Query strings:
app.get("/search", (c) => {
  const q    = c.req.query("q");
  const page = c.req.query("page") ?? "1";
  return c.json({ q, page: parseInt(page) });
});

// Headers:
app.get("/protected", (c) => {
  const token = c.req.header("Authorization");
  return c.json({ token });
});

// Method chaining:
const api = new Hono();
api.get("/users", listUsers)
   .post("/users", createUser)
   .get("/users/:id", getUser)
   .put("/users/:id", updateUser)
   .delete("/users/:id", deleteUser);

2. Middleware

Built-in middleware (logger, cors, jwt, bearer-auth), custom middleware, and app.use()
import { Hono } from "hono";
import { logger }     from "hono/logger";
import { cors }       from "hono/cors";
import { jwt }        from "hono/jwt";
import { bearerAuth } from "hono/bearer-auth";
import { prettyJSON } from "hono/pretty-json";

const app = new Hono();

// Global middleware (runs on every request):
app.use("*", logger());     // logs method + path + status + time
app.use("*", cors());       // CORS headers (configure origin/methods/headers)
app.use("*", prettyJSON()); // format JSON with ?pretty=true

// JWT middleware (protects routes below it):
app.use("/api/*", jwt({ secret: Deno.env.get("JWT_SECRET")! }));

// Bearer token auth:
app.use("/admin/*", bearerAuth({ token: process.env.API_TOKEN! }));

// Custom middleware:
const authMiddleware = async (c: Context, next: Next) => {
  const token = c.req.header("Authorization")?.split(" ")[1];
  if (!token) return c.json({ error: "Unauthorized" }, 401);
  try {
    const payload = await verify(token, "secret");
    c.set("user", payload);  // pass to handler via context
    await next();
  } catch {
    return c.json({ error: "Invalid token" }, 401);
  }
};

app.use("/api/*", authMiddleware);

// Access context variables in handler:
app.get("/api/me", (c) => {
  const user = c.get("user");
  return c.json({ user });
});

// CORS configuration:
app.use("*", cors({
  origin: ["https://app.example.com", "http://localhost:3000"],
  allowMethods: ["GET", "POST", "PUT", "DELETE"],
  allowHeaders: ["Content-Type", "Authorization"],
  credentials: true,
}));

3. Validation with Zod Validator

zValidator middleware, validate body/params/query, typed handler input
// npm install @hono/zod-validator zod
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const app = new Hono();

const createUserSchema = z.object({
  name:  z.string().min(2),
  email: z.string().email(),
  role:  z.enum(["admin", "user"]).default("user"),
});

const userParamsSchema = z.object({
  id: z.string().uuid(),
});

// Validate request body:
app.post(
  "/users",
  zValidator("json", createUserSchema),
  async (c) => {
    const data = c.req.valid("json");   // typed as CreateUser — validated + parsed
    // data.name, data.email, data.role are all typed correctly
    const user = await db.users.create({ data });
    return c.json(user, 201);
  }
);

// Validate path params:
app.get(
  "/users/:id",
  zValidator("param", userParamsSchema),
  async (c) => {
    const { id } = c.req.valid("param");  // typed as { id: string }
    const user = await db.users.findUnique({ where: { id } });
    if (!user) return c.json({ error: "Not found" }, 404);
    return c.json(user);
  }
);

// Validate query params:
const searchSchema = z.object({
  q:    z.string().optional(),
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().max(100).default(20),
});
app.get("/search", zValidator("query", searchSchema), (c) => {
  const { q, page, limit } = c.req.valid("query");
  return c.json({ q, page, limit });
});

4. Deployment — Cloudflare Workers, Node.js & Bun

serve() for Node.js and Bun, Cloudflare Workers export default, environment bindings
// Cloudflare Workers (wrangler.toml + same Hono app):
// wrangler.toml: name = "my-api", main = "src/index.ts"
// npm install wrangler -D; npx wrangler dev; npx wrangler deploy

// The app is the same — just export default:
import { Hono } from "hono";
const app = new Hono<{ Bindings: { DB: D1Database; KV: KVNamespace } }>();

app.get("/users", async (c) => {
  const users = await c.env.DB.prepare("SELECT * FROM users").all();
  return c.json(users.results);
});
app.get("/cache/:key", async (c) => {
  const val = await c.env.KV.get(c.req.param("key"));
  return c.json({ val });
});

export default app;  // Cloudflare Workers entry point

// Node.js (npm install @hono/node-server):
import { serve } from "@hono/node-server";
serve({ fetch: app.fetch, port: 3000 }, (info) => {
  console.log(`Listening on http://localhost:${info.port}`);
});

// Bun (built-in):
export default { port: 3000, fetch: app.fetch };  // bun run src/index.ts

// Deno:
Deno.serve(app.fetch);

// Path grouping and sub-applications:
const v1 = new Hono();
v1.get("/users", listUsers);
v1.post("/users", createUser);

const v2 = new Hono();
v2.get("/users", listUsersV2);

const root = new Hono();
root.route("/api/v1", v1);
root.route("/api/v2", v2);

export default root;

5. RPC Mode & Streaming

Hono RPC for end-to-end type safety, streaming responses, and Server-Sent Events
// Hono RPC — type-safe client from server routes (like tRPC but lighter):
import { Hono } from "hono";
import { hc }   from "hono/client";
import { zValidator } from "@hono/zod-validator";

const app = new Hono()
  .get("/users",      (c) => c.json([{ id: "1", name: "Alice" }]))
  .post("/users",
    zValidator("json", createUserSchema),
    async (c) => {
      const data = c.req.valid("json");
      return c.json({ id: "2", ...data }, 201);
    }
  );

export type AppType = typeof app;  // export for client

// Client (in frontend — no runtime code from server):
import type { AppType } from "./api";
const client = hc<AppType>("http://localhost:3000");

// Fully typed:
const users = await client.users.$get();     // GET /users
const data  = await users.json();            // User[]

const res   = await client.users.$post({     // POST /users
  json: { name: "Bob", email: "bob@x.com" }
});

// Streaming responses:
import { stream, streamText, streamSSE } from "hono/streaming";

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

// Server-Sent Events (SSE):
app.get("/events", (c) => streamSSE(c, async (stream) => {
  for (let i = 0; i < 10; i++) {
    await stream.writeSSE({ data: JSON.stringify({ count: i }), event: "update" });
    await stream.sleep(1000);
  }
}));

Track Node.js, Deno, and Bun releases at ReleaseRun. Related: Deno 2 Reference | Bun Reference | Express.js Reference

🔍 Free tool: npm Package Health Checker — check Hono and related edge runtime packages for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com