Use Hono RPC when a React client should call a Hono route with inferred request and response types. Add TanStack Query when the UI needs caching, request state, and invalidation around those calls.
npm install hono @tanstack/react-query zod @hono/zod-validatorDefine Typed Routes
Chain route methods on the same Hono instance. Hono infers the client contract from that chained route tree.
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const router = new Hono()
.get('/users', async (c) => {
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
return c.json({ users });
})
.get('/users/:id', async (c) => {
const id = c.req.param('id');
return c.json({ id, name: 'Alice', email: 'alice@example.com' });
})
.post('/users', zValidator('json', createUserSchema), async (c) => {
const body = c.req.valid('json');
const user = { id: crypto.randomUUID(), ...body };
return c.json({ user }, 201);
})
.delete('/users/:id', async (c) => {
const id = c.req.param('id');
return c.json({ deleted: id });
});
export type UsersRoute = typeof router;
export default router;Route files can import service clients, read environment variables, or use server-only framework APIs. Export their types through a type-only file so the browser never imports the route module as runtime code.
Export Route Types
Use a dedicated type-only file for client imports:
export type { UsersRoute } from '../api/users';
export type { PostsRoute } from '../api/posts';Then create the browser client:
import { hc } from 'hono/client';
import type { UsersRoute } from '../shared/api-types';
export const usersClient = hc<UsersRoute>('/api');The hc() base URL must match where the route is mounted. For a Hono app mounted at /api, use '/api'. For a framework proxy or deployed app with a different base path, use that path instead.
Add TanStack Query
Wrap your React app once:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRoot } from 'react-dom/client';
import { App } from './App';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
},
},
});
const root = document.getElementById('root');
if (!root) {
throw new Error('Root element not found');
}
createRoot(root).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);Create query and mutation hooks around the Hono client:
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { usersClient } from './api';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await usersClient.users.$get();
if (!res.ok) {
throw new Error(`Failed to fetch users: ${res.status}`);
}
return res.json();
},
});
}
export function useUser(id: string) {
return useQuery({
queryKey: ['users', id],
queryFn: async () => {
const res = await usersClient.users[':id'].$get({ param: { id } });
if (!res.ok) {
throw new Error(`Failed to fetch user: ${res.status}`);
}
return res.json();
},
enabled: id.length > 0,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: { readonly name: string; readonly email: string }) => {
const res = await usersClient.users.$post({ json: data });
if (!res.ok) {
throw new Error(`Failed to create user: ${res.status}`);
}
return res.json();
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}Use the hooks from a component:
import { useState } from 'react';
import { useCreateUser, useUsers } from './use-users';
export function UserList() {
const { data, error, isLoading } = useUsers();
const createUser = useCreateUser();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<form
onSubmit={(event) => {
event.preventDefault();
createUser.mutate({ name, email });
setName('');
setEmail('');
}}
>
<input value={name} onChange={(event) => setName(event.target.value)} />
<input value={email} onChange={(event) => setEmail(event.target.value)} />
<button disabled={createUser.isPending} type="submit">
{createUser.isPending ? 'Adding...' : 'Add User'}
</button>
</form>
<ul>
{data?.users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}Compose Multiple Routers
When you split routes across files, compose them with Hono and export the composed type:
import { Hono } from 'hono';
const router = new Hono()
.get('/posts', async (c) => {
return c.json({ posts: [] });
})
.get('/posts/:id', async (c) => {
return c.json({ id: c.req.param('id'), title: '', body: '' });
});
export type PostsRoute = typeof router;
export default router;import { Hono } from 'hono';
import posts from './posts';
import users from './users';
const app = new Hono()
.route('/', users)
.route('/', posts);
export type AppRoute = typeof app;
export default app;export type { AppRoute } from '../api';import { hc } from 'hono/client';
import type { AppRoute } from '../shared/api-types';
export const client = hc<AppRoute>('/api');If you mount a router at .route('/users', users), define the users router with relative paths like / and /:id. Otherwise Hono composes doubled paths such as /users/users.
With Agentuity Services
Hono RPC works with ordinary Hono route handlers. If the route needs Agentuity service clients, install @agentuity/hono and add the middleware before the route handlers that read c.var.*.
npm install @agentuity/honoimport { Hono } from 'hono';
import { agentuity } from '@agentuity/hono';
import type { Services } from '@agentuity/hono';
type Variables = Pick<Services, 'kv'>;
const app = new Hono<{ Variables: Variables }>();
app.use('*', agentuity());
app.get('/sessions/:id', async (c) => {
const result = await c.var.kv.get('sessions', c.req.param('id'));
return c.json(result.exists ? result.data : null);
});
export type AppRoute = typeof app;
export default app;Tips
- chain route methods on the same
Honoinstance when you want RPC inference - import route types with
import typeor a type-only barrel - keep the
hc()base URL aligned with your framework proxy or route mount - check
res.okbefore callingres.json()in query functions - use validators such as
zValidator()when the client should know the request body shape