Skip to main content

Making Requests

The Hono RPC client provides a fluent API for making HTTP requests with full type safety.

GET Requests

Simple GET requests with no parameters:
import { hc } from 'hono/client'

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

const res = await client.posts.$get()
const data = await res.json()

GET with Query Parameters

Pass query parameters using the query property:
const app = new Hono()
  .get('/search',
    validator('query', () => ({ q: '', tag: [''], filter: '' })),
    (c) => c.json({ results: [] })
  )

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

const res = await client.search.$get({
  query: {
    q: 'hono',
    tag: ['framework', 'typescript'],
    filter: 'recent'
  }
})
Array query parameters are automatically serialized as multiple parameters with the same key (e.g., tag=framework&tag=typescript).

POST Requests with JSON

Send JSON data using the json property:
const app = new Hono()
  .post('/posts',
    validator('json', () => ({ title: '', content: '' })),
    (c) => c.json({ id: 123, title: 'New Post' }, 201)
  )

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

const res = await client.posts.$post({
  json: {
    title: 'Hello Hono',
    content: 'My first post'
  }
})

if (res.status === 201) {
  const post = await res.json()
  console.log('Created post:', post.id)
}

POST with Form Data

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

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

const res = await client.upload.$post({
  form: {
    title: 'My File',
    file: selectedFile // File object from input
  }
})

Multiple Form Values

Form fields can have multiple values:
await client['form-endpoint'].$post({
  form: {
    tags: ['tag1', 'tag2', 'tag3'],
    title: 'Single value'
  }
})

Path Parameters

Access routes with path parameters using bracket notation:
const app = new Hono()
  .get('/posts/:id', (c) => c.json({ id: 123, title: 'Post' }))
  .put('/posts/:id', (c) => c.json({ success: true }))
  .delete('/posts/:id', (c) => c.json({ deleted: true }))

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

// GET /posts/123
await client.posts[':id'].$get({
  param: { id: '123' }
})

// PUT /posts/123
await client.posts[':id'].$put({
  param: { id: '123' },
  json: { title: 'Updated' }
})

// DELETE /posts/123
await client.posts[':id'].$delete({
  param: { id: '123' }
})

Multiple Path Parameters

const app = new Hono()
  .get('/posts/:postId/comments/:commentId', (c) => {
    return c.json({ postId: 1, commentId: 2, text: 'Great!' })
  })

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

const res = await client.posts[':postId'].comments[':commentId'].$get({
  param: {
    postId: '123',
    commentId: '456'
  }
})

Headers and Cookies

Request Headers

Add custom headers to individual requests:
const res = await client.api.protected.$get({
  header: {
    'Authorization': 'Bearer token123',
    'X-Custom-Header': 'value'
  }
})

Global Headers

Set headers for all requests when creating the client:
const client = hc<typeof app>('http://localhost', {
  headers: {
    'Authorization': 'Bearer token123',
    'X-App-Version': '1.0.0'
  }
})

Dynamic Headers

Use a function to compute headers dynamically:
let token = 'initial-token'

const client = hc<typeof app>('http://localhost', {
  headers: () => ({
    'Authorization': `Bearer ${token}`,
    'X-Timestamp': Date.now().toString()
  })
})

// Headers are evaluated on each request
await client.api.$get() // Uses current token value

token = 'new-token'
await client.api.$get() // Uses updated token

Async Headers

Headers can be computed asynchronously:
const client = hc<typeof app>('http://localhost', {
  headers: async () => {
    const token = await getTokenFromStorage()
    return {
      'Authorization': `Bearer ${token}`
    }
  }
})

Cookies

Send cookies with your requests:
const res = await client.api.$get({
  cookie: {
    sessionId: 'abc123',
    theme: 'dark'
  }
})

Handling Responses

Response Object

The client returns a standard Response object with typed methods:
const res = await client.posts.$get()

// Check status
if (res.ok) {
  console.log('Success!')
}
console.log(res.status) // 200, 404, etc.

// Access headers
const contentType = res.headers.get('content-type')

// Parse body (type-safe)
const data = await res.json() // TypeScript knows the shape

JSON Responses

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

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

// TypeScript knows:
// user.id is number
// user.name is string
console.log(user.name)

Text Responses

const app = new Hono()
  .get('/message', (c) => c.text('Hello, Hono!'))

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

// text is typed as 'Hello, Hono!' (literal type)
console.log(text)

Blob and Binary Data

const res = await client.download.$get()
const blob = await res.blob()
const arrayBuffer = await res.arrayBuffer()
const bytes = await res.bytes()

Error Handling

Status Code Checking

Handle different status codes with type narrowing:
const res = await client.posts[':id'].$get({ param: { id: '123' } })

if (res.status === 200) {
  const post = await res.json()
  // TypeScript knows this is the success type
  console.log(post.title)
} else if (res.status === 404) {
  const error = await res.json()
  // TypeScript knows this is the error type
  console.error(error.message)
}

Using res.ok

Check if the response is successful (status 200-299):
const res = await client.posts.$post({ json: { title: 'New' } })

if (res.ok) {
  // Status is 2xx
  const data = await res.json()
  console.log('Success:', data)
} else {
  // Status is not 2xx
  console.error('Failed with status:', res.status)
}
When using res.ok, TypeScript narrows the type to only success status codes.

Parsing Response Errors

Use the parseResponse utility for automatic error handling:
import { parseResponse } from 'hono/client'

try {
  const data = await parseResponse(client.posts.$get())
  // data is automatically parsed and typed
  console.log(data)
} catch (error) {
  // Throws DetailedError for non-2xx responses
  if (error instanceof DetailedError) {
    console.error('Status:', error.response.status)
    console.error('Body:', error.body)
  }
}

Middleware Error Responses

Handle errors returned from middleware:
const app = new Hono()
  .post('/posts',
    async (c, next) => {
      const auth = c.req.header('authorization')
      if (!auth) {
        return c.json({ error: 'Unauthorized' }, 401)
      }
      return next()
    },
    validator('json', (input, c) => {
      if (!input.title) {
        return c.json({ error: 'Bad request' }, 400)
      }
      return input
    }),
    (c) => c.json({ id: 1 }, 200)
  )

const client = hc<typeof app>('http://localhost')
const res = await client.posts.$post({ json: { title: 'Post' } })

// TypeScript knows about all possible status codes
if (res.status === 200) {
  const data = await res.json() // { id: number }
} else if (res.status === 400) {
  const error = await res.json() // { error: 'Bad request' }
} else if (res.status === 401) {
  const error = await res.json() // { error: 'Unauthorized' }
}

Advanced Features

Custom Fetch Implementation

Provide your own fetch implementation:
import { hc } from 'hono/client'
import nodeFetch from 'node-fetch'

const client = hc<typeof app>('http://localhost', {
  fetch: nodeFetch as typeof fetch
})

Using with app.request

Test your API without making network calls:
import { Hono } from 'hono'
import { hc } from 'hono/client'

const app = new Hono()
  .get('/hello', (c) => c.json({ message: 'Hello!' }))

const client = hc<typeof app>('', { fetch: app.request })

// Makes request directly to the app (no HTTP)
const res = await client.hello.$get()
const data = await res.json()
This is perfect for testing - no need to start a server!

Custom RequestInit

Pass custom RequestInit options:
const client = hc<typeof app>('http://localhost', {
  init: {
    credentials: 'include',
    mode: 'cors',
    cache: 'no-cache'
  }
})
You can also override per request:
await client.api.$get(undefined, {
  init: {
    signal: abortController.signal,
    cache: 'force-cache'
  }
})
The init option takes the highest priority and can overwrite Hono’s settings for body, method, and headers.

Custom Query Serialization

Customize how query parameters are serialized:
const client = hc<typeof app>('http://localhost', {
  buildSearchParams: (query) => {
    const params = new URLSearchParams()
    for (const [key, value] of Object.entries(query)) {
      if (Array.isArray(value)) {
        // Use bracket notation for arrays
        value.forEach(item => params.append(`${key}[]`, item))
      } else {
        params.set(key, value)
      }
    }
    return params
  }
})

// query: { tags: ['a', 'b'] }
// becomes: ?tags[]=a&tags[]=b
// instead of: ?tags=a&tags=b

WebSocket Support

Connect to WebSocket endpoints:
import { upgradeWebSocket } from 'hono/adapter'

const app = new Hono()
  .get('/ws', upgradeWebSocket((c) => ({
    onMessage(event, ws) {
      ws.send('Hello from server!')
    }
  })))

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

ws.addEventListener('message', (event) => {
  console.log('Received:', event.data)
})

URL Utilities

Get Full URL

Generate the full URL for a route:
const url = client.api.posts[':id'].$url({
  param: { id: '123' },
  query: { format: 'json' }
})

console.log(url.href) // http://localhost/api/posts/123?format=json
console.log(url.pathname) // /api/posts/123
console.log(url.search) // ?format=json

Get Path Only

Generate just the path (without domain):
const path = client.api.posts[':id'].$path({
  param: { id: '123' },
  query: { format: 'json' }
})

console.log(path) // /api/posts/123?format=json
These utilities are useful for generating links in your UI or for debugging.

Best Practices

Export a configured client to use throughout your app:
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'

export const client = hc<AppType>(import.meta.env.VITE_API_URL)
Create a wrapper for consistent error handling:
async function apiCall<T>(promise: Promise<T>) {
  const res = await promise
  if (!res.ok) {
    throw new Error(`API Error: ${res.status}`)
  }
  return res
}
Don’t hardcode API URLs:
const client = hc<typeof app>(process.env.API_URL || 'http://localhost:8787')
Use status code checks to narrow response types:
if (res.status === 200) {
  // TypeScript knows the exact response type here
}

What’s Next?

Validators

Add runtime validation to your API

Middleware

Learn about Hono’s middleware system

Build docs developers (and LLMs) love