Last Updated: 3/7/2026
RPC
The RPC feature allows sharing of the API specifications between the server and the client.
First, export the typeof your Hono app (commonly called AppType)—or just the routes you want available to the client—from your server code.
By accepting AppType as a generic parameter, the Hono Client can infer both the input type(s) specified by the Validator, and the output type(s) emitted by handlers returning c.json().
NOTE: For the RPC types to work properly in a monorepo, in both the Client’s and Server’s tsconfig.json files, set "strict": true in compilerOptions.
Server
All you need to do on the server side is to write a validator and create a variable route. The following example uses Zod Validator.
const route = app.post(
'/posts',
zValidator(
'form',
z.object({
title: z.string(),
body: z.string(),
})
),
(c) => {
// ...
return c.json(
{
ok: true,
message: 'Created!',
},
201
)
}
)Then, export the type to share the API spec with the Client.
export type AppType = typeof routeClient
On the Client side, import hc and AppType first.
import type { AppType } from '.'
import { hc } from 'hono/client'hc is a function to create a client. Pass AppType as Generics and specify the server URL as an argument.
const client = hc<AppType>('http://localhost:8787/')Call client.{path}.{method} and pass the data you wish to send to the server as an argument.
const res = await client.posts.$post({
form: {
title: 'Hello',
body: 'Hono is a cool project',
},
})The res is compatible with the “fetch” Response. You can retrieve data from the server with res.json().
if (res.ok) {
const data = await res.json()
console.log(data.message)
}Cookies
To make the client send cookies with every request, add { 'init': { 'credentials": 'include' } } to the options when you’re creating the client.
const client = hc<AppType>('http://localhost:8787/', {
init: {
credentials: 'include',
},
})
const res = await client.posts.$get({
query: {
id: '123',
},
})Status code
If you explicitly specify the status code, such as 200 or 404, in c.json(). It will be added as a type for passing to the client.
const app = new Hono().get(
'/posts',
zValidator(
'query',
z.object({
id: z.string(),
})
),
async (c) => {
const { id } = c.req.valid('query')
const post: Post | undefined = await getPost(id)
if (post === undefined) {
return c.json({ error: 'not found' }, 404) // Specify 404
}
return c.json({ post }, 200) // Specify 200
}
)
export type AppType = typeof appYou can get the data by the status code.
const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$get({
query: {
id: '123',
},
})
if (res.status === 404) {
const data: { error: string } = await res.json()
console.log(data.error)
}
if (res.ok) {
const data: { post: Post } = await res.json()
console.log(data.post)
}
// { post: Post } | { error: string }
type ResponseType = InferResponseType<typeof client.posts.$get>
// { post: Post }
type ResponseType200 = InferResponseType<
typeof client.posts.$get,
200
>Path parameters
You can also handle routes that include path parameters or query values.
const route = app.get(
'/posts/:id',
zValidator(
'query',
z.object({
page: z.coerce.number().optional(), // coerce to convert to number
})
),
(c) => {
// ...
return c.json({
title: 'Night',
body: 'Time to sleep',
})
}
)Both path parameters and query values must be passed as string, even if the underlying value is of a different type.
Specify the string you want to include in the path with param, and any query values with query.
const res = await client.posts[':id'].$get({
param: {
id: '123',
},
query: {
page: '1', // `string`, converted by the validator to `number`
},
})Multiple parameters
Handle routes with multiple parameters.
const route = app.get(
'/posts/:postId/:authorId',
zValidator(
'query',
z.object({
page: z.string().optional(),
})
),
(c) => {
// ...
return c.json({
title: 'Night',
body: 'Time to sleep',
})
}
)Add multiple [''] to specify params in path.
const res = await client.posts[':postId'][':authorId'].$get({
param: {
postId: '123',
authorId: '456',
},
query: {},
})Headers
You can append the headers to the request.
const res = await client.search.$get(
{
//...
},
{
headers: {
'X-Custom-Header': 'Here is Hono Client',
'X-User-Agent': 'hc',
},
}
)To add a common header to all requests, specify it as an argument to the hc function.
const client = hc<AppType>('/api', {
headers: {
Authorization: 'Bearer TOKEN',
},
})$url()
You can get a URL object for accessing the endpoint by using $url().
WARNING: You have to pass in an absolute URL for this to work. Passing in a relative URL / will result in an error.
const route = app
.get('/api/posts', (c) => c.json({ posts }))
.get('/api/posts/:id', (c) => c.json({ post }))
const client = hc<typeof route>('http://localhost:8787/')
let url = client.api.posts.$url()
console.log(url.pathname) // `/api/posts`
url = client.api.posts[':id'].$url({
param: {
id: '123',
},
})
console.log(url.pathname) // `/api/posts/123`$path()
$path() is similar to $url(), but returns a path string instead of a URL object. Unlike $url(), it does not include the base URL origin, so it works regardless of the base URL you pass to hc.
const route = app
.get('/api/posts', (c) => c.json({ posts }))
.get('/api/posts/:id', (c) => c.json({ post }))
const client = hc<typeof route>('http://localhost:8787/')
let path = client.api.posts.$path()
console.log(path) // `/api/posts`
path = client.api.posts[':id'].$path({
param: {
id: '123',
},
})
console.log(path) // `/api/posts/123`You can also pass query parameters:
const path = client.api.posts.$path({
query: {
page: '1',
limit: '10',
},
})
console.log(path) // `/api/posts?page=1&limit=10`Using RPC with larger applications
In the case of a larger application, you need to be careful about the type of inference. A simple way to do this is to chain the handlers so that the types are always inferred.
// authors.ts
import { Hono } from 'hono'
const app = new Hono()
.get('/', (c) => c.json('list authors'))
.post('/', (c) => c.json('create an author', 201))
.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default app// books.ts
import { Hono } from 'hono'
const app = new Hono()
.get('/', (c) => c.json('list books'))
.post('/', (c) => c.json('create a book', 201))
.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default appYou can then import the sub-routers as you usually would, and make sure you chain their handlers as well.
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'
const app = new Hono()
const routes = app.route('/authors', authors).route('/books', books)
export default app
export type AppType = typeof routesYou can now create a new client using the registered AppType and use it as you would normally.