Skip to main content
Technology & EngineeringApi Frameworks341 lines

Hono

"Hono: ultra-fast web framework for edge, serverless, and Node.js — middleware, routing, Zod validation, JWT, CORS, RPC client, and JSX support"

Quick Summary18 lines
Hono is an ultra-fast, lightweight web framework that runs everywhere: Cloudflare Workers, Deno Deploy, Bun, AWS Lambda, Vercel Edge Functions, and Node.js. It uses Web Standard APIs (Request/Response) so code is portable across runtimes without modification. Hono provides first-class TypeScript support with type-safe routing, a rich middleware ecosystem, and a built-in RPC client that gives you tRPC-like type safety without a separate protocol.

## Key Points

- Use `hc` RPC client for internal service-to-service or frontend-to-backend calls to get full type safety without codegen.
- Chain route definitions on a single Hono instance so the `AppType` export captures all routes for the RPC client.
- Validate all inputs with `zValidator` — it integrates cleanly and gives typed access via `c.req.valid()`.
- Use environment-specific adapters (`@hono/node-server`, Cloudflare Workers export) but keep handler logic runtime-agnostic.
- Scope middleware with path patterns (`/api/*`) rather than applying everything globally.
- Use `HTTPException` for expected errors and let `onError` handle unexpected ones uniformly.
- Prefer `c.env` for runtime bindings (secrets, KV namespaces) to stay compatible across edge runtimes.
- **Importing Node.js built-ins in edge-targeted code.** Hono runs on Web Standards; `fs`, `path`, and `http` do not exist on Workers or Deno Deploy.
- **Not chaining routes.** If you define routes with separate `app.get()` calls without chaining, the RPC client type loses track of the routes.
- **Blocking the event loop with synchronous work.** Edge runtimes have strict CPU time limits; offload heavy computation.
- **Hardcoding secrets.** Use `c.env` bindings or environment variables, never string literals in source.
- **Ignoring response status codes.** Always return appropriate HTTP status codes (201 for creation, 404 for not found); the RPC client types include status.
skilldb get api-frameworks-skills/HonoFull skill: 341 lines
Paste into your CLAUDE.md or agent config

Hono

Core Philosophy

Hono is an ultra-fast, lightweight web framework that runs everywhere: Cloudflare Workers, Deno Deploy, Bun, AWS Lambda, Vercel Edge Functions, and Node.js. It uses Web Standard APIs (Request/Response) so code is portable across runtimes without modification. Hono provides first-class TypeScript support with type-safe routing, a rich middleware ecosystem, and a built-in RPC client that gives you tRPC-like type safety without a separate protocol.

The framework prioritizes zero dependencies, minimal bundle size, and router performance. Its RegExpRouter achieves near-zero overhead for route matching, making it ideal for latency-sensitive edge deployments.

Setup

Basic Application

// Install: npm install hono
// For Node.js: npm install @hono/node-server

// src/index.ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";

const app = new Hono();

app.get("/", (c) => c.text("Hello Hono!"));
app.get("/json", (c) => c.json({ message: "typed response", status: "ok" }));

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

// For Cloudflare Workers — just export
// export default app;

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

Typed Application with Base Path

import { Hono } from "hono";

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

type Variables = {
  user: { id: string; email: string; role: string };
};

const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
  .basePath("/api/v1");

Key Techniques

Routing and Route Groups

import { Hono } from "hono";

const app = new Hono();

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

// Wildcards and regex
app.get("/files/*", (c) => c.text(`Path: ${c.req.path}`));
app.get("/posts/:id{[0-9]+}", (c) => c.json({ id: c.req.param("id") }));

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

const users = new Hono()
  .get("/", async (c) => {
    const users = await db.user.findMany();
    return c.json(users);
  })
  .post("/", async (c) => {
    const body = await c.req.json();
    const user = await db.user.create({ data: body });
    return c.json(user, 201);
  })
  .get("/:id", async (c) => {
    const user = await db.user.findUnique({ where: { id: c.req.param("id") } });
    if (!user) return c.json({ error: "Not found" }, 404);
    return c.json(user);
  });

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

Middleware

import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { secureHeaders } from "hono/secure-headers";
import { timing, startTime, endTime } from "hono/timing";
import { compress } from "hono/compress";

// Built-in middleware
app.use("*", logger());
app.use("*", secureHeaders());
app.use("*", timing());
app.use("*", compress());
app.use("*", prettyJSON());
app.use(
  "/api/*",
  cors({
    origin: ["https://example.com", "https://staging.example.com"],
    allowMethods: ["GET", "POST", "PUT", "DELETE"],
    allowHeaders: ["Content-Type", "Authorization"],
    maxAge: 86400,
  })
);

// Custom middleware
const rateLimiter = (limit: number, window: number) => {
  const store = new Map<string, { count: number; reset: number }>();

  return async (c: Context, next: Next) => {
    const key = c.req.header("CF-Connecting-IP") ?? "unknown";
    const now = Date.now();
    const record = store.get(key);

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

    if (!record || record.reset <= now) {
      store.set(key, { count: 1, reset: now + window });
    } else {
      record.count++;
    }

    await next();
  };
};

app.use("/api/*", rateLimiter(100, 60_000));

Zod Validation with zValidator

// npm install @hono/zod-validator
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).max(5).default([]),
  published: z.boolean().default(false),
});

const paginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(["asc", "desc"]).default("desc"),
});

const posts = new Hono()
  .get("/", zValidator("query", paginationSchema), async (c) => {
    const { page, limit, sort } = c.req.valid("query");
    const offset = (page - 1) * limit;
    const posts = await db.post.findMany({
      skip: offset,
      take: limit,
      orderBy: { createdAt: sort },
    });
    return c.json({ posts, page, limit });
  })
  .post("/", zValidator("json", createPostSchema), async (c) => {
    const data = c.req.valid("json");
    const post = await db.post.create({ data });
    return c.json(post, 201);
  })
  .put(
    "/:id",
    zValidator("param", z.object({ id: z.string().uuid() })),
    zValidator("json", createPostSchema.partial()),
    async (c) => {
      const { id } = c.req.valid("param");
      const data = c.req.valid("json");
      const post = await db.post.update({ where: { id }, data });
      return c.json(post);
    }
  );

JWT Authentication

import { jwt } from "hono/jwt";
import { sign, verify } from "hono/jwt";

// JWT middleware for protected routes
app.use(
  "/api/protected/*",
  jwt({ secret: process.env.JWT_SECRET! })
);

// Login endpoint
app.post("/api/auth/login", zValidator("json", loginSchema), async (c) => {
  const { email, password } = c.req.valid("json");
  const user = await authenticateUser(email, password);

  if (!user) return c.json({ error: "Invalid credentials" }, 401);

  const token = await sign(
    { sub: user.id, email: user.email, role: user.role, exp: Math.floor(Date.now() / 1000) + 3600 },
    c.env.JWT_SECRET
  );

  return c.json({ token, user: { id: user.id, email: user.email } });
});

// Access JWT payload in protected routes
app.get("/api/protected/profile", (c) => {
  const payload = c.get("jwtPayload");
  return c.json({ userId: payload.sub, email: payload.email });
});

RPC Client (Type-Safe Client)

// server.ts — Export the typed app
const routes = app
  .get("/api/users", async (c) => {
    const users = await db.user.findMany();
    return c.json(users);
  })
  .post(
    "/api/users",
    zValidator("json", createUserSchema),
    async (c) => {
      const data = c.req.valid("json");
      const user = await db.user.create({ data });
      return c.json(user, 201);
    }
  );

export type AppType = typeof routes;

// client.ts — Fully typed client
import { hc } from "hono/client";
import type { AppType } from "./server";

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

// Full type safety and autocompletion
const res = await client.api.users.$get();
const users = await res.json(); // typed as User[]

const newUser = await client.api.users.$post({
  json: { name: "Alice", email: "alice@example.com" },
});

JSX and HTML Responses

import { Hono } from "hono";
import { html } from "hono/html";

const app = new Hono();

const Layout = (props: { title: string; children: any }) => html`
  <!DOCTYPE html>
  <html>
    <head><title>${props.title}</title></head>
    <body>${props.children}</body>
  </html>
`;

app.get("/page", (c) => {
  return c.html(
    Layout({
      title: "Dashboard",
      children: html`<h1>Welcome</h1><p>Server-rendered HTML with Hono.</p>`,
    })
  );
});

Error Handling

import { HTTPException } from "hono/http-exception";

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

app.notFound((c) => c.json({ error: "Route not found" }, 404));

// Throwing HTTP exceptions in handlers
app.get("/api/resource/:id", async (c) => {
  const resource = await db.resource.findUnique({ where: { id: c.req.param("id") } });
  if (!resource) {
    throw new HTTPException(404, { message: "Resource not found" });
  }
  return c.json(resource);
});

Best Practices

  • Use hc RPC client for internal service-to-service or frontend-to-backend calls to get full type safety without codegen.
  • Chain route definitions on a single Hono instance so the AppType export captures all routes for the RPC client.
  • Validate all inputs with zValidator — it integrates cleanly and gives typed access via c.req.valid().
  • Use environment-specific adapters (@hono/node-server, Cloudflare Workers export) but keep handler logic runtime-agnostic.
  • Scope middleware with path patterns (/api/*) rather than applying everything globally.
  • Use HTTPException for expected errors and let onError handle unexpected ones uniformly.
  • Prefer c.env for runtime bindings (secrets, KV namespaces) to stay compatible across edge runtimes.

Anti-Patterns

  • Importing Node.js built-ins in edge-targeted code. Hono runs on Web Standards; fs, path, and http do not exist on Workers or Deno Deploy.
  • Not chaining routes. If you define routes with separate app.get() calls without chaining, the RPC client type loses track of the routes.
  • Blocking the event loop with synchronous work. Edge runtimes have strict CPU time limits; offload heavy computation.
  • Hardcoding secrets. Use c.env bindings or environment variables, never string literals in source.
  • Ignoring response status codes. Always return appropriate HTTP status codes (201 for creation, 404 for not found); the RPC client types include status.
  • Using body parsing without validation. Raw c.req.json() gives any; always pair with Zod validation for safety.

Install this skill directly: skilldb add api-frameworks-skills

Get CLI access →