| name | rpc-typesafe |
| description | This skill activates when setting up Hono RPC client, configuring type-safe API calls, or discussing client-server type sharing. It provides patterns for the hc client, type inference, and React Query integration. |
Hono RPC Type Safety
Patterns for type-safe client-server communication with Hono.
Server Setup
Export App Type
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const routes = app
.get('/users', async (c) => {
return c.json({ users: [{ id: '1', name: 'John' }] })
})
.post('/users',
zValidator('json', z.object({
email: z.string().email(),
name: z.string()
})),
async (c) => {
const data = c.req.valid('json')
return c.json({ id: crypto.randomUUID(), ...data }, 201)
}
)
.get('/users/:id', async (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'John' })
})
export default routes
export type AppType = typeof routes
Route Chaining for Type Inference
const userRoutes = new Hono()
.get('/', async (c) => c.json({ users: [] }))
.post('/',
zValidator('json', createUserSchema),
async (c) => c.json({ id: '1' }, 201)
)
const postRoutes = new Hono()
.get('/', async (c) => c.json({ posts: [] }))
.post('/',
zValidator('json', createPostSchema),
async (c) => c.json({ id: '1' }, 201)
)
const app = new Hono()
.route('/users', userRoutes)
.route('/posts', postRoutes)
export type AppType = typeof app
Client Setup
Basic Client
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
const res = await client.users.$get()
const data = await res.json()
Client Factory
import { hc } from 'hono/client'
import type { AppType } from '@/server'
export function createClient(baseUrl: string, token?: string) {
return hc<AppType>(baseUrl, {
headers: token ? { Authorization: `Bearer ${token}` } : undefined
})
}
export const api = createClient(
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8787'
)
Request Patterns
GET Requests
const res = await api.users.$get()
const { users } = await res.json()
const res = await api.users.$get({
query: {
page: 1,
limit: 20,
search: 'john'
}
})
const res = await api.users[':id'].$get({
param: { id: 'user-123' }
})
POST Requests
const res = await api.users.$post({
json: {
email: 'user@example.com',
name: 'New User'
}
})
if (res.ok) {
const user = await res.json()
console.log(user.id)
}
PUT/PATCH Requests
const res = await api.users[':id'].$put({
param: { id: 'user-123' },
json: {
name: 'Updated Name'
}
})
DELETE Requests
const res = await api.users[':id'].$delete({
param: { id: 'user-123' }
})
if (res.status === 204) {
console.log('Deleted successfully')
}
Type Utilities
InferRequestType / InferResponseType
import { hc, InferRequestType, InferResponseType } from 'hono/client'
import type { AppType } from './server'
type CreateUserRequest = InferRequestType<typeof api.users.$post>['json']
type UserResponse = InferResponseType<typeof api.users[':id'].$get>
async function createUser(data: CreateUserRequest): Promise<UserResponse> {
const res = await api.users.$post({ json: data })
return res.json()
}
URL Generation
const url = api.users[':id'].$url({
param: { id: 'user-123' }
})
Error Handling
Response Status Checking
async function getUser(id: string) {
const res = await api.users[':id'].$get({ param: { id } })
if (!res.ok) {
if (res.status === 404) {
throw new Error('User not found')
}
throw new Error(`API error: ${res.status}`)
}
return res.json()
}
Typed Error Responses
app.get('/users/:id', async (c) => {
const user = await getUser(c.req.param('id'))
if (!user) {
return c.json({ error: 'User not found' }, 404)
}
return c.json(user)
})
const res = await api.users[':id'].$get({ param: { id } })
if (res.status === 404) {
const { error } = await res.json()
console.log(error)
}
React Query Integration
Query Hooks
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
export function useUsers(page = 1) {
return useQuery({
queryKey: ['users', page],
queryFn: async () => {
const res = await api.users.$get({ query: { page } })
if (!res.ok) throw new Error('Failed to fetch users')
return res.json()
}
})
}
export function useUser(id: string) {
return useQuery({
queryKey: ['users', id],
queryFn: async () => {
const res = await api.users[':id'].$get({ param: { id } })
if (!res.ok) throw new Error('User not found')
return res.json()
},
enabled: !!id
})
}
Mutation Hooks
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data: { email: string; name: string }) => {
const res = await api.users.$post({ json: data })
if (!res.ok) throw new Error('Failed to create user')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: { name: string } }) => {
const res = await api.users[':id'].$put({ param: { id }, json: data })
if (!res.ok) throw new Error('Failed to update user')
return res.json()
},
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['users', id] })
}
})
}
Component Usage
function UserList() {
const { data, isLoading, error } = useUsers()
const createUser = useCreateUser()
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
{data?.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button
onClick={() => createUser.mutate({
email: 'new@example.com',
name: 'New User'
})}
>
Add User
</button>
</div>
)
}
Configuration Requirements
TypeScript Config
Both server and client need strict mode:
{
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler"
}
}
Status Codes for Type Discrimination
app.post('/users', async (c) => {
return c.json({ id: '1', name: 'User' }, 201)
})
app.get('/users/:id', async (c) => {
const user = await getUser(id)
if (!user) {
return c.json({ error: 'Not found' }, 404)
}
return c.json(user, 200)
})
Best Practices
- Always export
AppType from server
- Chain routes for proper type inference
- Use
zValidator for automatic request type inference
- Specify status codes for response type discrimination
- Use
strict: true in both server and client tsconfig
- Create typed error handlers for consistent error responses
- Wrap in React Query for caching and state management
- Use
InferRequestType/InferResponseType for complex types