hono-rpc by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill hono-rpcHono RPC 通过 TypeScript 的类型系统实现了服务器与客户端之间的 API 规范共享。导出服务器的类型后,客户端自动知晓所有路由、请求结构和响应类型——无需代码生成。
主要特性 :
在以下场景中使用 Hono RPC:
// server/index.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// 定义带验证的路由
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 AppType = typeof route
export default app
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// client/api.ts
import { hc } from 'hono/client'
import type { AppType } from '../server'
// 创建类型化客户端
const client = hc<AppType>('http://localhost:3000')
// 所有方法都是类型安全的!
async function examples() {
// GET /users
const usersRes = await client.users.$get()
const { users } = await usersRes.json()
// users: { id: string; name: string }[]
// POST /users - 请求体已类型化
const createRes = await client.users.$post({
json: {
name: 'Bob',
email: 'bob@example.com'
}
})
const created = await createRes.json()
// created: { id: string; name: string; email: string }
// GET /users/:id - 参数已类型化
const userRes = await client.users[':id'].$get({
param: { id: '123' }
})
const user = await userRes.json()
// user: { id: string; name: string }
}
重要:使用链式调用以获得正确的类型推断:
// 正确:链式调用所有路由
const route = app
.get('/a', handlerA)
.post('/b', handlerB)
.get('/c', handlerC)
export type AppType = typeof route
// 错误:分开的语句会丢失类型信息
app.get('/a', handlerA)
app.post('/b', handlerB) // 类型丢失!
export type AppType = typeof app // 缺少路由!
// 服务器
const route = app.get('/posts/:postId/comments/:commentId', async (c) => {
const { postId, commentId } = c.req.param()
return c.json({ postId, commentId })
})
// 客户端
const res = await client.posts[':postId'].comments[':commentId'].$get({
param: {
postId: '1',
commentId: '42'
}
})
// 服务器
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 })
}
)
// 客户端
const res = await client.search.$get({
query: {
q: 'typescript',
page: 1,
limit: 20
}
})
// 服务器
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)
}
)
// 客户端
const res = await client.posts.$post({
json: {
title: 'Hello World',
content: 'My first post',
tags: ['typescript', 'hono']
}
})
// 服务器
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 })
}
)
// 客户端
const formData = new FormData()
formData.append('file', file)
formData.append('description', 'My file')
const res = await client.upload.$post({
form: formData
})
// 服务器
const route = app.get(
'/protected',
zValidator('header', z.object({
authorization: z.string()
})),
async (c) => {
return c.json({ authenticated: true })
}
)
// 客户端
const res = await client.protected.$get({
header: {
authorization: 'Bearer token123'
}
})
// 服务器
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)
})
// 客户端 - 使用 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 }
// 处理不同的状态码
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 }
// 用于表单验证、状态管理等
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')
// 路由是嵌套的
await client.users.$get() // GET /users
await client.users[':id'].$get() // GET /users/:id
await client.posts.$get() // GET /posts
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) {
// 网络错误
throw new Error('Network error')
}
throw error
}
}
// 服务器
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)
}
})
// 客户端
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) // 已类型化!
} else {
console.error(result.error)
}
const client = hc<AppType>('http://localhost:3000', {
// 自定义 fetch(用于测试、日志记录等)
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}`
})
})
// 或者使用返回请求头的函数
const client = hc<AppType>('http://localhost:3000', {
headers: () => {
const token = getAuthToken()
return token ? { 'Authorization': `Bearer ${token}` } : {}
}
})
// tsconfig.json
{
"compilerOptions": {
"strict": true // 正确的类型推断所必需!
}
}
// 正确:明确的状态码支持类型区分
return c.json({ data }, 200)
return c.json({ error: 'Not found' }, 404)
// 避免:c.notFound() 与 RPC 配合不佳
return c.notFound() // 响应类型无法正确推断
// 对于大型应用,拆分路由以减少 IDE 开销
const v1 = new Hono()
.route('/users', usersRoute)
.route('/posts', postsRoute)
const v2 = new Hono()
.route('/users', usersV2Route)
// 导出独立的类型
export type V1Type = typeof v1
export type V2Type = typeof v2
// 定义标准的响应包装器
type ApiSuccess<T> = { ok: true; data: T }
type ApiError = { ok: false; error: string; code?: string }
type ApiResponse<T> = ApiSuccess<T> | ApiError
// 一致地使用
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' }, // 路径参数
query: { page: 1 }, // 查询参数
json: { name: 'Alice' }, // JSON 请求体
form: formData, // 表单数据
header: { 'X-Custom': 'v' } // 请求头
})
import { InferRequestType, InferResponseType } from 'hono/client'
// 提取请求类型
type ReqType = InferRequestType<typeof client.users.$post>
// 按状态码提取响应类型
type Res200 = InferResponseType<typeof client.users.$get, 200>
type Res404 = InferResponseType<typeof client.users.$get, 404>
版本 : Hono 4.x 最后更新 : 2025年1月 许可证 : MIT
每周安装量
123
代码仓库
GitHub 星标
22
首次出现
2026年1月23日
安全审计
已安装于
gemini-cli99
opencode99
codex96
claude-code89
github-copilot89
cursor84
Hono RPC enables sharing API specifications between server and client through TypeScript's type system. Export your server's type, and the client automatically knows all routes, request shapes, and response types - no code generation required.
Key Features :
Use Hono RPC when:
// 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: 'bob@example.com'
}
})
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 }
}
Important : Chain routes for proper type inference:
// 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
}
})
// 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
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)
}
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}` } : {}
}
})
// tsconfig.json
{
"compilerOptions": {
"strict": true // Required for proper type inference!
}
}
// 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
// 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
// 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 Method | Client Method |
|---|---|
| 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>
Version : Hono 4.x Last Updated : January 2025 License : MIT
Weekly Installs
123
Repository
GitHub Stars
22
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli99
opencode99
codex96
claude-code89
github-copilot89
cursor84
Lark CLI Wiki API 使用指南:获取知识空间节点信息与权限管理
33,000 周安装
PydanticAI 模型集成指南:多提供商AI模型调用、备用模型与流式响应
85 周安装
Pine Script可视化工具:自动分析YouTube交易视频并生成Pine Script实现规范
85 周安装
MongoDB迁移专家:安全架构变更与数据迁移方案设计指南
85 周安装
依赖解析器技能:推送前自动检测修复本地与CI环境不匹配,节省45分钟调试时间
85 周安装
dotnet-install:自动化 .NET SDK 和运行时安装指南 - 支持 Windows/macOS/Linux
85 周安装
UltraThink Orchestrator - 已弃用的AI工作流编排工具,推荐使用dev-orchestrator替代
85 周安装