Claude
Skills
Sign in
Back

hono-rpc

Included with Lifetime
$97 forever

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

Backend & APIshonorpctype-safetytypescriptapi-clientfull-stack

What this skill does


# 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.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 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: '[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 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!
```

## 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'
  }
})
```

### 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
  }
})
```

### JSON Body

```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']
  }
})
```

### 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
})
```

### 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'
  }
})
```

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

### 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.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
```

## 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
// 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 ApiRespons

Related in Backend & APIs