Spoosh
Type Adapters

Hono

Type-safe API toolkit from your Hono server

The @spoosh/hono package transforms your Hono app type into Spoosh's ApiSchema format, giving you end-to-end type safety.

For proper type inference, your Hono routes must follow the Hono RPC guide. Chain your routes directly on the app instance and export the app type.

Installation

npm install @spoosh/hono

Setup

Server (Hono)

Define your Hono routes and export the app type:

server.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const app = new Hono()
  .basePath("/api")
  .get("/posts", (c) => {
    return c.json([
      { id: 1, title: "Hello World" },
      { id: 2, title: "Getting Started" },
    ]);
  })
  .post("/posts", zValidator("json", z.object({ title: z.string() })), (c) => {
    const body = c.req.valid("json");
    return c.json({ id: 3, title: body.title });
  })
  .get("/posts/:id", (c) => {
    const id = c.req.param("id");
    return c.json({ id: Number(id), title: "Post Title" });
  })
  .delete("/posts/:id", (c) => {
    return c.json({ success: true });
  });

// Export the app type for client usage
export type AppType = typeof app;

Client (Spoosh)

Use HonoToSpoosh with your Hono app type:

client.ts
import { Spoosh, StripPrefix } from "@spoosh/core";
import { createReactSpoosh } from "@spoosh/react";
import type { HonoToSpoosh } from "@spoosh/hono";
import type { AppType } from "./server";

// Full schema has prefixed paths: "api/posts", "api/posts/:id"
type FullSchema = HonoToSpoosh<AppType>;

// Since baseUrl already includes "/api", strip the prefix to avoid
// double prefixing (e.g., "api/api/posts")
type ApiSchema = StripPrefix<FullSchema, "api">;

const spoosh = new Spoosh<ApiSchema, Error>("http://localhost:3000/api");

export const { useRead, useWrite } = createReactSpoosh(spoosh);

Usage

All API calls are fully typed:

// GET /api/posts
const { data: posts } = await api("posts").GET();
// posts: { id: number; title: string }[]

// POST /api/posts
const { data: newPost } = await api("posts").POST({
  body: { title: "New Post" }, // body is typed
});
// newPost: { id: number; title: string }

// GET /api/posts/1
const { data: post } = await api("posts/:id").GET({ params: { id: 1 } });
// post: { id: number; title: string }

// DELETE /api/posts/1
await api("posts/:id").DELETE({ params: { id: 1 } });

Type Mapping

HonoSpoosh
c.json(data)Response data type
zValidator("json", schema)Request body type
zValidator("query", schema)Query params type
zValidator("form", schema)Form data type
/posts/:id"posts/:id" (path-based key)

Path Parameters

Dynamic segments (:id, :slug, etc.) are converted to path-based keys in the schema and accessed with params:

// Hono route: /users/:userId/posts/:postId

// Path-based access with params
api("users/:userId/posts/:postId").GET({
  params: { userId: 123, postId: 456 },
});

// With variables
const userId = 123;
const postId = 456;
api("users/:userId/posts/:postId").GET({
  params: { userId, postId },
});

With React Hooks

import { useRead, useWrite } from "./api/client";

function UserProfile({ userId }: { userId: number }) {
  // Path-based access with useRead
  const { data: user } = useRead((api) =>
    api("users/:id").GET({ params: { id: userId } })
  );

  // Path-based access with useWrite
  const { trigger: updateUser } = useWrite((api) => api("users/:id").PUT);
  const { trigger: deleteUser } = useWrite((api) => api("users/:id").DELETE);

  const handleUpdate = async () => {
    await updateUser({ params: { id: userId }, body: { name: "Updated" } });
  };

  return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={handleUpdate}>Update</button>
    </div>
  );
}

Handling Large Apps (TS2589)

Due to Spoosh's flat schema structure, TS2589 errors are rare. If you do encounter them with very large route trees, follow the Hono RPC best practices for splitting routes into smaller groups.

On this page