The problem both tools solve is the same: you write an API in TypeScript and your frontend immediately knows the types — request shape, response shape, errors — without a code generation step, without an OpenAPI schema, without a shared types package you have to keep synchronized manually.
The implementation is very different.
tRPC builds on React Query and has deep Next.js integration. The client is a React Query wrapper — you get caching, background refetching, and optimistic updates automatically. Hono RPC is lighter. It generates a typed fetch client from your route definitions, works with any TypeScript consumer (React, Vue, Svelte, React Native, plain Node.js scripts), and runs on Cloudflare Workers, Bun, and Deno with zero changes.
The Core Model
tRPC: Procedures on a Router
// server/routers/users.ts
export const usersRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().cuid() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input.id } })
if (!user) throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' })
return user
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
}))
.mutation(async ({ input, ctx }) => {
const existing = await db.user.findUnique({ where: { email: input.email } })
if (existing) throw new TRPCError({ code: 'CONFLICT', message: 'Email already in use' })
return db.user.create({ data: { ...input, createdBy: ctx.session.userId } })
}),
})
export type AppRouter = typeof appRouter
The AppRouter type carries the entire API contract to the client.
Hono RPC: Routes as Types
// server/routes/users.ts
const users = new Hono()
.get('/:id', async (c) => {
const user = await db.user.findUnique({ where: { id: c.req.param('id') } })
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json(user)
})
.post(
'/',
requireAuth,
zValidator('json', z.object({ name: z.string().min(1), email: z.string().email() })),
async (c) => {
const body = c.req.valid('json')
const existing = await db.user.findUnique({ where: { email: body.email } })
if (existing) return c.json({ error: 'Email already in use' }, 409)
return c.json(await db.user.create({ data: body }), 201)
}
)
const app = new Hono().route('/users', users)
export type AppType = typeof app
Client Integration
tRPC: React Query Under the Hood
// components/UserList.tsx
export function UserList() {
const utils = trpc.useUtils()
const { data, isLoading } = trpc.users.list.useQuery({ page: 1, limit: 20 })
const createUser = trpc.users.create.useMutation({
onSuccess: () => utils.users.list.invalidate()
})
// Cache invalidation, loading states, background refetch — all free
}
Hono RPC: Typed Fetch, You Handle Caching
// lib/client.ts
export const client = hc<AppType>('http://localhost:3000')
// components/UserList.tsx — React Query wired manually
const { data } = useQuery({
queryKey: ['users', 1],
queryFn: async () => {
const res = await client.users.$get({ query: { page: '1', limit: '20' } })
return res.json() // fully typed
}
})
Non-React Clients
Hono RPC's clear advantage:
// Vue 3
const { data } = await useAsyncData('users', () =>
client.users.$get({ query: { page: '1' } }).then(r => r.json())
)
// Node.js seeding script
const client = hc<AppType>('http://localhost:3000')
await client.users.$post({ json: { name: 'Seed', email: 'seed@example.com' } })
// Server-to-server microservice call
const user = await internalClient.users[':id'].$get({ param: { id } }).then(r => r.json())
// Fully typed response — no manual type assertions
Subscriptions
tRPC wins here — first-class support:
// tRPC — real-time with subscriptions
const notificationsRouter = router({
onNewMessage: publicProcedure
.input(z.object({ roomId: z.string() }))
.subscription(async function* ({ input }) {
for await (const message of subscribeToRoom(input.roomId)) {
yield message
}
})
})
// Client
trpc.notifications.onNewMessage.useSubscription(
{ roomId: 'room-123' },
{ onData: (msg) => setMessages(prev => [...prev, msg]) }
)
Hono handles real-time via SSE separately from the RPC layer.
Decision Framework
| Situation | Choose |
|---|---|
| Next.js app, React-heavy frontend | tRPC |
| Already using React Query | tRPC |
| Need real-time subscriptions | tRPC |
| Non-React clients (Vue, Svelte, React Native) | Hono RPC |
| Edge deployment (Cloudflare Workers, Bun) | Hono RPC |
| Server-to-server API calls | Hono RPC |
| Multi-runtime backend | Hono RPC |
Both are better than maintaining an OpenAPI schema or a shared types package that drifts. The choice is about what already exists in your stack — not which API is more elegant.
Full article at stacknotice.com/blog/trpc-vs-hono-rpc-2026
Top comments (0)