hono-rpc

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hono RPC - Type-Safe Client

Hono RPC - 类型安全客户端

Overview

概述

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:
  • Zero-codegen type-safe client
  • Automatic TypeScript inference
  • Works with Zod validators
  • Status code-aware response types
  • Supports path params, query, headers
Hono RPC 可通过 TypeScript 的类型系统在服务器与客户端之间共享 API 规范。导出服务器的类型后,客户端会自动知晓所有路由、请求结构和响应类型——无需代码生成。
核心特性:
  • 零代码生成的类型安全客户端
  • 自动 TypeScript 类型推断
  • 支持 Zod 验证器
  • 感知状态码的响应类型
  • 支持路径参数、查询参数和请求头

When to Use This Skill

适用场景

Use Hono RPC when:
  • Building full-stack TypeScript applications
  • Need type-safe API consumption without OpenAPI/codegen
  • Want compile-time validation of API calls
  • Sharing types between client and server in monorepos
在以下场景中使用 Hono RPC:
  • 构建全栈 TypeScript 应用
  • 无需 OpenAPI/代码生成即可实现类型安全的 API 调用
  • 希望在编译阶段验证 API 调用
  • 在单体仓库中实现客户端与服务器的类型共享

Basic Setup

基础配置

Server Side

服务器端

typescript
// 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
typescript
// 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

Client Side

客户端

typescript
// 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 }
}
typescript
// 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 }
}

Route Chaining for Type Export

路由链式调用以实现类型导出

Important: Chain routes for proper type inference:
typescript
// 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!
重要提示: 需通过链式调用路由以确保正确的类型推断:
typescript
// 正确方式: 链式调用所有路由
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  // 缺少路由信息!

Request Patterns

请求模式

Path Parameters

路径参数

typescript
// 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'
  }
})
typescript
// 服务器端
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'
  }
})

Query Parameters

查询参数

typescript
// 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
  }
})
typescript
// 服务器端
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
  }
})

JSON Body

JSON 请求体

typescript
// 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']
  }
})
typescript
// 服务器端
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']
  }
})

Form Data

表单数据

typescript
// 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
})
typescript
// 服务器端
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
})

Headers

请求头

typescript
// 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'
  }
})
typescript
// 服务器端
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'
  }
})

Response Type Inference

响应类型推断

Status Code-Aware Types

感知状态码的类型

typescript
// 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 }
}
typescript
// 服务器端
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 }
}

Request Type Inference

请求类型推断

typescript
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: ''
})
typescript
import { InferRequestType } from 'hono/client'

type CreateUserRequest = InferRequestType<typeof client.users.$post>['json']
// { name: string; email: string }

// 用于表单验证、状态管理等场景
const [formData, setFormData] = useState<CreateUserRequest>({
  name: '',
  email: ''
})

Multi-File Route Organization

多文件路由组织

Organize Routes

路由组织

typescript
// 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
typescript
// 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

Client Usage

客户端使用

typescript
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
typescript
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

Error Handling

错误处理

Handle Fetch Errors

捕获 Fetch 错误

typescript
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
  }
}
typescript
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
  }
}

Type-Safe Error Responses

类型安全的错误响应

typescript
// 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)
}
typescript
// 服务器端
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)
}

Configuration Options

配置选项

Custom Fetch

自定义 Fetch

typescript
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)
  }
})
typescript
const client = hc<AppType>('http://localhost:3000', {
  // 自定义 fetch(用于测试、日志等场景)
  fetch: async (input, init) => {
    console.log('Fetching:', input)
    return fetch(input, init)
  }
})

Default Headers

默认请求头

typescript
const client = hc<AppType>('http://localhost:3000', {
  headers: {
    'Authorization': 'Bearer token',
    'X-Custom-Header': 'value'
  }
})
typescript
const client = hc<AppType>('http://localhost:3000', {
  headers: {
    'Authorization': 'Bearer token',
    'X-Custom-Header': 'value'
  }
})

Dynamic Headers

动态请求头

typescript
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}` } : {}
  }
})
typescript
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}` } : {}
  }
})

Best Practices

最佳实践

1. Enable Strict Mode

1. 启用严格模式

json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // Required for proper type inference!
  }
}
json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true  // 正确类型推断的必要条件!
  }
}

2. Use Explicit Status Codes

2. 使用显式状态码

typescript
// 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
typescript
// 正确方式: 显式状态码可实现类型区分
return c.json({ data }, 200)
return c.json({ error: 'Not found' }, 404)

// 避免使用: c.notFound() 与 RPC 兼容性不佳
return c.notFound()  // 响应类型无法正确推断

3. Split Large Apps

3. 拆分大型应用

typescript
// 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
typescript
// 对于大型应用,拆分路由以减少 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

4. Consistent Response Shapes

4. 统一响应格式

typescript
// 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)
})
typescript
// 定义标准响应包装器
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)
})

Quick Reference

快速参考

Client Methods

客户端方法

HTTP MethodClient Method
GET
client.path.$get()
POST
client.path.$post()
PUT
client.path.$put()
DELETE
client.path.$delete()
PATCH
client.path.$patch()
HTTP 方法客户端方法
GET
client.path.$get()
POST
client.path.$post()
PUT
client.path.$put()
DELETE
client.path.$delete()
PATCH
client.path.$patch()

Request Options

请求选项

typescript
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
})
typescript
client.path.$method({
  param: { id: '1' },           // 路径参数
  query: { page: 1 },           // 查询参数
  json: { name: 'Alice' },      // JSON 请求体
  form: formData,               // 表单数据
  header: { 'X-Custom': 'v' }   // 请求头
})

Type Utilities

类型工具

typescript
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>
typescript
import { InferRequestType, InferResponseType } from 'hono/client'

// 提取请求类型
type ReqType = InferRequestType<typeof client.users.$post>

// 按状态码提取响应类型
type Res200 = InferResponseType<typeof client.users.$get, 200>

Related Skills

相关技能

  • hono-core - Framework fundamentals
  • hono-validation - Request validation
  • typescript-core - TypeScript patterns

Version: Hono 4.x Last Updated: January 2025 License: MIT
  • hono-core - 框架基础
  • hono-validation - 请求验证
  • typescript-core - TypeScript 模式

版本: Hono 4.x 最后更新: 2025年1月 许可证: MIT