RPC feature exports server API types to client
The RPC feature allows sharing of API specifications between server and client. 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 input types specified by the Validator and output types emitted by handlers returning c.json().
RPC types require strict TypeScript mode in monorepo
For RPC types to work properly in a monorepo, in both the Client's and Server's tsconfig.json files, set "strict": true in compilerOptions.
RPC client creation with hc function
On the client side, import hc and AppType. hc is a function to create a client. Pass AppType as generics and specify the server URL as an argument. Example: const client = hc<AppType>('http://localhost:8787/')
RPC client request calling convention
Call client.{path}.{method} and pass the data you wish to send to the server as an argument. The response res is compatible with the fetch Response API. Retrieve data from the server with res.json().
RPC client send cookies with every request
To make the client send cookies with every request, add { 'init': { 'credentials': 'include' } } to the options when creating the client. Example: const client = hc<AppType>('http://localhost:8787/', { init: { credentials: 'include' } })
RPC status code type inference
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. The client can then get the data by the status code using res.status.
RPC InferResponseType for status-specific response types
Use InferResponseType to infer the response type from a client route. You can optionally pass a specific status code as the second parameter: InferResponseType<typeof client.posts.$get, 200> returns only the type for that status code.
RPC ApplyGlobalResponse for global error types
Hono RPC client does not automatically infer response types from global error handlers like app.onError() or global middleware. Use the ApplyGlobalResponse type helper to merge global error response types into all routes. Example: type AppWithErrors = ApplyGlobalResponse<typeof app, { 500: { json: { error: string } } }>
RPC ApplyGlobalResponse multiple status codes
You can define multiple global error status codes at once with ApplyGlobalResponse: type AppWithErrors = ApplyGlobalResponse<typeof app, { 401: { json: { error: string; message: string } }, 500: { json: { error: string; message: string } } }>
RPC avoid c.notFound() with client
Do not use c.notFound() for the Not Found response if you want to use a client. The data that the client gets from the server cannot be inferred correctly. Instead, use c.json() and specify the status code for the 404 response, or use module augmentation to extend the NotFoundResponse interface.
RPC module augmentation for typed notFound response
You can use module augmentation to extend the NotFoundResponse interface to make c.notFound() return a typed response. Declare module 'hono' with interface NotFoundResponse extending Response and TypedResponse<{ error: string }, 404, 'json'>.
RPC path parameters must be passed as strings
Both path parameters and query values must be passed as string when using the RPC client, 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.
RPC multiple path parameters syntax
Handle routes with multiple parameters by adding multiple [''] to specify params in path. Example: client.posts[':postId'][':authorId'].$get({ param: { postId: '123', authorId: '456' } })
RPC parameters with slashes using regex
The hc function does not URL-encode the values of param. To include slashes in parameters, use regular expressions on the server. Example server route: app.get('/posts/:id{.+}', ...). Note that basic path parameters without regular expressions do not match slashes. Encoding parameters using encodeURIComponent is the recommended approach.
RPC append headers to request
You can append headers to individual requests using the second parameter: const res = await client.search.$get({}, { headers: { 'X-Custom-Header': 'value' } }). 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' } })
RPC init option for RequestInit
You can pass the fetch RequestInit object to the request as the init option. A RequestInit object defined by init takes the highest priority and can be used to overwrite things set by other options like body, method, or headers. Example: { init: { signal: abortController.signal } }
RPC $url() method requires absolute URL
Use $url() to get a URL object for accessing the endpoint. You must pass an absolute URL for this to work; passing a relative URL like '/' will result in a TypeError. Passing a relative URL will throw: Uncaught TypeError: Failed to construct 'URL': Invalid URL
RPC $url() returns URL object for endpoint
Call $url() on a client route to get a URL object. Access the pathname property: client.api.posts.$url() returns URL with pathname '/api/posts'. For path parameters: client.api.posts[':id'].$url({ param: { id: '123' } }) returns pathname '/api/posts/123'
RPC typed URL for type-safe URL keys
Pass the base URL as the second type parameter to hc to get more precise URL types: const client = hc<typeof route, 'http://localhost:8787'>('http://localhost:8787/'). This returns a TypedURL with precise type information including protocol, host, and path, useful as a type-safe key for libraries like SWR.
RPC $path() returns path string instead of URL
$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 passed to hc. Example: client.api.posts.$path() returns '/api/posts'
RPC $path() with query parameters
Pass query parameters to $path(): const path = client.api.posts.$path({ query: { page: '1', limit: '10' } }) returns '/api/posts?page=1&limit=10'
RPC file upload via form body
Upload files using a form body on the client: const res = await client.user.picture.$put({ form: { file: new File([fileToUpload], filename, { type: fileToUpload.type }) } }). On the server use zValidator with z.instanceof(File).
RPC custom fetch method
You can set a custom fetch method when creating the client. Example for Cloudflare Worker Service Bindings: const client = hc<CreateProfileType>('http://localhost', { fetch: c.env.AUTH.fetch.bind(c.env.AUTH) })
RPC custom query serializer with buildSearchParams
Customize how query parameters are serialized using the buildSearchParams option. This is useful for bracket notation for arrays or custom formats. Pass a function that takes query and returns URLSearchParams.
RPC buildSearchParams example for array bracket notation
Example custom buildSearchParams: buildSearchParams: (query) => { const searchParams = new URLSearchParams(); for (const [k, v] of Object.entries(query)) { if (v === undefined) continue; if (Array.isArray(v)) { v.forEach((item) => searchParams.append(`${k}[]`, item)) } else { searchParams.set(k, v) } } return searchParams }
RPC InferRequestType and InferResponseType helpers
Use InferRequestType and InferResponseType to know the type of object to be requested and the type of object to be returned. Example: type ReqType = InferRequestType<typeof $post>['form']; type ResType = InferResponseType<typeof $post>
RPC parseResponse helper for type-safe parsing
Use parseResponse() helper to easily parse a Response from hc with type-safety. It automatically parses the response body based on Content-Type and throws an error if response is not ok. Example: const result = await parseResponse(client.hello.$get()).catch((e: DetailedError) => { console.error(e) })
RPC with React SWR library example
Example using SWR with Hono RPC client: import useSWR from 'swr'; const App = () => { const client = hc<AppType>('/api'); const $get = client.hello.$get; const fetcher = (arg: InferRequestType<typeof $get>) => async () => { const res = await $get(arg); return await res.json() }; const { data, error, isLoading } = useSWR('api-hello', fetcher({ query: { name: 'SWR' } })); return <h1>{data?.message}</h1> }
RPC with larger applications - split into multiple files
For larger applications, split your app into multiple files with separate handlers. Chain the handlers so that types are always inferred. At the top level, chain .route() calls and export the type of the result. Example: const routes = app.route('/authors', authors).route('/books', books); export type AppType = typeof routes
RPC IDE performance issue with many routes
When using RPC, the more routes you have, the slower your IDE becomes. This is because massive amounts of type instantiations are executed to infer the type of your app. Each route results in multiple type arguments being instantiated, which takes significant time for tsserver.
RPC Hono version mismatch causes type issues
If your backend is separated from the frontend in a different directory, ensure that Hono versions match. Using one Hono version on the backend and another on the frontend causes issues such as 'Type instantiation is excessively deep and possibly infinite'.
RPC TypeScript project references for monorepo
If your backend and frontend are separate, use TypeScript project references. TypeScript project references allow one TypeScript codebase to access and use code from another TypeScript codebase. This is necessary to access code like AppType from the backend on the frontend.
RPC compile code before using for IDE performance
tsc can do heavy type instantiation tasks at compile time instead of during IDE usage. Compile your client including the server app and export a pre-calculated Client type. This avoids tsserver needing to instantiate all type arguments every time, making IDE performance significantly faster.
RPC hcWithType trick for compiled type performance
Create a type-safe client wrapper that pre-calculates types at compile time: export type Client = ReturnType<typeof hc<typeof app>>; export const hcWithType = (...args: Parameters<typeof hc>): Client => hc<typeof app>(...args). Then use hcWithType instead of hc.
RPC specify type arguments manually for performance
You can specify type arguments manually to avoid type instantiation, though this is cumbersome. Example: const app = new Hono().get<'foo/:id'>('foo/:id', (c) => c.json({ ok: true }, 200)). This helps with IDE performance but requires effort for many routes.
RPC split app into multiple files for IDE performance
As described in using RPC with larger applications, split your app into multiple apps and create a client for each. This way tsserver does not need to instantiate types for all routes at once, improving IDE performance.
RPC client access request parameters and query strings
On the server side, use c.req.valid() to access validated request parameters. The RPC client automatically passes parameters through the validators you define with zValidator. For path parameters use param: { id: '123' }, for query strings use query: { page: '1' }, for JSON body use json: { field: 'value' }, and for form data use form: { field: 'value' }.
RPC server validator setup with Zod
On the server side, write a validator and create a variable route. 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 type AppType = typeof route