mcpField
H
hono-rpc

hono-rpc

github.com/bobmatnyc开发78441 次安装15 周前更新
代码仓库

关于 hono-rpc

hono-rpc 是用于在 TypeScript 服务端和客户端之间共享 Hono API 路由类型的技能。它无需代码生成即可推断请求和响应类型,并支持按状态码区分响应。

主要功能

  1. 01无需代码生成的类型安全客户端
  2. 02自动推断 TypeScript 类型
  3. 03支持 Zod 校验器
  4. 04按状态码区分响应类型
  5. 05支持路径、查询和标头参数

适用场景

  1. 01从 TypeScript 客户端调用 Hono API
  2. 02在 monorepo 中共享服务端与客户端类型
  3. 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 方法客户端方法
GETclient.path.$get()
POSTclient.path.$post()
PUTclient.path.$put()
DELETEclient.path.$delete()
PATCHclient.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