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
}
})
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
Add custom headers to individual requests:
const res = await client . api . protected . $get ({
header: {
'Authorization' : 'Bearer token123' ,
'X-Custom-Header' : 'value'
}
})
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'
}
})
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
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
Create a shared client instance
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 )
Handle errors consistently
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
}
Use environment variables for URLs
Don’t hardcode API URLs: const client = hc < typeof app >( process . env . API_URL || 'http://localhost:8787' )
Leverage TypeScript's type narrowing
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