Skills hono-rpc
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/hono-rpc" ~/.claude/skills/terminalskills-skills-hono-rpc && rm -rf "$T"
manifest:
skills/hono-rpc/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
Hono RPC
Overview
Hono RPC provides end-to-end type safety between a Hono server and its clients. Define routes with typed validators on the server, export the
AppType, and use the hc client to make fully type-safe HTTP calls from the frontend or another service — no code generation, no schema files.
Installation
# npm / Node.js npm install hono npm install zod @hono/zod-validator # Bun bun add hono zod @hono/zod-validator # Deno (deno.json imports) # "hono": "jsr:@hono/hono@^4"
Server Setup
Basic Route Definition
// src/server.ts import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; const app = new Hono(); // Define a route with typed response const route = app.get("/hello", (c) => { return c.json({ message: "Hello, Hono!" }); }); export type AppType = typeof route; export default app;
Full API with Multiple Routes
// src/server.ts import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; // Schemas const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), age: z.number().int().positive().optional(), }); const updateUserSchema = createUserSchema.partial(); // Router const users = new Hono() .get("/", (c) => { const users = [{ id: 1, name: "Alice", email: "alice@example.com" }]; return c.json({ users }); }) .get("/:id", (c) => { const id = Number(c.req.param("id")); return c.json({ user: { id, name: "Alice", email: "alice@example.com" } }); }) .post( "/", zValidator("json", createUserSchema), async (c) => { const data = c.req.valid("json"); // Fully typed const user = { id: Date.now(), ...data }; return c.json({ user }, 201); } ) .patch( "/:id", zValidator("json", updateUserSchema), async (c) => { const id = Number(c.req.param("id")); const data = c.req.valid("json"); return c.json({ user: { id, ...data } }); } ) .delete("/:id", (c) => { const id = Number(c.req.param("id")); return c.json({ deleted: id }); }); const app = new Hono().route("/users", users); // Export the type — this is the key to Hono RPC export type AppType = typeof app; export default app;
Client Usage
// src/client.ts import { hc } from "hono/client"; import type { AppType } from "./server"; // Create the typed client const client = hc<AppType>("http://localhost:3000"); // All calls are fully type-safe async function main() { // GET /users — response is typed const res = await client.users.$get(); const { users } = await res.json(); // users: { id: number; name: string; email: string }[] // POST /users — request body is type-checked const createRes = await client.users.$post({ json: { name: "Bob", email: "bob@example.com", age: 30, }, }); const { user } = await createRes.json(); // GET /users/:id — typed param const userRes = await client.users[":id"].$get({ param: { id: "1" } }); const data = await userRes.json(); // PATCH /users/:id await client.users[":id"].$patch({ param: { id: "1" }, json: { name: "Bob Updated" }, }); // DELETE /users/:id await client.users[":id"].$delete({ param: { id: "1" } }); }
Query Parameters
// Server — define query schema const searchSchema = z.object({ q: z.string().optional(), page: z.coerce.number().default(1), limit: z.coerce.number().default(20), }); const app = new Hono().get( "/search", zValidator("query", searchSchema), (c) => { const { q, page, limit } = c.req.valid("query"); return c.json({ results: [], q, page, limit }); } ); export type AppType = typeof app; // Client const client = hc<AppType>("http://localhost:3000"); const res = await client.search.$get({ query: { q: "hono", page: "1", limit: "10" }, });
Headers and Auth
// Pass headers in the client factory const client = hc<AppType>("http://localhost:3000", { headers: { Authorization: `Bearer ${token}`, }, }); // Or per-request const res = await client.users.$get(undefined, { headers: { "X-Request-ID": crypto.randomUUID() }, });
Cloudflare Workers Deployment
// src/index.ts import { Hono } from "hono"; import { cors } from "hono/cors"; import { logger } from "hono/logger"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod"; const app = new Hono<{ Bindings: CloudflareBindings }>(); // Middleware app.use("*", logger()); app.use( "/api/*", cors({ origin: ["https://myapp.com"], allowMethods: ["GET", "POST", "PUT", "DELETE"] }) ); // Routes with D1 database binding const api = app .get("/api/items", async (c) => { const db = c.env.DB; // Cloudflare D1 const items = await db.prepare("SELECT * FROM items").all(); return c.json({ items: items.results }); }) .post( "/api/items", zValidator("json", z.object({ name: z.string(), price: z.number() })), async (c) => { const { name, price } = c.req.valid("json"); const db = c.env.DB; await db.prepare("INSERT INTO items (name, price) VALUES (?, ?)").bind(name, price).run(); return c.json({ ok: true }, 201); } ); export type AppType = typeof api; export default app;
# wrangler.toml name = "my-api" main = "src/index.ts" compatibility_date = "2024-01-01" [[d1_databases]] binding = "DB" database_name = "my-db" database_id = "your-database-id"
Sharing Types with Frontend
// packages/api/src/types.ts — shared package export type { AppType } from "./server"; // packages/web/src/api.ts — frontend import { hc } from "hono/client"; import type { AppType } from "@myapp/api"; export const api = hc<AppType>(import.meta.env.VITE_API_URL); // React component — fully typed const { data } = useQuery({ queryKey: ["users"], queryFn: async () => { const res = await api.users.$get(); return res.json(); }, });
Type Inference Helpers
import type { InferRequestType, InferResponseType } from "hono/client"; import type { AppType } from "./server"; const client = hc<AppType>("http://localhost:3000"); // Infer types from client methods type CreateUserRequest = InferRequestType<typeof client.users.$post>; type CreateUserResponse = InferResponseType<typeof client.users.$post, 201>; // Use in form handlers async function createUser(data: CreateUserRequest["json"]) { const res = await client.users.$post({ json: data }); const result: CreateUserResponse = await res.json(); return result; }
Error Handling
// Server — use HTTPException import { HTTPException } from "hono/http-exception"; app.get("/users/:id", (c) => { const user = findUser(c.req.param("id")); if (!user) { throw new HTTPException(404, { message: "User not found" }); } return c.json({ user }); }); // Global error handler 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); }); // Client — check response status const res = await client.users[":id"].$get({ param: { id: "999" } }); if (!res.ok) { const error = await res.json(); console.error("Error:", error); }
Guidelines
- Export
from the server file asAppType
(not as an interface) to preserve route types.typeof app - Keep schemas close to routes so types stay in sync.
- Use
for all user input — body, query, params, headers.zValidator - In monorepos, put
in a shared package so both server and client import from one place.AppType - Use
andInferRequestType
to derive types for form handlers and tests.InferResponseType - Hono RPC works on any runtime — Cloudflare Workers, Deno Deploy, Bun, and Node.js.
- The
client uses nativehc
under the hood — no special transport needed.fetch