| name | hono |
| description | Use when building Hono web applications or when the user asks about Hono APIs, routing, middleware, JSX, validation, testing, or streaming. TRIGGER when code imports from 'hono' or 'hono/*', or user mentions Hono. Use `pnpm exec hono request` (or `pnpm dlx hono request`) to test endpoints. |
Hono Skill
Build Hono web applications. This skill provides inline API knowledge for AI. CLI examples use pnpm; Hono itself is package-manager agnostic. If the hono-docs MCP server is configured, prefer its tools for the latest documentation over the inline reference.
pnpm vs npx
pnpm exec hono … — runs the hono binary from this project’s node_modules (use when hono is installed, e.g. devDependencies).
pnpm dlx hono … — downloads and runs the CLI once, similar to npx hono …, when you do not have a local install.
npm/yarn users: substitute npx hono … or yarn dlx hono … for pnpm dlx hono …, or run the local binary with npm exec hono … / yarn hono … where your toolchain exposes it.
Hono CLI Usage
Request Testing
Test endpoints without starting an HTTP server. Uses app.request() internally.
pnpm exec hono request [file] -P /path
pnpm exec hono request [file] -X POST -P /api/users -d '{"name": "test"}'
Without hono in the project, use pnpm dlx instead of pnpm exec for the same flags.
Note: Do not pass credentials directly in CLI arguments. Use environment variables for sensitive values. hono request does not support Cloudflare Workers bindings (KV, D1, R2, etc.). When bindings are required, use workers-fetch instead:
pnpm exec workers-fetch /path
pnpm exec workers-fetch -X POST -H "Content-Type:application/json" -d '{"name":"test"}' /api/users
Use pnpm dlx workers-fetch if the package is not installed locally.
Hono API Reference
App Constructor
import { Hono } from "hono";
const app = new Hono();
type Env = {
Bindings: { DATABASE: D1Database; KV: KVNamespace };
Variables: { user: User };
};
const app = new Hono<Env>();
Routing Methods
app.get("/path", handler);
app.post("/path", handler);
app.put("/path", handler);
app.delete("/path", handler);
app.patch("/path", handler);
app.options("/path", handler);
app.all("/path", handler);
app.on("PURGE", "/path", handler);
app.on(["PUT", "DELETE"], "/path", handler);
Routing Patterns
app.get("/user/:name", (c) => {
const name = c.req.param("name");
return c.json({ name });
});
app.get("/posts/:id/comments/:commentId", (c) => {
const { id, commentId } = c.req.param();
});
app.get("/api/animal/:type?", (c) => c.text("Animal!"));
app.get("/wild/*/card", (c) => c.text("Wildcard"));
app.get("/post/:date{[0-9]+}/:title{[a-z]+}", (c) => {
const { date, title } = c.req.param();
});
app
.get("/endpoint", (c) => c.text("GET"))
.post((c) => c.text("POST"))
.delete((c) => c.text("DELETE"));
Route Grouping
const api = new Hono();
api.get("/users", (c) => c.json([]));
const app = new Hono();
app.route("/api", api);
const app = new Hono().basePath("/api");
app.get("/users", (c) => c.json([]));
Error Handling
app.notFound((c) => c.json({ message: "Not Found" }, 404));
app.onError((err, c) => {
console.error(err);
return c.json({ message: "Internal Server Error" }, 500);
});
Context (c)
Response Methods
c.text("Hello");
c.json({ message: "Hello" });
c.html("<h1>Hello</h1>");
c.redirect("/new-path");
c.redirect("/new-path", 301);
c.body("raw body", 200, headers);
c.notFound();
Headers & Status
c.status(201);
c.header("X-Custom", "value");
c.header("Cache-Control", "no-store");
Variables (request-scoped data)
c.set("user", { id: 1, name: "Alice" });
const user = c.get("user");
const user = c.var.user;
Environment (Cloudflare Workers)
const value = await c.env.KV.get("key");
const db = c.env.DATABASE;
c.executionCtx.waitUntil(promise);
Renderer
app.use(async (c, next) => {
c.setRenderer((content) =>
c.html(
<html><body>{content}</body></html>
)
)
await next()
})
app.get('/', (c) => c.render(<h1>Hello</h1>))
HonoRequest (c.req)
c.req.param("id");
c.req.param();
c.req.query("page");
c.req.query();
c.req.queries("tags");
c.req.header("Authorization");
c.req.header();
await c.req.json();
await c.req.text();
await c.req.formData();
await c.req.parseBody();
await c.req.arrayBuffer();
await c.req.blob();
c.req.valid("json");
c.req.valid("query");
c.req.valid("form");
c.req.valid("param");
c.req.url;
c.req.path;
c.req.method;
c.req.raw;
Middleware
Using Built-in Middleware
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { basicAuth } from "hono/basic-auth";
import { prettyJSON } from "hono/pretty-json";
import { secureHeaders } from "hono/secure-headers";
import { etag } from "hono/etag";
import { compress } from "hono/compress";
import { poweredBy } from "hono/powered-by";
import { timing } from "hono/timing";
import { cache } from "hono/cache";
import { bearerAuth } from "hono/bearer-auth";
import { jwt } from "hono/jwt";
import { csrf } from "hono/csrf";
import { ipRestriction } from "hono/ip-restriction";
import { bodyLimit } from "hono/body-limit";
import { requestId } from "hono/request-id";
import { methodOverride } from "hono/method-override";
import { trailingSlash, trimTrailingSlash } from "hono/trailing-slash";
app.use(logger());
app.use("/api/*", cors());
app.post("/api/*", basicAuth({ username: "admin", password: "secret" }));
Custom Middleware
app.use(async (c, next) => {
const start = Date.now();
await next();
const elapsed = Date.now() - start;
c.res.headers.set("X-Response-Time", `${elapsed}ms`);
});
import { createMiddleware } from "hono/factory";
const auth = createMiddleware(async (c, next) => {
const token = c.req.header("Authorization");
if (!token) return c.json({ error: "Unauthorized" }, 401);
await next();
});
app.use("/api/*", auth);
Middleware Execution Order
Middleware executes in registration order. await next() calls the next middleware/handler, and code after next() runs on the way back:
Request → mw1 before → mw2 before → handler → mw2 after → mw1 after → Response
app.use(async (c, next) => {
await next();
});
Validation
Validation targets: json, form, query, header, param, cookie.
Zod Validator
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const schema = z.object({
title: z.string().min(1),
body: z.string(),
});
app.post("/posts", zValidator("json", schema), (c) => {
const data = c.req.valid("json");
return c.json(data, 201);
});
Valibot / Standard Schema Validator
import { sValidator } from "@hono/standard-validator";
import * as v from "valibot";
const schema = v.object({ name: v.string(), age: v.number() });
app.post("/users", sValidator("json", schema), (c) => {
const data = c.req.valid("json");
return c.json(data, 201);
});
JSX
Setup
In tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
Or use pragma: /** @jsxImportSource hono/jsx */
Important: Files using JSX must have a .tsx extension. Rename .ts to .tsx or the compiler will fail.
Components
import type { PropsWithChildren } from "hono/jsx";
const Layout = (props: PropsWithChildren) => (
<html>
<head>
<title>My App</title>
</head>
<body>{props.children}</body>
</html>
);
const UserCard = ({ name }: { name: string }) => (
<div class="card">
<h2>{name}</h2>
</div>
);
app.get("/", (c) => {
return c.html(
<Layout>
<UserCard name="Alice" />
</Layout>,
);
});
jsxRenderer Middleware
Use jsxRenderer middleware for layouts. See pnpm exec hono docs /docs/middleware/builtin/jsx-renderer (or pnpm dlx hono docs … without a local install) for details.
Async Components
const UserList = async () => {
const users = await fetchUsers();
return (
<ul>
{users.map((u) => (
<li>{u.name}</li>
))}
</ul>
);
};
Fragments
const Items = () => (
<>
<li>Item 1</li>
<li>Item 2</li>
</>
);
Streaming
import { stream, streamText, streamSSE } from "hono/streaming";
app.get("/stream", (c) => {
return stream(c, async (stream) => {
stream.onAbort(() => console.log("Aborted"));
await stream.write(new Uint8Array([0x48, 0x65]));
await stream.pipe(readableStream);
});
});
app.get("/stream-text", (c) => {
return streamText(c, async (stream) => {
await stream.writeln("Hello");
await stream.sleep(1000);
await stream.write("World");
});
});
app.get("/sse", (c) => {
return streamSSE(c, async (stream) => {
let id = 0;
while (true) {
await stream.writeSSE({
data: JSON.stringify({ time: new Date().toISOString() }),
event: "time-update",
id: String(id++),
});
await stream.sleep(1000);
}
});
});
Testing with app.request()
Test endpoints without starting an HTTP server:
const res = await app.request("/posts");
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ posts: [] });
const res = await app.request("/posts", {
method: "POST",
body: JSON.stringify({ title: "Hello" }),
headers: { "Content-Type": "application/json" },
});
const formData = new FormData();
formData.append("name", "Alice");
const res = await app.request("/users", { method: "POST", body: formData });
const res = await app.request(
"/api/data",
{},
{ KV: mockKV, DATABASE: mockDB },
);
const req = new Request("http://localhost/api", { method: "DELETE" });
const res = await app.request(req);
Hono Client (RPC)
Type-safe API client using shared types between server and client.
IMPORTANT: Routes MUST be chained for type inference to work. Without chaining, the client cannot infer route types.
const route = app
.post("/posts", zValidator("json", schema), (c) => {
return c.json({ ok: true }, 201);
})
.get("/posts", (c) => {
return c.json({ posts: [] });
});
export type AppType = typeof route;
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("http://localhost:8787/");
const res = await client.posts.$post({ json: { title: "Hello" } });
const data = await res.json();
Type utilities:
import type { InferRequestType, InferResponseType } from "hono/client";
type ReqType = InferRequestType<typeof client.posts.$post>;
type ResType = InferResponseType<typeof client.posts.$post, 200>;
Helpers
Helpers are utility functions imported from hono/<helper-name>:
import { getConnInfo } from "hono/conninfo";
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
import { css, Style } from "hono/css";
import { createFactory } from "hono/factory";
import { html, raw } from "hono/html";
import { stream, streamText, streamSSE } from "hono/streaming";
import { testClient } from "hono/testing";
import { upgradeWebSocket } from "hono/cloudflare-workers";
Available helpers: Accepts, Adapter, ConnInfo, Cookie, css, Dev, Factory, html, JWT, Proxy, Route, SSG, Streaming, Testing, WebSocket.
For details, use pnpm exec hono docs /docs/helpers/<helper-name> (or pnpm dlx hono docs …).
Factory
Use createFactory to define Env once and share it across app, middleware, and handlers:
import { createFactory } from "hono/factory";
const factory = createFactory<Env>();
const app = factory.createApp();
const mw = factory.createMiddleware(async (c, next) => {
await next();
});
const handlers = factory.createHandlers(logger(), (c) =>
c.json({ message: "Hello" }),
);
app.get("/api", ...handlers);
Best Practices
- Write handlers inline in route definitions for proper type inference of path params.
- Use
app.route() to organize large apps by feature, not Rails-style controllers.
- Use
createFactory() to share Env type across app, middleware, and handlers.
- Use
c.set()/c.get() to pass data between middleware and handlers.
- Chain validators for multiple request parts (param + query + json).
- Export app type for RPC:
export type AppType = typeof routes
- Use
app.request() for testing — no server startup needed.
Adapters
Hono runs on multiple runtimes. The default export works for Cloudflare Workers, Deno, and Bun. For Node.js, use the Node adapter:
export default app;
import { serve } from "@hono/node-server";
serve(app);