Hono RPC Mode Complete Guide — Type-Safe Fullstack Development Without Code Generation [2026]
Hono's RPC mode delivers tRPC-like type-safe fullstack development without code generation or OpenAPI. Complete 2026 guide covering hc client, InferRequestType, multi-route integration, and production patterns.
What Is Hono RPC Mode?
Hono RPC mode lets the client automatically infer server-side route types — no code generation or OpenAPI spec required. One line, `export type AppType = typeof route`, connects server and client through TypeScript types, delivering a tRPC-like developer experience natively within Hono.
How Hono RPC Works
The server exports `AppType`, which is passed directly to `hc<AppType>`. TypeScript validates everything at build time, eliminating an entire class of runtime errors before they reach production.
Hono RPC vs. tRPC vs. OpenAPI vs. GraphQL
The table below compares the major type-safe API approaches.
| Criteria | Hono RPC | tRPC | OpenAPI | GraphQL |
|---|---|---|---|---|
| Code generation | Not required | Not required | Required | Required |
| Schema definition | Zod (optional) | Zod/Yup | YAML/JSON | SDL |
| Framework lock-in | Hono only | Framework-agnostic | Agnostic | Agnostic |
| Bundle size | Minimal | Medium | Large | Large |
| Edge runtime | Native | Limited | Agnostic | Agnostic |
| Learning curve | Low | Low–Medium | Medium | High |
Server-Side Implementation
Define your routes with Hono and export the type. The `zValidator` middleware validates request bodies and feeds the type information into the RPC chain.
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().min(1),
body: z.string()
})),
(c) => {
const data = c.req.valid('json')
return c.json({ ok: true, id: 1, ...data }, 201)
}
)
export type AppType = typeof route
export default appClient-Side Implementation
Pass `AppType` to the `hc` factory function. Method names, argument shapes, and return types are all fully inferred — your IDE's autocomplete will guide you through every call.
import type { AppType } from './server'
import { hc } from 'hono/client'
const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$post({
json: { title: 'Hello', body: 'Hono RPC is great!' }
})
if (res.ok) {
const data = await res.json()
// data: { ok: true, id: number, title: string, body: string }
}InferRequestType and InferResponseType
Use these type helpers to extract the exact request and response shapes from your client. They are invaluable for form validation, test fixtures, and shared type libraries.
import { InferRequestType, InferResponseType } from 'hono/client'
import type { AppType } from './server'
import { hc } from 'hono/client'
const client = hc<AppType>('/')
type CreatePostInput = InferRequestType<typeof client.posts.$post>['json']
// => { title: string; body: string }
type CreatePostOutput = InferResponseType<typeof client.posts.$post, 201>
// => { ok: boolean; id: number; title: string; body: string }$url() and $path() Utilities
These utilities generate URL objects and path strings from the client, eliminating hardcoded URL strings across your codebase. Renaming a route no longer requires a find-and-replace across every file.
const url = client.posts.$url()
// => URL { href: 'http://localhost:8787/posts' }
const path = client.posts.$path()
// => '/posts'
// With path parameters
const postUrl = client.posts[':id'].$url({ param: { id: '42' } })
// => URL { href: 'http://localhost:8787/posts/42' }Multi-Route Integration — Modular RPC API Design
For larger applications, split routes by domain and merge them into a single `app`. The exported `AppType` encompasses all routes, so the `hc` client gets the full type picture.
// routes/posts.ts
const postsRoute = new Hono()
.get('/', (c) => c.json({ posts: [] }))
.post('/', zValidator('json', postSchema), (c) => {
return c.json({ ok: true }, 201)
})
export type PostsType = typeof postsRoute
// routes/users.ts
const usersRoute = new Hono()
.get('/:id', (c) => c.json({ user: { id: c.req.param('id') } }))
export type UsersType = typeof usersRoute
// app.ts
const app = new Hono()
.route('/posts', postsRoute)
.route('/users', usersRoute)
export type AppType = typeof app
export default appIntegration with Next.js and React
Keep the `hc` client as a singleton to stabilize type inference and avoid repeated instantiation costs. The pattern below works with both Server Components and Client Components in the Next.js App Router.
// lib/client.ts
import { hc } from 'hono/client'
import type { AppType } from '@/server/app'
export const client = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!)
// app/posts/page.tsx (Server Component)
import { client } from '@/lib/client'
export default async function PostsPage() {
const res = await client.posts.$get()
const { posts } = await res.json()
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}Error Handling
Hono's `HTTPException` provides a consistent error contract across the entire API surface. Errors thrown inside route handlers are caught by the global error handler and serialized cleanly.
import { HTTPException } from 'hono/http-exception'
const route = app
.get('/posts/:id', async (c) => {
const post = await db.find(c.req.param('id'))
if (!post) throw new HTTPException(404, { message: 'Not Found' })
return c.json({ post }, 200)
})
app.onError((err, c) => {
if (err instanceof HTTPException) return err.getResponse()
return c.json({ error: 'Internal Server Error' }, 500)
})Type Inference Constraints and Optimization
At scale, TypeScript compile times can increase. The following strategies keep builds fast.
| Strategy | Effect |
|---|---|
| Split routes by domain | Narrows the type inference scope |
| Avoid deeply nested `satisfies` | Reduces type complexity |
| Singleton `hc` client | Eliminates repeated instantiation overhead |
| TypeScript 5.5+ incremental builds | Differential compilation for faster rebuilds |
Migrating from Existing OpenAPI Projects
A gradual migration is the safest path. Use `@hono/zod-openapi` and `@hono/swagger-ui` to maintain an OpenAPI document while introducing Hono RPC for new endpoints. Migrate legacy endpoints incrementally, validating type coverage at each step.
FAQ
Q1. Can Hono RPC fully replace tRPC? A. It depends on your stack. tRPC has a mature Next.js integration and is framework-agnostic. Hono RPC shines when you are already running a Hono server or targeting Edge runtimes — no extra library required. Q2. Does the hc client work in the browser? A. Yes. `hono/client` is built on Web Standards and runs in Node.js, Bun, Deno, and modern browsers without modification. Q3. Can file uploads be handled type-safely? A. Use `zValidator('form', ...)` on the server to validate FormData. The client will infer the correct form shape from that definition. Q4. Does Hono RPC support WebSockets? A. Not currently. Hono RPC targets HTTP request/response pairs. Use Hono's native WebSocket support (`hono/ws`) for real-time communication. Q5. Is Zod mandatory for RPC mode? A. No. `zValidator` is optional. Routes without validators still export a valid `AppType`, though request type precision will be reduced. Q6. Does it work on Cloudflare Workers? A. Fully supported. Hono was built with Cloudflare Workers as a first-class target, and RPC mode works identically at the Edge. Q7. How do I handle multiple microservices with different base URLs? A. Create a separate `hc<ServiceType>(baseUrl)` client per service. Each client has its own isolated type context, preserving full type safety across service boundaries.
How Oflight Can Help
Oflight's engineers specialize in type-safe fullstack architecture using Hono, TypeScript, and modern Edge runtimes. Whether you're starting a greenfield project or migrating a legacy REST API, we'll guide you from design to deployment. Initial consultations are free. Learn more at our Software Development service page.
Feel free to contact us
Contact Us