March 26, 20268 min read

Hono — The Ultrafast Web Framework for the Edge

Practical guide to Hono, the lightweight web framework that runs everywhere — Cloudflare Workers, Bun, Deno, Node.js, and Vercel Edge.

hono framework edge cloudflare api
Ad 336x280

Express.js was built in 2010 for a world where servers were long-running processes on dedicated machines. Hono was built for 2024, where your code runs on edge networks across 300 data centers, cold starts matter, and you might deploy to Cloudflare Workers, AWS Lambda, or Deno Deploy depending on the day.

Hono is a small, fast web framework for the edge. It runs on every major JavaScript runtime — Cloudflare Workers, Bun, Deno, Node.js, Fastly, Vercel Edge, AWS Lambda, and more — with the same API. It weighs about 14KB. It's faster than Express by an order of magnitude. And unlike Express, it was designed with TypeScript from the start.

Getting Started

# Create a new project
bun create hono my-api

# Or add to existing project
bun add hono

The simplest Hono app:

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

const app = new Hono();

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

app.get("/api/users", (c) => {
return c.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
});

export default app;

That c parameter is the Context object — Hono's equivalent of (req, res) in Express, but designed for modern runtimes. It uses Web Standard APIs (Request, Response) instead of Node-specific ones.

Routing

Hono's router is one of the fastest in the JavaScript ecosystem. It uses a RegExpRouter by default that compiles all routes into a single regex for O(1) matching.

const app = new Hono();

// Basic routes
app.get("/users", (c) => c.json({ users: [] }));
app.post("/users", (c) => c.json({ created: true }, 201));
app.put("/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ updated: id });
});
app.delete("/users/:id", (c) => c.body(null, 204));

// Route parameters
app.get("/users/:userId/posts/:postId", (c) => {
const { userId, postId } = c.req.param();
return c.json({ userId, postId });
});

// Wildcard
app.get("/files/*", (c) => {
const path = c.req.path;
return c.text(Serving file: ${path});
});

// Route grouping
const api = new Hono();

api.get("/users", (c) => c.json([]));
api.get("/posts", (c) => c.json([]));

app.route("/api/v1", api);

Middleware

Middleware in Hono follows the same pattern as Express but with async/await instead of callbacks.

import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { bearerAuth } from "hono/bearer-auth";
import { compress } from "hono/compress";
import { etag } from "hono/etag";
import { secureHeaders } from "hono/secure-headers";

const app = new Hono();

// Built-in middleware
app.use("*", logger());
app.use("*", cors({ origin: "https://myapp.com" }));
app.use("*", secureHeaders());
app.use("*", compress());
app.use("*", etag());

// Auth middleware for specific routes
app.use("/api/admin/*", bearerAuth({ token: "my-secret-token" }));

// Custom middleware
app.use("*", async (c, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
c.header("X-Response-Time", ${ms}ms);
});

// Rate limiting (custom)
const rateLimit = new Map<string, { count: number; resetAt: number }>();

app.use("/api/*", async (c, next) => {
const ip = c.req.header("cf-connecting-ip") || "unknown";
const now = Date.now();
const entry = rateLimit.get(ip);

if (entry && entry.resetAt > now && entry.count >= 100) {
return c.json({ error: "Rate limit exceeded" }, 429);
}

if (!entry || entry.resetAt <= now) {
rateLimit.set(ip, { count: 1, resetAt: now + 60_000 });
} else {
entry.count++;
}

await next();
});

Type-Safe Request Handling

Hono can validate request bodies, query parameters, and headers with Zod integration — and the types flow through to your handler.

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(1).max(100),
email: z.string().email(),
role: z.enum(["admin", "user"]).default("user"),
});

const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
});

// Validated POST body — type is inferred
app.post(
"/api/users",
zValidator("json", createUserSchema),
async (c) => {
const data = c.req.valid("json");
// data is typed as { name: string; email: string; role: "admin" | "user" }
return c.json({ user: data }, 201);
}
);

// Validated query parameters
app.get(
"/api/users",
zValidator("query", querySchema),
async (c) => {
const { page, limit, search } = c.req.valid("query");
// page: number, limit: number, search: string | undefined
return c.json({ page, limit, search });
}
);

No any types. No manual parsing. If someone sends invalid data, Hono returns a 400 error automatically before your handler runs.

Running on Different Platforms

The same Hono app runs everywhere with minimal adapter changes:

// src/app.ts — shared app logic
import { Hono } from "hono";

const app = new Hono();
app.get("/", (c) => c.json({ runtime: "universal" }));

export default app;

// For Cloudflare Workers — wrangler.toml points to this
export default app;

// For Bun
export default {
port: 3000,
fetch: app.fetch,
};

// For Node.js
import { serve } from "@hono/node-server";
serve({ fetch: app.fetch, port: 3000 });

// For Deno
Deno.serve({ port: 3000 }, app.fetch);

// For AWS Lambda
import { handle } from "hono/aws-lambda";
export const handler = handle(app);

// For Vercel Edge
export const config = { runtime: "edge" };
export default app;

Write once, deploy anywhere. The app logic stays identical.

Hono vs Express vs Fastify

FeatureExpressFastifyHono
Weight~200KB~350KB~14KB
TypeScriptBolted on (@types)Built-inBuilt-in
Edge runtimeNo (Node only)No (Node only)Yes (everywhere)
Requests/sec~15,000~50,000~130,000+
Middleware patterncallback chainhooks + pluginsasync/await chain
ValidatorManual / express-validatorJSON Schema (Ajv)Zod, Valibot, built-in
Cold startSlow (lots of deps)MediumFast (tiny bundle)
CommunityMassiveLargeGrowing fast
Use Hono when:
  • You're deploying to edge runtimes (Cloudflare Workers, Vercel Edge)
  • You need multi-runtime support
  • Cold start time matters (serverless)
  • You want built-in TypeScript without compromise
  • Bundle size is a constraint
Stick with Express/Fastify when:
  • You have a large existing Node.js codebase
  • You need the massive Express middleware ecosystem
  • Your team knows Express inside out and isn't deploying to the edge

Building a Real API

Here's a more complete example — a REST API with authentication, validation, and error handling:

import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { HTTPException } from "hono/http-exception";
import { jwt } from "hono/jwt";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

type Env = {
Variables: {
userId: number;
};
};

const app = new Hono<Env>();

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

// JWT auth for protected routes
app.use("/api/protected/*", jwt({ secret: "your-secret-key" }));

// Error handling
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
console.error(err);
return c.json({ error: "Internal Server Error" }, 500);
});

// 404 handler
app.notFound((c) => {
return c.json({ error: "Not Found", path: c.req.path }, 404);
});

// Public routes
app.post(
"/api/auth/login",
zValidator("json", z.object({
email: z.string().email(),
password: z.string().min(8),
})),
async (c) => {
const { email, password } = c.req.valid("json");
// ... verify credentials, generate JWT
return c.json({ token: "jwt-token-here" });
}
);

// Protected routes
app.get("/api/protected/profile", async (c) => {
const payload = c.get("jwtPayload");
return c.json({ user: payload });
});

// File uploads
app.post("/api/upload", async (c) => {
const body = await c.req.parseBody();
const file = body["file"];

if (file instanceof File) {
const buffer = await file.arrayBuffer();
// Process file...
return c.json({
name: file.name,
size: file.size,
type: file.type,
});
}

return c.json({ error: "No file provided" }, 400);
});

export default app;

RPC Mode — Full-Stack Type Safety

Hono has an RPC feature that gives you tRPC-like type safety between your API and client. Define routes on the server, and the client gets full autocomplete and type checking.

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

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

export type AppType = typeof app;

// Client
import { hc } from "hono/client";
import type { AppType } from "./server";

const client = hc<AppType>("http://localhost:3000");

// Fully typed — autocomplete shows available routes and parameters
const users = await client.api.users.$get();
const data = await users.json(); // typed as { id: number; name: string }[]

const newUser = await client.api.users.$post({
json: { name: "Bob" }, // TypeScript enforces this matches the Zod schema
});

No code generation. No schema syncing. The types are inferred directly from the route definitions.

Hono hits a sweet spot that few frameworks manage — it's small enough for edge deployments, fast enough for high-throughput APIs, and typed enough for serious TypeScript projects. If you're building APIs in 2026 and not locked into Express, give it a serious look. More hands-on framework comparisons available at CodeUp.

Ad 728x90