关于 hono-rpc
hono-rpc 是用于在 TypeScript 服务端和客户端之间共享 Hono API 路由类型的技能。它无需代码生成即可推断请求和响应类型,并支持按状态码区分响应。
主要功能
- 01无需代码生成的类型安全客户端
- 02自动推断 TypeScript 类型
- 03支持 Zod 校验器
- 04按状态码区分响应类型
- 05支持路径、查询和标头参数
适用场景
- 01从 TypeScript 客户端调用 Hono API
- 02在 monorepo 中共享服务端与客户端类型
- 03在编译时检查 API 调用
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill hono-rpc安装后在 Claude Code 中输入/hono-rpc确认已加载。
附带文件metadata.json
详细介绍
Hono RPC - 类型安全客户端
概述
Hono RPC 通过 TypeScript 的类型系统,在服务端和客户端之间共享 API 规范。导出服务端的类型后,客户端便会自动识别所有路由、请求结构和响应类型,无需代码生成。
主要特性:
- 无需代码生成的类型安全客户端
- 自动 TypeScript 类型推断
- 支持 Zod 验证器
- 感知状态码的响应类型
- 支持路径参数、查询参数和请求头
何时使用此技能
在以下情况下使用 Hono RPC:
- 构建全栈 TypeScript 应用
- 需要在不使用 OpenAPI/代码生成的情况下安全地使用 API
- 希望在编译时验证 API 调用
- 在 monorepo 中共享客户端与服务端的类型
基本设置
服务端
// server/index.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// Define routes with validation
const route = app
.get('/users', async (c) => {
const users = [{ id: '1', name: 'Alice' }]
return c.json({ users })
})
.post(
'/users',
zValidator('json', z.object({
name: z.string(),
email: z.string().email()
})),
async (c) => {
const data = c.req.valid('json')
return c.json({ id: '1', ...data }, 201)
}
)
.get('/users/:id', async (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'Alice' })
})
// Export type for client
export type AppType = typeof route
export default app
客户端
// client/api.ts
import { hc } from 'hono/client'
import type { AppType } from '../server'
// Create typed client
const client = hc<AppType>('http://localhost:3000')
// All methods are type-safe!
async function examples() {
// GET /users
const usersRes = await client.users.$get()
const { users } = await usersRes.json()
// users: { id: string; name: string }[]
// POST /users - body is typed
const createRes = await client.users.$post({
json: {
name: 'Bob',
email: '[email protected]'
}
})
const created = await createRes.json()
// created: { id: string; name: string; email: string }
// GET /users/:id - params are typed
const userRes = await client.users[':id'].$get({
param: { id: '123' }
})
const user = await userRes.json()
// user: { id: string; name: string }
}
用于导出类型的路由链式调用
重要提示:使用链式路由,才能正确推断类型:
// CORRECT: Chain all routes
const route = app
.get('/a', handlerA)
.post('/b', handlerB)
.get('/c', handlerC)
export type AppType = typeof route
// WRONG: Separate statements lose type info
app.get('/a', handlerA)
app.post('/b', handlerB) // Types lost!
export type AppType = typeof app // Missing routes!
请求模式
路径参数
// Server
const route = app.get('/posts/:postId/comments/:commentId', async (c) => {
const { postId, commentId } = c.req.param()
return c.json({ postId, commentId })
})
// Client
const res = await client.posts[':postId'].comments[':commentId'].$get({
param: {
postId: '1',
commentId: '42'
}
})
查询参数
// Server
const route = app.get(
'/search',
zValidator('query', z.object({
q: z.string(),
page: z.coerce.number().optional(),
limit: z.coerce.number().optional()
})),
async (c) => {
const { q, page, limit } = c.req.valid('query')
return c.json({ query: q, page, limit })
}
)
// Client
const res = await client.search.$get({
query: {
q: 'typescript',
page: 1,
limit: 20
}
})
JSON 请求体
// Server
const route = app.post(
'/posts',
zValidator('json', z.object({
title: z.string(),
content: z.string(),
tags: z.array(z.string()).optional()
})),
async (c) => {
const data = c.req.valid('json')
return c.json({ id: '1', ...data }, 201)
}
)
// Client
const res = await client.posts.$post({
json: {
title: 'Hello World',
content: 'My first post',
tags: ['typescript', 'hono']
}
})
表单数据
// Server
const route = app.post(
'/upload',
zValidator('form', z.object({
file: z.instanceof(File),
description: z.string().optional()
})),
async (c) => {
const { file, description } = c.req.valid('form')
return c.json({ filename: file.name })
}
)
// Client
const formData = new FormData()
formData.append('file', file)
formData.append('description', 'My file')
const res = await client.upload.$post({
form: formData
})
请求头
// Server
const route = app.get(
'/protected',
zValidator('header', z.object({
authorization: z.string()
})),
async (c) => {
return c.json({ authenticated: true })
}
)
// Client
const res = await client.protected.$get({
header: {
authorization: 'Bearer token123'
}
})
响应类型推断
感知状态码的类型
// Server
const route = app.get('/user', async (c) => {
const user = await getUser()
if (!user) {
return c.json({ error: 'Not found' }, 404)
}
return c.json({ id: user.id, name: user.name }, 200)
})
// Client - use InferResponseType
import { InferResponseType } from 'hono/client'
type SuccessResponse = InferResponseType<typeof client.user.$get, 200>
// { id: string; name: string }
type ErrorResponse = InferResponseType<typeof client.user.$get, 404>
// { error: string }
// Handle different status codes
const res = await client.user.$get()
if (res.status === 200) {
const data = await res.json()
// data: { id: string; name: string }
} else if (res.status === 404) {
const error = await res.json()
// error: { error: string }
}
请求类型推断
import { InferRequestType } from 'hono/client'
type CreateUserRequest = InferRequestType<typeof client.users.$post>['json']
// { name: string; email: string }
// Use for form validation, state management, etc.
const [formData, setFormData] = useState<CreateUserRequest>({
name: '',
email: ''
})
多文件路由组织
组织路由
// server/routes/users.ts
import { Hono } from 'hono'
export const users = new Hono()
.get('/', async (c) => c.json({ users: [] }))
.post('/', async (c) => c.json({ created: true }, 201))
.get('/:id', async (c) => c.json({ id: c.req.param('id') }))
// server/routes/posts.ts
export const posts = new Hono()
.get('/', async (c) => c.json({ posts: [] }))
.post('/', async (c) => c.json({ created: true }, 201))
// server/index.ts
import { Hono } from 'hono'
import { users } from './routes/users'
import { posts } from './routes/posts'
const app = new Hono()
const route = app
.route('/users', users)
.route('/posts', posts)
export type AppType = typeof route
export default app
客户端用法
import { hc } from 'hono/client'
import type { AppType } from '../server'
const client = hc<AppType>('http://localhost:3000')
// Routes are nested
await client.users.$get() // GET /users
await client.users[':id'].$get() // GET /users/:id
await client.posts.$get() // GET /posts
错误处理
处理 Fetch 错误
async function fetchUser(id: string) {
try {
const res = await client.users[':id'].$get({
param: { id }
})
if (!res.ok) {
const error = await res.json()
throw new Error(error.message || 'Failed to fetch user')
}
return await res.json()
} catch (error) {
if (error instanceof TypeError) {
// Network error
throw new Error('Network error')
}
throw error
}
}
类型安全的错误响应
// Server
const route = app.get('/resource', async (c) => {
try {
const data = await fetchData()
return c.json({ success: true, data })
} catch (e) {
return c.json({ success: false, error: 'Failed' }, 500)
}
})
// Client
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string }
const res = await client.resource.$get()
const result: ApiResponse<DataType> = await res.json()
if (result.success) {
console.log(result.data) // Typed!
} else {
console.error(result.error)
}
配置选项
自定义 Fetch
const client = hc<AppType>('http://localhost:3000', {
// Custom fetch (for testing, logging, etc.)
fetch: async (input, init) => {
console.log('Fetching:', input)
return fetch(input, init)
}
})
默认请求头
const client = hc<AppType>('http://localhost:3000', {
headers: {
'Authorization': 'Bearer token',
'X-Custom-Header': 'value'
}
})
动态请求头
const getClient = (token: string) =>
hc<AppType>('http://localhost:3000', {
headers: () => ({
'Authorization': `Bearer ${token}`
})
})
// Or with a function that returns headers
const client = hc<AppType>('http://localhost:3000', {
headers: () => {
const token = getAuthToken()
return token ? { 'Authorization': `Bearer ${token}` } : {}
}
})
最佳实践
1. 启用严格模式
// tsconfig.json
{
"compilerOptions": {
"strict": true // Required for proper type inference!
}
}
2. 使用显式状态码
// CORRECT: Explicit status enables type discrimination
return c.json({ data }, 200)
return c.json({ error: 'Not found' }, 404)
// AVOID: c.notFound() doesn't work well with RPC
return c.notFound() // Response type is not properly inferred
3. 拆分大型应用
// For large apps, split routes to reduce IDE overhead
const v1 = new Hono()
.route('/users', usersRoute)
.route('/posts', postsRoute)
const v2 = new Hono()
.route('/users', usersV2Route)
// Export separate types
export type V1Type = typeof v1
export type V2Type = typeof v2
4. 保持响应结构一致
// Define standard response wrapper
type ApiSuccess<T> = { ok: true; data: T }
type ApiError = { ok: false; error: string; code?: string }
type ApiResponse<T> = ApiSuccess<T> | ApiError
// Use consistently
const route = app.get('/users/:id', async (c) => {
const user = await findUser(c.req.param('id'))
if (!user) {
return c.json({ ok: false, error: 'User not found' } as ApiError, 404)
}
return c.json({ ok: true, data: user } as ApiSuccess<User>, 200)
})
快速参考
客户端方法
| HTTP 方法 | 客户端方法 |
|---|---|
| GET | client.path.$get() |
| POST | client.path.$post() |
| PUT | client.path.$put() |
| DELETE | client.path.$delete() |
| PATCH | client.path.$patch() |
请求选项
client.path.$method({
param: { id: '1' }, // Path parameters
query: { page: 1 }, // Query parameters
json: { name: 'Alice' }, // JSON body
form: formData, // Form data
header: { 'X-Custom': 'v' } // Headers
})
类型工具
import { InferRequestType, InferResponseType } from 'hono/client'
// Extract request type
type ReqType = InferRequestType<typeof client.users.$post>
// Extract response type by status
type Res200 = InferResponseType<typeof client.users.$get, 200>
type Res404 = InferResponseType<typeof client.users.$get, 404>
相关技能
- hono-core - 框架基础知识
- hono-validation - 请求验证
- typescript-core - TypeScript 模式
版本:Hono 4.x 最后更新:2025 年 1 月 许可证:MIT
评论
支持 Markdown