# Hono RPC Pattern Deep Dive Complete guide to type-safe client/server communication using Hono's RPC feature. **Last Updated**: 2025-10-22 --- ## What is Hono RPC? Hono RPC allows you to create **fully type-safe client/server communication** without manually defining API types. The client automatically infers types directly from server routes. **Key Benefits**: - ✅ Full type inference (request + response) - ✅ No manual type definitions - ✅ Compile-time error checking - ✅ Auto-complete in IDE - ✅ Refactoring safety --- ## Basic Setup ### Server ```typescript // server.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() const userSchema = z.object({ name: z.string(), email: z.string().email(), }) // CRITICAL: Use const route = app.get(...) pattern const route = app.post( '/users', zValidator('json', userSchema), (c) => { const data = c.req.valid('json') return c.json({ success: true, user: { id: '1', ...data } }, 201) } ) // Export type for RPC client export type AppType = typeof route export default app ``` ### Client ```typescript // client.ts import { hc } from 'hono/client' import type { AppType } from './server' const client = hc('http://localhost:8787') // Type-safe API call const res = await client.users.$post({ json: { name: 'Alice', email: 'alice@example.com', }, }) const data = await res.json() // Type: { success: boolean, user: { id: string, name: string, email: string } } ``` --- ## Export Patterns ### Pattern 1: Export Single Route ```typescript const route = app.get('/users', handler) export type AppType = typeof route ``` **When to use**: Single route, simple API ### Pattern 2: Export Multiple Routes ```typescript const getUsers = app.get('/users', handler) const createUser = app.post('/users', handler) const getUser = app.get('/users/:id', handler) export type AppType = typeof getUsers | typeof createUser | typeof getUser ``` **When to use**: Multiple routes, moderate complexity ### Pattern 3: Export Sub-apps ```typescript const usersApp = new Hono() usersApp.get('/', handler) usersApp.post('/', handler) export type UsersType = typeof usersApp ``` **When to use**: Organized route groups, large APIs ### Pattern 4: Export Entire App ```typescript const app = new Hono() // ... many routes ... export type AppType = typeof app ``` **When to use**: Small apps only (performance issue with large apps) --- ## Performance Optimization ### Problem: Slow Type Inference With many routes, exporting `typeof app` causes slow IDE performance due to complex type instantiation. ### Solution: Export Specific Route Groups ```typescript // ❌ Slow: 100+ routes export type AppType = typeof app // ✅ Fast: Export by domain const userRoutes = app.basePath('/users').get('/', ...).post('/', ...) const postRoutes = app.basePath('/posts').get('/', ...).post('/', ...) export type UserRoutes = typeof userRoutes export type PostRoutes = typeof postRoutes // Client uses specific routes const userClient = hc('http://localhost:8787/users') const postClient = hc('http://localhost:8787/posts') ``` --- ## Client Usage Patterns ### Basic GET Request ```typescript const res = await client.users.$get() const data = await res.json() ``` ### POST with JSON Body ```typescript const res = await client.users.$post({ json: { name: 'Alice', email: 'alice@example.com', }, }) const data = await res.json() ``` ### Route Parameters ```typescript const res = await client.users[':id'].$get({ param: { id: '123' }, }) const data = await res.json() ``` ### Query Parameters ```typescript const res = await client.search.$get({ query: { q: 'hello', page: '2', }, }) const data = await res.json() ``` ### Custom Headers ```typescript const res = await client.users.$get({}, { headers: { Authorization: 'Bearer token', }, }) ``` ### Fetch Options ```typescript const res = await client.users.$get({}, { signal: AbortSignal.timeout(5000), // 5 second timeout cache: 'no-cache', }) ``` --- ## Error Handling ### Basic Error Handling ```typescript const res = await client.users.$post({ json: { name: 'Alice', email: 'alice@example.com' }, }) if (!res.ok) { console.error('Request failed:', res.status) return } const data = await res.json() ``` ### Typed Error Responses ```typescript const res = await client.users.$post({ json: { name: '', email: 'invalid' }, }) if (res.status === 400) { const error = await res.json() // Typed as error response console.error('Validation error:', error) return } if (res.status === 500) { const error = await res.json() console.error('Server error:', error) return } const data = await res.json() // Typed as success response ``` ### Try-Catch ```typescript try { const res = await client.users.$get() if (!res.ok) { throw new Error(`HTTP ${res.status}: ${res.statusText}`) } const data = await res.json() console.log('Users:', data) } catch (error) { console.error('Network error:', error) } ``` --- ## Authentication ### Bearer Token ```typescript const client = hc('http://localhost:8787', { headers: { Authorization: 'Bearer your-token-here', }, }) const res = await client.protected.$get() ``` ### Dynamic Headers ```typescript async function makeAuthenticatedRequest() { const token = await getAuthToken() const res = await client.protected.$get({}, { headers: { Authorization: `Bearer ${token}`, }, }) return res.json() } ``` --- ## React Integration ### Basic Hook ```typescript import { useState, useEffect } from 'react' import { hc } from 'hono/client' import type { AppType } from './server' const client = hc('http://localhost:8787') function useUsers() { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { async function fetchUsers() { setLoading(true) setError(null) try { const res = await client.users.$get() if (!res.ok) { throw new Error('Failed to fetch users') } const data = await res.json() setUsers(data.users) } catch (err) { setError(err as Error) } finally { setLoading(false) } } fetchUsers() }, []) return { users, loading, error } } ``` ### With TanStack Query ```typescript import { useQuery } from '@tanstack/react-query' import { hc } from 'hono/client' import type { AppType } from './server' const client = hc('http://localhost:8787') function useUsers() { return useQuery({ queryKey: ['users'], queryFn: async () => { const res = await client.users.$get() if (!res.ok) { throw new Error('Failed to fetch users') } return res.json() }, }) } function UsersComponent() { const { data, isLoading, error } = useUsers() if (isLoading) return
Loading...
if (error) return
Error: {error.message}
return (
    {data?.users.map((user) => (
  • {user.name}
  • ))}
) } ``` --- ## Advanced Patterns ### Middleware Responses Server: ```typescript const authMiddleware = async (c, next) => { const token = c.req.header('Authorization') if (!token) { return c.json({ error: 'Unauthorized' }, 401) } await next() } const route = app.get('/protected', authMiddleware, (c) => { return c.json({ data: 'Protected data' }) }) export type ProtectedType = typeof route ``` Client: ```typescript const res = await client.protected.$get() // Response type includes both middleware (401) and handler (200) responses if (res.status === 401) { const error = await res.json() // Type: { error: string } console.error('Unauthorized:', error) return } const data = await res.json() // Type: { data: string } ``` ### Multiple Sub-apps Server: ```typescript const usersApp = new Hono() usersApp.get('/', handler) usersApp.post('/', handler) const postsApp = new Hono() postsApp.get('/', handler) postsApp.post('/', handler) const app = new Hono() app.route('/users', usersApp) app.route('/posts', postsApp) export type UsersType = typeof usersApp export type PostsType = typeof postsApp ``` Client: ```typescript const userClient = hc('http://localhost:8787/users') const postClient = hc('http://localhost:8787/posts') const users = await userClient.index.$get() const posts = await postClient.index.$get() ``` --- ## TypeScript Tips ### Type Inference ```typescript // Infer request type type UserRequest = Parameters[0]['json'] // Type: { name: string, email: string } // Infer response type type UserResponse = Awaited> type UserData = Awaited> // Type: { success: boolean, user: { id: string, name: string, email: string } } ``` ### Generic Client Functions ```typescript async function fetchFromAPI( endpoint: T, options?: Parameters[0] ) { const res = await endpoint.$get(options) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } return res.json() } // Usage const users = await fetchFromAPI(client.users) ``` --- ## Performance Best Practices 1. **Export specific routes** for large APIs 2. **Use route groups** for better organization 3. **Batch requests** when possible 4. **Cache client instance** (don't recreate on every request) 5. **Use AbortController** for request cancellation --- ## Common Pitfalls ### ❌ Don't: Anonymous Routes ```typescript app.get('/users', (c) => c.json({ users: [] })) export type AppType = typeof app // Won't infer route properly ``` ### ✅ Do: Named Routes ```typescript const route = app.get('/users', (c) => c.json({ users: [] })) export type AppType = typeof route ``` ### ❌ Don't: Forget Type Import ```typescript import { AppType } from './server' // Wrong: runtime import ``` ### ✅ Do: Type-Only Import ```typescript import type { AppType } from './server' // Correct: type-only import ``` --- ## Official Documentation - **Hono RPC Guide**: https://hono.dev/docs/guides/rpc - **hc Client API**: https://hono.dev/docs/helpers/hc