hono-query-rpc
v0.1.4
Published
tRPC-style TanStack Query integration for Hono RPC
Maintainers
Readme
hono-query-rpc
A utility library that brings tRPC-like developer experience when using Hono RPC with TanStack Query.
Uses TanStack Query's queryOptions / mutationOptions pattern as-is.
const { data } = useQuery(
api.users.$get.queryOptions({ query: { page: "1" } }),
);
const create = useMutation(api.users.$post.mutationOptions({}));Key features
- Auto-generated query functions —
queryOptions()andmutationOptions()are derived directly from the Hono RPC client type, so there is no boilerplate to write. - Query key management —
queryKey()returns a stable, type-safe key for every endpoint. Use it for cache invalidation without hardcoding strings. - Auto Idempotency-Key injection — mutation requests automatically receive a unique
Idempotency-Keyheader (refreshed after each successful mutation) to prevent accidental duplicate writes. Disable withautoIdempotency: false.
Installation
# npm
npm install hono-query-rpc
# pnpm
pnpm add hono-query-rpc
# bun
bun add hono-query-rpcPeer dependencies
bun add hono @tanstack/react-query reactQuick Start
// lib/api.ts
import { hc } from "hono/client";
import { createHonoQuery } from "hono-query-rpc";
import type { AppType } from "@/server";
const client = hc<AppType>("/");
export const api = createHonoQuery(client);// components/UserList.tsx
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
function UserList() {
const { data, isLoading } = useQuery(api.api.users.$get.queryOptions({}));
if (isLoading) {
return <p>Loading...</p>;
}
return (
<ul>
{data?.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}API
createHonoQuery(client, options?)
Creates a proxy client that wraps a Hono RPC client with TanStack Query integration.
| Option | Type | Default | Description |
| ----------------- | ------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| defaultHeaders | HeadersFactory | { "content-type": "application/json", "accept": "application/json" } | Headers merged into every request. Accepts a static object, a Headers instance, an entry tuple array, or a (async) getter function. Providing this option replaces the default headers entirely. |
| autoIdempotency | boolean | true | Automatically adds an Idempotency-Key header to mutation requests. The key is refreshed after each successful mutation. |
| parseResponse | (res: Response) => unknown \| Promise<unknown> | Throws HTTPError on !res.ok, otherwise returns res.json() | Customize how responses are parsed and errors are thrown. |
.queryOptions(input, options?)
Returns a TanStack Query queryOptions object. Pass undefined as input when the endpoint takes no parameters.
// With input
useQuery(api.api.users.$get.queryOptions({ query: { page: "1" } }));
// Without input
useQuery(api.api.users.$get.queryOptions());
// With TanStack Query options
useQuery(api.api.users.$get.queryOptions({}, { enabled: false }));
// With per-call hono headers
useQuery(
api.api.users.$get.queryOptions(
{},
{
hono: { headers: { "x-trace-id": crypto.randomUUID() } },
},
),
);.mutationOptions(options?)
Returns a TanStack Query mutationOptions object.
// Basic
useMutation(api.api.users.$post.mutationOptions({}));
// With TanStack Query callbacks
useMutation(
api.api.users.$post.mutationOptions({
onSuccess: () => queryClient.invalidateQueries(...),
onError: (err) => console.error(err),
}),
);
// With per-call hono headers
useMutation(
api.api.users.$post.mutationOptions({
hono: { headers: { "x-custom": "value" } },
}),
);.queryKey(input?)
Returns the query key for manual cache operations.
queryClient.invalidateQueries({ queryKey: api.users.$get.queryKey() });
queryClient.invalidateQueries({
queryKey: api.users.$get.queryKey({ query: { page: "1" } }),
});Header Management
Headers are managed in two layers with the following priority (call level overrides factory level):
Factory level — applied to all requests
// Static
const api = createHonoQuery(client, {
defaultHeaders: { "x-app-id": "my-app" },
});
// Dynamic (evaluated on every request)
const api = createHonoQuery(client, {
defaultHeaders: () => ({
authorization: `Bearer ${useAuthStore.getState().token}`,
}),
});
// Async dynamic
const api = createHonoQuery(client, {
defaultHeaders: async () => ({
authorization: `Bearer ${await refreshTokenIfNeeded()}`,
}),
});
// Auto idempotency key for mutation requests (default: true, disable with false)
const api = createHonoQuery(client, {
autoIdempotency: false,
});Call level — applied to a specific request only
// queryOptions
useQuery(
api.users.$get.queryOptions(input, {
hono: { headers: { "x-trace-id": crypto.randomUUID() } },
}),
);
// mutationOptions
useMutation(
api.users.$post.mutationOptions({
hono: { headers: { "x-custom": "value" } },
}),
);Custom Response Parsing
The default behavior is equivalent to:
import { HTTPError } from "hono-query-rpc";
// default parseResponse
(res) => {
if (!res.ok) {
throw new HTTPError(res);
}
return res.json();
};You can override this with parseResponse:
import { createHonoQuery } from "hono-query-rpc";
const api = createHonoQuery(client, {
parseResponse: async (res) => {
if (!res.ok) {
const body = await res.json();
throw new MyAppError(res.status, body.message);
}
return res.json();
},
});HTTPError
The default error class thrown on non-OK responses.
import { HTTPError } from "hono-query-rpc";
// err.status — HTTP status code (e.g. 404)
// err.statusText — HTTP status text (e.g. "Not Found")
// err.response — original Response objectExample
A minimal blog app (Next.js 15 + Hono + TanStack Query) is included in the example/ directory.
# 1. Build the library (from repo root)
bun run build
# 2. Install example dependencies
cd example
bun install
# 3. Start dev server
bun run devOpen http://localhost:3000.
What the example demonstrates
| Feature | Where |
| ------------------------- | --------------------------------------------------------- |
| queryOptions() | app/page.tsx — fetch all posts |
| queryOptions({ param }) | app/posts/[id]/page.tsx — fetch single post |
| mutationOptions() | components/CreatePostModal.tsx — create post |
| mutationOptions() | app/page.tsx + app/posts/[id]/page.tsx — delete post |
| queryKey() invalidation | after create/delete mutations |
| HTTPError handling | 404 on post detail page |
| Error testing panel | app/page.tsx — trigger query/mutation errors in-browser |
| TanStack Query DevTools | bottom-right corner of the example app |
Development
Prerequisites
# Install dependencies
bun install
# Build
bun run build
# Test
bun test
# Test (watch mode)
bun test --watch
# Type check
bun run typecheckLicense
MIT
