Hono Rpc

bobmatnyc/claude-mpm-skills/toolchains/javascript/frameworks/hono/hono-rpc

作者 bobmatnyc718070a7d622無授權條款77 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫2 個月前更新

Hono RPC - end-to-end type-safe API client generation with hc client and TypeScript inference

AI 產生的概覽

指導使用 hc 用戶端與 TypeScript 型別推斷,打造端到端型別安全的 Hono API 用戶端。

功能
本技能說明如何將 Hono 伺服器的路由型別分享給用戶端,讓 API 呼叫在編譯期接受型別檢查,且不需要程式碼產生。內容涵蓋匯出應用程式型別、以鏈式方式定義路由,以及使用 hc 用戶端處理路徑參數、查詢字串、JSON 請求主體、表單資料與請求標頭。此外也說明回應與請求型別工具、依狀態碼推斷型別、錯誤處理及用戶端設定選項。
適用情境
適用於以 Hono 建構全端 TypeScript 應用程式,且用戶端需要型別安全 API 存取的情況。也適合希望在不用 OpenAPI 或程式碼產生的前提下,對 API 呼叫進行編譯期驗證的 monorepo 或專案。
執行需求
需要啟用嚴格模式的 TypeScript 專案、Hono 4.x 以及 hono/client 模組;範例另使用 Zod 與 @hono/zod-validator。本技能不含指令碼,僅為說明文件。

Hono RPC - Type-Safe Client

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

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

Basic Setup

Server Side

typescript
// server/index.tsimport { Hono } from 'hono'import { zValidator } from '@hono/zod-validator'import { z } from 'zod'
const app = new Hono()
// Define routes with validationconst 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 clientexport type AppType = typeof route
export default app

Client Side

typescript
// client/api.tsimport { hc } from 'hono/client'import type { AppType } from '../server'
// Create typed clientconst 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 }}

Route Chaining for Type Export

Important: Chain routes for proper type inference:

typescript
// CORRECT: Chain all routesconst route = app  .get('/a', handlerA)  .post('/b', handlerB)  .get('/c', handlerC)
export type AppType = typeof route
// WRONG: Separate statements lose type infoapp.get('/a', handlerA)app.post('/b', handlerB)  // Types lost!
export type AppType = typeof app  // Missing routes!

Request Patterns

Path Parameters

typescript
// Serverconst route = app.get('/posts/:postId/comments/:commentId', async (c) => {  const { postId, commentId } = c.req.param()  return c.json({ postId, commentId })})
// Clientconst res = await client.posts[':postId'].comments[':commentId'].$get({  param: {    postId: '1',    commentId: '42'  }})

Query Parameters

typescript
// Serverconst 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 })  })
// Clientconst res = await client.search.$get({  query: {    q: 'typescript',    page: 1,    limit: 20  }})

JSON Body

typescript
// Serverconst 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)  })
// Clientconst res = await client.posts.$post({  json: {    title: 'Hello World',    content: 'My first post',    tags: ['typescript', 'hono']  }})

Form Data

typescript
// Serverconst 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 })  })
// Clientconst formData = new FormData()formData.append('file', file)formData.append('description', 'My file')
const res = await client.upload.$post({  form: formData})

Headers

typescript
// Serverconst route = app.get(  '/protected',  zValidator('header', z.object({    authorization: z.string()  })),  async (c) => {    return c.json({ authenticated: true })  })
// Clientconst res = await client.protected.$get({  header: {    authorization: 'Bearer token123'  }})

Response Type Inference

Status Code-Aware Types

typescript
// Serverconst 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 InferResponseTypeimport { 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 codesconst 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: ''})

Multi-File Route Organization

Organize Routes

typescript
// server/routes/users.tsimport { 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.tsexport const posts = new Hono()  .get('/', async (c) => c.json({ posts: [] }))  .post('/', async (c) => c.json({ created: true }, 201))
// server/index.tsimport { 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 routeexport default app

Client Usage

typescript
import { hc } from 'hono/client'import type { AppType } from '../server'
const client = hc<AppType>('http://localhost:3000')
// Routes are nestedawait client.users.$get()         // GET /usersawait client.users[':id'].$get()  // GET /users/:idawait client.posts.$get()         // GET /posts

Error Handling

Handle Fetch Errors

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

Type-Safe Error Responses

typescript
// Serverconst 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)  }})
// Clienttype 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)}

Configuration Options

Custom 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)  }})

Default Headers

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 headersconst client = hc<AppType>('http://localhost:3000', {  headers: () => {    const token = getAuthToken()    return token ? { 'Authorization': `Bearer ${token}` } : {}  }})

Best Practices

1. Enable Strict Mode

json
// tsconfig.json{  "compilerOptions": {    "strict": true  // Required for proper type inference!  }}

2. Use Explicit Status Codes

typescript
// CORRECT: Explicit status enables type discriminationreturn c.json({ data }, 200)return c.json({ error: 'Not found' }, 404)
// AVOID: c.notFound() doesn't work well with RPCreturn c.notFound()  // Response type is not properly inferred

3. Split Large Apps

typescript
// For large apps, split routes to reduce IDE overheadconst v1 = new Hono()  .route('/users', usersRoute)  .route('/posts', postsRoute)
const v2 = new Hono()  .route('/users', usersV2Route)
// Export separate typesexport type V1Type = typeof v1export type V2Type = typeof v2

4. Consistent Response Shapes

typescript
// Define standard response wrappertype ApiSuccess<T> = { ok: true; data: T }type ApiError = { ok: false; error: string; code?: string }type ApiResponse<T> = ApiSuccess<T> | ApiError
// Use consistentlyconst 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
GETclient.path.$get()
POSTclient.path.$post()
PUTclient.path.$put()
DELETEclient.path.$delete()
PATCHclient.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})

Type Utilities

typescript
import { InferRequestType, InferResponseType } from 'hono/client'
// Extract request typetype ReqType = InferRequestType<typeof client.users.$post>
// Extract response type by statustype Res200 = InferResponseType<typeof client.users.$get, 200>type Res404 = InferResponseType<typeof client.users.$get, 404>

Related Skills

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

Version: Hono 4.x Last Updated: January 2025 License: MIT

來源與署名

來源:bobmatnyc/claude-mpm-skills位於toolchains/javascript/frameworks/hono/hono-rpc提交718070a

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架