Last Updated: 3/9/2026
RPC Client
Hono’s RPC (Remote Procedure Call) feature enables end-to-end type safety between your server and client. Share API types automatically without code generation.
Why RPC?
Without RPC:
// server.ts
app.post('/posts', async (c) => {
const body = await c.req.json()
return c.json({ id: 1, title: body.title }, 201)
})
// client.ts
const res = await fetch('/posts', {
method: 'POST',
body: JSON.stringify({ title: 'Hello' })
})
const data = await res.json() // any - no type safety!With RPC:
// server.ts
const route = app.post('/posts',
zValidator('json', z.object({ title: z.string() })),
(c) => {
const { title } = c.req.valid('json')
return c.json({ id: 1, title }, 201)
}
)
export type AppType = typeof route
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:3000')
const res = await client.posts.$post({
json: { title: 'Hello' }
})
const data = await res.json()
// ^? { id: number; title: string } - fully typed!Quick Start
1. Export Server Type
Create your API and export its type:
// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const route = app.post('/posts',
zValidator('json', z.object({
title: z.string(),
body: z.string(),
})),
(c) => {
const data = c.req.valid('json')
return c.json(
{ ok: true, post: { id: 1, ...data } },
201
)
}
)
export type AppType = typeof route
export default app2. Create Client
Import the type and create a typed client:
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:3000')
const res = await client.posts.$post({
json: {
title: 'My Post',
body: 'Post content'
}
})
if (res.ok) {
const data = await res.json()
console.log(data.post.id) // Typed!
console.log(data.post.title) // Typed!
}Making Requests
GET Requests
// server.ts
const route = app.get('/posts', (c) => {
return c.json({ posts: [{ id: 1, title: 'Hello' }] })
})
// client.ts
const res = await client.posts.$get()
const data = await res.json()
// ^? { posts: { id: number; title: string }[] }POST Requests
// server.ts
const route = app.post('/posts',
zValidator('json', z.object({ title: z.string() })),
(c) => {
const { title } = c.req.valid('json')
return c.json({ post: { id: 1, title } }, 201)
}
)
// client.ts
const res = await client.posts.$post({
json: { title: 'New Post' }
})PUT/PATCH Requests
// server.ts
const route = app.put('/posts/:id',
zValidator('json', z.object({ title: z.string() })),
(c) => {
const id = c.req.param('id')
const { title } = c.req.valid('json')
return c.json({ post: { id, title } })
}
)
// client.ts
const res = await client.posts[':id'].$put({
param: { id: '123' },
json: { title: 'Updated' }
})DELETE Requests
// server.ts
const route = app.delete('/posts/:id', (c) => {
const id = c.req.param('id')
return c.json({ deleted: id })
})
// client.ts
const res = await client.posts[':id'].$delete({
param: { id: '123' }
})Path Parameters
Single Parameter
// server.ts
app.get('/posts/:id', (c) => {
const id = c.req.param('id')
return c.json({ post: { id, title: 'Post' } })
})
// client.ts
const res = await client.posts[':id'].$get({
param: { id: '123' }
})Important: Parameters must be strings, even if the server coerces them:
// ❌ Wrong
await client.posts[':id'].$get({
param: { id: 123 } // Type error!
})
// ✅ Correct
await client.posts[':id'].$get({
param: { id: '123' }
})Multiple Parameters
// server.ts
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param()
return c.json({ post: postId, comment: commentId })
})
// client.ts
const res = await client.posts[':postId'].comments[':commentId'].$get({
param: {
postId: '123',
commentId: '456'
}
})Parameters with Slashes
Use regex patterns to capture slashes:
// server.ts
app.get('/posts/:id{.+}',
zValidator('param', z.object({ id: z.string() })),
(c) => {
const { id } = c.req.valid('param')
return c.json({ id }) // id can be "123/456"
}
)
// client.ts
const res = await client.posts[':id'].$get({
param: { id: '123/456' } // Works!
})Query Parameters
// server.ts
app.get('/search',
zValidator('query', z.object({
q: z.string(),
page: z.coerce.number().optional(),
})),
(c) => {
const { q, page } = c.req.valid('query')
return c.json({ query: q, page })
}
)
// client.ts
const res = await client.search.$get({
query: {
q: 'hono',
page: '2' // Must be string!
}
})Note: Query parameters are always strings. Use z.coerce on the server to convert.
Headers
Per-Request Headers
const res = await client.api.posts.$get(
{},
{
headers: {
'Authorization': 'Bearer token',
'X-Custom-Header': 'value'
}
}
)Global Headers
Set headers for all requests:
const client = hc<AppType>('http://localhost:3000', {
headers: {
'Authorization': 'Bearer token'
}
})
// All requests include Authorization header
const res = await client.api.posts.$get()Status Codes
Type-safe status code handling:
// server.ts
app.get('/posts/:id', async (c) => {
const post = await db.getPost(c.req.param('id'))
if (!post) {
return c.json({ error: 'Not found' }, 404)
}
return c.json({ post }, 200)
})
// client.ts
const res = await client.posts[':id'].$get({ param: { id: '123' } })
if (res.status === 404) {
const data = await res.json()
// ^? { error: string }
console.log(data.error)
}
if (res.ok) {
const data = await res.json()
// ^? { post: Post }
console.log(data.post)
}InferResponseType
Get response types programmatically:
import type { InferResponseType } from 'hono/client'
// All possible responses
type AllResponses = InferResponseType<typeof client.posts[':id'].$get>
// ^? { post: Post } | { error: string }
// Specific status code
type Success = InferResponseType<typeof client.posts[':id'].$get, 200>
// ^? { post: Post }
type NotFound = InferResponseType<typeof client.posts[':id'].$get, 404>
// ^? { error: string }Global Error Responses
Include global error types:
import type { ApplyGlobalResponse } from 'hono/client'
// server.ts
const app = new Hono()
.get('/api/users', (c) => c.json({ users: [] }, 200))
.onError((err, c) => c.json({ error: err.message }, 500))
type AppWithErrors = ApplyGlobalResponse<
typeof app,
{
500: { json: { error: string } }
}
>
// client.ts
const client = hc<AppWithErrors>('http://localhost')
const res = await client.api.users.$get()
if (res.status === 500) {
const data = await res.json()
// ^? { error: string }
}Cookies
Send cookies with requests:
const client = hc<AppType>('http://localhost:3000', {
init: {
credentials: 'include'
}
})
// Now cookies are sent with every request
const res = await client.api.posts.$get()Request Options
Abort Requests
const abortController = new AbortController()
const res = await client.api.posts.$get(
{},
{
init: {
signal: abortController.signal
}
}
)
// Later...
abortController.abort()Custom Fetch
Use a custom fetch implementation:
const client = hc<AppType>('http://localhost', {
fetch: customFetch
})Example - Cloudflare Service Bindings:
// wrangler.toml
// services = [{ binding = "API", service = "api-service" }]
const client = hc<AppType>('http://localhost', {
fetch: c.env.API.fetch.bind(c.env.API)
})URL Helpers
$url()
Get the full URL:
const url = client.api.posts[':id'].$url({
param: { id: '123' }
})
console.log(url.pathname) // "/api/posts/123"
console.log(url.toString()) // "http://localhost:3000/api/posts/123"Note: Requires absolute base URL:
// ❌ Wrong - throws error
const client = hc<AppType>('/')
client.api.posts.$url()
// ✅ Correct
const client = hc<AppType>('http://localhost:3000')
client.api.posts.$url()$path()
Get just the path:
const path = client.api.posts[':id'].$path({
param: { id: '123' }
})
console.log(path) // "/api/posts/123"With query parameters:
const path = client.api.posts.$path({
query: {
page: '2',
limit: '10'
}
})
console.log(path) // "/api/posts?page=2&limit=10"File Uploads
// server.ts
app.put('/user/picture',
zValidator('form', z.object({
file: z.instanceof(File)
})),
async (c) => {
const { file } = c.req.valid('form')
// Process file...
return c.json({ filename: file.name })
}
)
// client.ts
const file = new File([blob], 'photo.jpg', { type: 'image/jpeg' })
const res = await client.user.picture.$put({
form: { file }
})Larger Applications
Organizing Routes
Split routes into modules:
// routes/posts.ts
import { Hono } from 'hono'
const posts = new Hono()
.get('/', (c) => c.json({ posts: [] }))
.post('/', (c) => c.json({ post: {} }, 201))
.get('/:id', (c) => c.json({ post: {} }))
export default posts// routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
.get('/', (c) => c.json({ users: [] }))
.post('/', (c) => c.json({ user: {} }, 201))
export default users// index.ts
import { Hono } from 'hono'
import posts from './routes/posts'
import users from './routes/users'
const app = new Hono()
const routes = app
.route('/posts', posts)
.route('/users', users)
export default app
export type AppType = typeof routes// client.ts
import { hc } from 'hono/client'
import type { AppType } from './index'
const client = hc<AppType>('http://localhost:3000')
// Fully typed!
await client.posts.$get()
await client.posts[':id'].$get({ param: { id: '1' } })
await client.users.$get()Integration with SWR
Use with React and SWR:
import useSWR from 'swr'
import { hc } from 'hono/client'
import type { InferRequestType } from 'hono/client'
import type { AppType } from './server'
const App = () => {
const client = hc<AppType>('/api')
const $get = client.posts.$get
const fetcher = (arg: InferRequestType<typeof $get>) => async () => {
const res = await $get(arg)
return await res.json()
}
const { data, error, isLoading } = useSWR(
'posts',
fetcher({
query: { page: '1' }
})
)
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
return <div>{data?.posts.length} posts</div>
}Performance Tips
Compile Types
Pre-compile types for better IDE performance:
// client-factory.ts
import { app } from './server'
import { hc } from 'hono/client'
export type Client = ReturnType<typeof hc<typeof app>>
export const createClient = (...args: Parameters<typeof hc>): Client =>
hc<typeof app>(...args)// usage.ts
import { createClient } from './client-factory'
const client = createClient('http://localhost:3000')
// Types are already compiled!Version Matching
Important: Ensure Hono versions match between server and client in monorepos.
// Both package.json files
{
"dependencies": {
"hono": "^4.0.0" // Same version!
}
}What’s Next?
- Learn validation: Validation Guide
- Test your API: Testing Guide
- Deploy: Deployment Guides