Skip to main content

Overview

The hc() function creates a type-safe HTTP client that automatically infers types from your Hono server routes. It uses Proxy objects to provide a chainable API that mirrors your server’s route structure.

Import

import { hc } from 'hono/client'

Function Signature

function hc<T extends Hono<any, any, any>, Prefix extends string = string>(
  baseUrl: Prefix,
  options?: ClientRequestOptions
): Client<T, Prefix>
baseUrl
string
required
The base URL of your API server (e.g., 'http://localhost:3000' or 'https://api.example.com')
options
ClientRequestOptions
Configuration options for the client
Client
Client<T, Prefix>
A proxy-based client that mirrors your server’s route structure with type-safe methods

Usage

Basic Usage

import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:3000')

// Make a GET request
const response = await client.api.posts.$get()
const data = await response.json()

With Headers

// Static headers
const client = hc<AppType>('http://localhost:3000', {
  headers: {
    'Authorization': 'Bearer token123'
  }
})

// Dynamic headers (useful for auth tokens)
const client = hc<AppType>('http://localhost:3000', {
  headers: async () => {
    const token = await getAuthToken()
    return {
      'Authorization': `Bearer ${token}`
    }
  }
})

Custom Fetch Implementation

import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:3000', {
  fetch: customFetch // Useful for testing with MSW or node-fetch
})

Request Methods

The client provides methods for all HTTP verbs, prefixed with $:
  • $get(args?, options?) - GET request
  • $post(args?, options?) - POST request
  • $put(args?, options?) - PUT request
  • $patch(args?, options?) - PATCH request
  • $delete(args?, options?) - DELETE request
  • $options(args?, options?) - OPTIONS request
  • $head(args?, options?) - HEAD request

Request Arguments

Each method accepts an optional args object with the following properties:
query
Record<string, string | string[]>
Query parameters for the request
param
Record<string, string>
Path parameters (e.g., for routes like /posts/:id)
json
object
JSON body data (automatically sets Content-Type: application/json)
form
Record<string, string | Blob | File | (string | Blob | File)[]>
Form data (automatically creates FormData and handles arrays)
header
Record<string, string>
Additional headers for this specific request
Cookies to send with the request

Example Requests

// GET with query parameters
const res = await client.api.search.$get({
  query: {
    q: 'hono',
    limit: '10'
  }
})

// POST with JSON body
const res = await client.api.posts.$post({
  json: {
    title: 'Hello Hono',
    content: 'This is a post'
  }
})

// PUT with path parameters
const res = await client.api.posts[':id'].$put({
  param: { id: '123' },
  json: { title: 'Updated Title' }
})

// DELETE with path parameters
const res = await client.api.posts[':id'].$delete({
  param: { id: '123' }
})

// POST with form data
const res = await client.api.upload.$post({
  form: {
    file: fileBlob,
    name: 'document.pdf'
  }
})

Utility Methods

$url()

Generates a fully-qualified URL object for the route.
const url = client.api.posts[':id'].$url({
  param: { id: '123' },
  query: { details: 'true' }
})
// Returns: URL object for "http://localhost:3000/api/posts/123?details=true"
Type Signature:
$url(arg?: { param?: Record<string, string>, query?: Record<string, string> }): URL

$path()

Generates the path portion of the URL (without the base URL).
const path = client.api.posts[':id'].$path({
  param: { id: '123' },
  query: { details: 'true' }
})
// Returns: "/api/posts/123?details=true"
Type Signature:
$path(arg?: { param?: Record<string, string>, query?: Record<string, string> }): string

$ws()

Creates a WebSocket connection for routes that support WebSocket upgrades.
const ws = client.api.ws.$ws({
  query: { token: 'auth-token' }
})

ws.addEventListener('message', (event) => {
  console.log('Received:', event.data)
})
Type Signature:
$ws(args?: { param?: Record<string, string>, query?: Record<string, string> }): WebSocket

Response Handling

All HTTP methods return a Promise<ClientResponse> which extends the standard Response interface with type-safe methods:
const response = await client.api.posts.$get()

// Type-safe JSON parsing
const data = await response.json() // Type is inferred from server

// Text response
const text = await response.text()

// Other Response methods
const blob = await response.blob()
const buffer = await response.arrayBuffer()
const bytes = await response.bytes()
const formData = await response.formData()

// Response properties
console.log(response.status)      // HTTP status code
console.log(response.ok)          // true if status is 2xx
console.log(response.headers)     // Response headers
console.log(response.statusText)  // Status text

Per-Request Options

You can override client-level options for individual requests:
const client = hc<AppType>('http://localhost:3000')

// Override headers for this request only
const response = await client.api.posts.$get(
  { query: { limit: '10' } },
  {
    headers: {
      'X-Custom-Header': 'value'
    }
  }
)

// Use custom fetch for this request
const response = await client.api.posts.$get(
  undefined,
  {
    fetch: mockFetch,
    init: {
      signal: abortController.signal
    }
  }
)

Type Safety

The client automatically infers request and response types from your server definition:
// On the server
const app = new Hono()
  .get('/posts/:id', (c) => {
    return c.json({
      id: c.req.param('id'),
      title: 'Hello',
      author: { name: 'John' }
    })
  })

export type AppType = typeof app

// On the client
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:3000')

// TypeScript knows the shape of the response
const res = await client.posts[':id'].$get({ param: { id: '123' } })
const data = await res.json()
// data.title        ✓ string
// data.author.name  ✓ string
// data.invalid      ✗ TypeScript error
See api/client/types for helper types like InferRequestType and InferResponseType.

Notes

  • The client automatically handles URL parameter replacement for path parameters like :id
  • Index routes (ending in /index) are automatically normalized
  • GET and HEAD requests never include a body, even if you accidentally provide one
  • FormData is automatically created when using the form option
  • Array values in forms are automatically expanded (multiple form fields with the same key)
  • The client is built on Proxy objects, so it works with any route structure without requiring code generation

Build docs developers (and LLMs) love