Skip to main content

End-to-End Type Safety

One of Hono’s most powerful features is its end-to-end type safety. The RPC client uses TypeScript’s type inference to automatically derive types from your server routes - no code generation or manual type definitions needed.
// Server
const app = new Hono()
  .get('/user', (c) => c.json({ id: 123, name: 'Alice' }))

// Client - types are automatically inferred
const client = hc<typeof app>('http://localhost')
const res = await client.user.$get()
const data = await res.json()

// TypeScript knows:
// data.id is number
// data.name is string
// data.email causes a type error (doesn't exist)

Type Inference

TypeScript automatically infers types from your Hono app definition. This includes:
  • Route paths and parameters
  • Request body types (JSON, form data)
  • Query parameters
  • Response body types
  • HTTP status codes
The type inference works entirely at compile time - there’s no runtime overhead.

InferResponseType

The InferResponseType utility type extracts the response type from a client method:
import { hc } from 'hono/client'
import type { InferResponseType } from 'hono/client'

const app = new Hono()
  .get('/posts/:id', (c) => {
    return c.json({
      id: 123,
      title: 'Hello Hono',
      published: true
    })
  })

const client = hc<typeof app>('http://localhost')

// Extract the response type
type Post = InferResponseType<typeof client.posts[':id'].$get>
// Post = { id: number, title: string, published: boolean }

Usage in Components

This is especially useful when passing data to components:
import type { InferResponseType } from 'hono/client'

type PostData = InferResponseType<typeof client.posts[':id'].$get>

function PostComponent({ post }: { post: PostData }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>ID: {post.id}</p>
    </div>
  )
}

InferRequestType

The InferRequestType utility type extracts the request parameter types:
import type { InferRequestType } from 'hono/client'

const app = new Hono()
  .post('/posts', 
    validator('json', () => ({ title: '', content: '' })),
    (c) => c.json({ id: 123 }, 201)
  )

const client = hc<typeof app>('http://localhost')

// Extract request type
type CreatePostInput = InferRequestType<typeof client.posts.$post>
// CreatePostInput = { json: { title: string, content: string } }

// Use in functions
function createPost(data: CreatePostInput['json']) {
  return client.posts.$post({ json: data })
}

Status Code Types

When your server returns different types based on status codes, the client preserves this information:
const app = new Hono()
  .get('/api/data', (c) => {
    const success = Math.random() > 0.5
    if (success) {
      return c.json({ data: 'Success!' }, 200)
    }
    return c.json({ error: 'Failed' }, 400)
  })

const client = hc<typeof app>('http://localhost')
const res = await client.api.data.$get()

// TypeScript knows the response type is a union
if (res.status === 200) {
  const data = await res.json()
  // data is { data: string }
  console.log(data.data)
} else if (res.status === 400) {
  const error = await res.json()
  // error is { error: string }
  console.log(error.error)
}

InferResponseType with Status Codes

You can specify a status code to narrow the response type:
// Get only the 200 response type
type SuccessData = InferResponseType<typeof client.api.data.$get, 200>
// SuccessData = { data: string }

// Get only the 400 response type
type ErrorData = InferResponseType<typeof client.api.data.$get, 400>
// ErrorData = { error: string }

Validation Types

When using validators on the server, the client automatically knows about required parameters:
import { validator } from 'hono/validator'

const app = new Hono()
  .get('/search',
    validator('query', () => ({ q: '', limit: 10 })),
    validator('header', () => ({ 'x-api-key': '' })),
    (c) => c.json({ results: [] })
  )

const client = hc<typeof app>('http://localhost')

// TypeScript enforces the query and header types
const res = await client.search.$get({
  query: { 
    q: 'hono',
    limit: 20  // TypeScript knows this should be a number
  },
  header: {
    'x-api-key': 'secret'
  }
})

Request Body Types

JSON Bodies

const app = new Hono()
  .post('/users',
    validator('json', () => ({
      name: '',
      email: '',
      age: 0
    })),
    (c) => c.json({ id: 1 }, 201)
  )

const client = hc<typeof app>('http://localhost')

// TypeScript enforces the JSON structure
await client.users.$post({
  json: {
    name: 'Alice',
    email: '[email protected]',
    age: 30
  }
})

Form Data

const app = new Hono()
  .post('/upload',
    validator('form', () => ({ file: new File([], ''), title: '' })),
    (c) => c.json({ success: true })
  )

const client = hc<typeof app>('http://localhost')

// TypeScript knows the form structure
await client.upload.$post({
  form: {
    file: selectedFile,
    title: 'My Upload'
  }
})

Optional and Required Parameters

The client automatically distinguishes between required and optional parameters:
const app = new Hono()
  // No input required
  .get('/status', (c) => c.json({ ok: true }))
  // Input required
  .post('/posts',
    validator('json', () => ({ title: '' })),
    (c) => c.json({ id: 1 })
  )

const client = hc<typeof app>('http://localhost')

// No arguments needed
await client.status.$get()

// Argument required (TypeScript error if omitted)
await client.posts.$post({
  json: { title: 'Hello' }
})

Type-safe Response Methods

The client response object has typed methods based on the content type:
const app = new Hono()
  .get('/json', (c) => c.json({ data: 'value' }))
  .get('/text', (c) => c.text('Hello'))
  .get('/html', (c) => c.html('<h1>Hello</h1>'))

const client = hc<typeof app>('http://localhost')

// JSON responses
const jsonRes = await client.json.$get()
const data = await jsonRes.json() // { data: string }

// Text responses  
const textRes = await client.text.$get()
const text = await textRes.text() // 'Hello' (literal type)

// Calling .json() on text response returns Promise<never>
const error = await textRes.json() // TypeScript error

Complex Type Scenarios

Union Types

const app = new Hono()
  .get('/data', (c) => {
    if (Math.random() > 0.5) {
      return c.json({ type: 'success', value: 123 })
    }
    return c.json({ type: 'error', message: 'Failed' })
  })

const client = hc<typeof app>('http://localhost')
const res = await client.data.$get()
const data = await res.json()

// TypeScript knows this is a union type
if (data.type === 'success') {
  console.log(data.value) // number
} else {
  console.log(data.message) // string
}

Nested Routes

const api = new Hono()
  .get('/users/:id', (c) => c.json({ id: 1, name: 'Alice' }))

const app = new Hono()
  .route('/api/v1', api)

const client = hc<typeof app>('http://localhost')

// Type inference works through nested routes
const res = await client.api.v1.users[':id'].$get({
  param: { id: '123' }
})
type User = InferResponseType<typeof client.api.v1.users[':id'].$get>

Best Practices

Always export the app type so clients can import it:
export type AppType = typeof app
Extract response types for use in multiple places:
type User = InferResponseType<typeof client.users[':id'].$get>
Use status codes to narrow response types:
if (res.status === 200) {
  // Narrowed to success type
}
The types are derived from your server code. When you change routes, the client types update automatically.

What’s Next?

Usage Guide

Learn practical patterns for using the client

Validators

Add runtime validation to your routes

Build docs developers (and LLMs) love