ProvenPin Install

Hono RPC typed client with zod validator: working versions (tested 2026-10-15)

Expose a Hono app as an RPC-typed AppType, call it in-process with hc and app.request, and validate JSON bodies with @hono/zod-validator.

passed2 passed · 0 failed2026-10-15 14:59 UTC
Status
passed
Last passed
2026-10-15 14:59 UTC
Tests
2 / 2
Language
TypeScript
Runtime
node v22.23.3
pinned versions5
hono@4.13.13zod@4.6.7@hono/zod-validator@0.9.1@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38755782594

The code

Exactly the files that passed. Your agent can pull them with get_recipe("hono-rpc-typed-client") (install), or see them on GitHub.

package.json
{
  "name": "hono-rpc-typed-client",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "hono": "4.13.13",
    "zod": "4.6.7",
    "@hono/zod-validator": "0.9.1"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/app.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const CreateItem = z.object({
  name: z.string().min(2),
  qty: z.number().int().positive(),
});

const items: Array<{ id: number; name: string; qty: number }> = [];

export const app = new Hono()
  .post("/items", zValidator("json", CreateItem), (c) => {
    const body = c.req.valid("json");
    const item = { id: items.length + 1, ...body };
    items.push(item);
    return c.json(item, 201);
  })
  .get("/items/:id", (c) => {
    const id = Number(c.req.param("id"));
    const item = items.find((i) => i.id === id);
    if (!item) return c.json({ error: "not_found" }, 404);
    return c.json(item);
  })
  .get("/items", (c) => c.json(items));

export type AppType = typeof app;

export function reset() {
  items.length = 0;
}
src/client.ts
import { hc } from "hono/client";
import type { AppType } from "./app.ts";

/** Build an RPC client against any fetch-compatible base (including app.request). */
export function makeClient(baseUrl: string, fetchImpl: typeof fetch) {
  return hc<AppType>(baseUrl, { fetch: fetchImpl });
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { app, reset } from "../src/app.ts";
import { makeClient } from "../src/client.ts";

test("rpc client creates and fetches an item", async () => {
  reset();
  const client = makeClient("http://local", (input, init) => app.request(input, init));
  const created = await client.items.$post({ json: { name: "bolt", qty: 3 } });
  assert.equal(created.status, 201);
  const body = await created.json();
  assert.equal(body.name, "bolt");
  const got = await client.items[":id"].$get({ param: { id: String(body.id) } });
  assert.equal(got.status, 200);
  assert.deepEqual(await got.json(), body);
});

test("invalid body is 400", async () => {
  reset();
  const res = await app.request("/items", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ name: "x", qty: 0 }),
  });
  assert.equal(res.status, 400);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.