DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

tRPC vs Hono RPC (2026): End-to-End Type Safety Without the Setup Tax

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  }
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]) }
)
Enter fullscreen mode Exit fullscreen mode

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)