Skip to Content
guidesRpc Client

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 app

2. 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?