Initial commit
This commit is contained in:
542
references/rpc-guide.md
Normal file
542
references/rpc-guide.md
Normal file
@@ -0,0 +1,542 @@
|
||||
# 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<AppType>('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<UserRoutes>('http://localhost:8787/users')
|
||||
const postClient = hc<PostRoutes>('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<AppType>('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<AppType>('http://localhost:8787')
|
||||
|
||||
function useUsers() {
|
||||
const [users, setUsers] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(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<AppType>('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 <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data?.users.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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<UsersType>('http://localhost:8787/users')
|
||||
const postClient = hc<PostsType>('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<typeof client.users.$post>[0]['json']
|
||||
// Type: { name: string, email: string }
|
||||
|
||||
// Infer response type
|
||||
type UserResponse = Awaited<ReturnType<typeof client.users.$post>>
|
||||
type UserData = Awaited<ReturnType<UserResponse['json']>>
|
||||
// Type: { success: boolean, user: { id: string, name: string, email: string } }
|
||||
```
|
||||
|
||||
### Generic Client Functions
|
||||
|
||||
```typescript
|
||||
async function fetchFromAPI<T extends typeof client[keyof typeof client]>(
|
||||
endpoint: T,
|
||||
options?: Parameters<T['$get']>[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
|
||||
Reference in New Issue
Block a user