Skip to main content

Hono RPC vs tRPC vs ts-rest: Type-Safe APIs 2026

·PkgPulse Team
0

TL;DR

End-to-end type safety between your backend and frontend has become a table-stakes expectation in TypeScript monorepos. tRPC pioneered this space — define your API as TypeScript procedures, get a type-safe client automatically, no schema or code generation needed. Hono RPC extends Hono's ultra-fast HTTP framework with a type-safe client: define routes on the server, export the type, import the client. ts-rest takes the REST-first approach — define a contract (schema-per-route with Zod), implement it on the server, and use the same contract on the client. In 2026, tRPC wins for pure TypeScript monorepos; ts-rest wins when you need REST + OpenAPI; Hono RPC wins for edge deployments with Hono's performance profile.

Key Takeaways

  • tRPC v11: 2.5M weekly downloads, 36K GitHub stars — the dominant choice for full-stack TypeScript, deeply integrated with Next.js, Tanstack Router
  • ts-rest: 250K weekly downloads, 5.4K GitHub stars — defines API contracts with Zod schemas, generates OpenAPI spec, REST-compatible (any HTTP client can consume it)
  • Hono RPC: ships with Hono (2.8M weekly downloads) — zero runtime overhead, ultra-minimal, built for edge runtimes (Cloudflare Workers, Deno Deploy)
  • OpenAPI compatibility: ts-rest generates OpenAPI automatically; tRPC has a trpc-openapi adapter; Hono has @hono/zod-openapi
  • Subscriptions/real-time: tRPC supports WebSocket subscriptions natively; ts-rest and Hono RPC are request/response only
  • Learning curve: tRPC is the easiest for pure TS teams; ts-rest requires understanding contract patterns; Hono RPC requires learning Hono's routing API

The End-to-End Type Safety Problem

Without type-safe API layers, TypeScript teams face a documentation-and-faith system: backend developers write routes and hope frontend developers use them correctly. fetch('/api/users/123') returns any. The response shape is undocumented in code and discovered only at runtime (or in a Swagger page that's usually outdated).

The type-safe API layer pattern solves this: one source of truth for request/response types, shared between server and client, verified by TypeScript at compile time. If you change a response field on the server and the client references it, TypeScript errors before deployment.

tRPC

tRPC defines your API as TypeScript functions (procedures) grouped in a router. The client infers types directly from the router definition — no code generation, no schema files, no build step.

Server Setup

// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()
export const router = t.router
export const publicProcedure = t.procedure

// Procedure with auth middleware
const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' })
  return next({ ctx: { ...ctx, user: ctx.session.user } })
})
// server/routers/users.ts
import { router, publicProcedure, protectedProcedure } from '../trpc'
import { z } from 'zod'

export const usersRouter = router({
  // Query
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      return ctx.db.user.findUnique({ where: { id: input.id } })
    }),

  // Mutation
  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.user.create({ data: input })
    }),

  // Subscription (WebSocket)
  onUpdate: publicProcedure
    .subscription(() => {
      return observable<User>((emit) => {
        const unsubscribe = userUpdateEmitter.on('update', emit.next)
        return () => unsubscribe()
      })
    }),
})

Client Usage

// src/utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '../../server/root'

export const trpc = createTRPCReact<AppRouter>()

// src/components/UserProfile.tsx
import { trpc } from '../utils/trpc'

function UserProfile({ userId }: { userId: string }) {
  // Fully typed — TypeScript knows the return type from server definition
  const { data: user, isLoading } = trpc.users.getById.useQuery({ id: userId })

  const createUser = trpc.users.create.useMutation({
    onSuccess: () => trpc.useUtils().users.getById.invalidate(),
  })

  if (isLoading) return <Spinner />

  return (
    <div>
      <h1>{user?.name}</h1>
      {/* user.email, user.createdAt — all typed */}
    </div>
  )
}

tRPC ships built-in TanStack Query integration — useQuery, useMutation, useInfiniteQuery all work out of the box.

Next.js App Router Integration

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '../../../../server/root'
import { createContext } from '../../../../server/context'

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext,
  })

export { handler as GET, handler as POST }

ts-rest

ts-rest takes a contract-first approach. You define a typed contract (using Zod) that both the server and client implement. The contract is an npm package shared between your frontend and backend — not just a TypeScript type, but a runnable schema that generates OpenAPI specs.

Defining a Contract

// packages/api-contract/src/contract.ts
import { initContract } from '@ts-rest/core'
import { z } from 'zod'

const c = initContract()

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.date(),
})

export const contract = c.router({
  getUser: {
    method: 'GET',
    path: '/users/:id',
    pathParams: z.object({ id: z.string() }),
    responses: {
      200: UserSchema,
      404: z.object({ message: z.string() }),
    },
    summary: 'Get a user by ID',
  },
  createUser: {
    method: 'POST',
    path: '/users',
    body: z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }),
    responses: {
      201: UserSchema,
      409: z.object({ message: z.string() }),
    },
  },
})

Server Implementation

// apps/api/src/users/router.ts
import { createExpressEndpoints, initServer } from '@ts-rest/express'
import { contract } from '@acme/api-contract'

const s = initServer()

const usersRouter = s.router(contract, {
  getUser: async ({ params }) => {
    const user = await db.user.findUnique({ where: { id: params.id } })
    if (!user) return { status: 404, body: { message: 'Not found' } }
    return { status: 200, body: user }
  },
  createUser: async ({ body }) => {
    const existing = await db.user.findUnique({ where: { email: body.email } })
    if (existing) return { status: 409, body: { message: 'Email already exists' } }
    const user = await db.user.create({ data: body })
    return { status: 201, body: user }
  },
})

createExpressEndpoints(contract, usersRouter, app)

TypeScript enforces that every contract route is implemented and that return types match the contract schemas. Returning { status: 200, body: { invalid: 'shape' } } is a compile error.

Client Usage

// apps/web/src/api/client.ts
import { initClient } from '@ts-rest/core'
import { contract } from '@acme/api-contract'

export const client = initClient(contract, {
  baseUrl: 'http://localhost:3000',
  baseHeaders: {},
})

// Usage in component
const result = await client.getUser({ params: { id: '123' } })

if (result.status === 200) {
  console.log(result.body.name)  // typed: string
  console.log(result.body.email) // typed: string
} else if (result.status === 404) {
  console.log(result.body.message)  // typed: string
}

The discriminated union return type (by status code) forces you to handle each response case — no silent any slipping through.

OpenAPI Generation

import { generateOpenApi } from '@ts-rest/open-api'
import { contract } from '@acme/api-contract'

const openApiDocument = generateOpenApi(contract, {
  info: { title: 'My API', version: '1.0.0' },
})

// Serve via Swagger UI or save to file
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument))

This is ts-rest's primary advantage over tRPC — the contract generates a valid OpenAPI spec that external teams, mobile clients, and third parties can consume.

Hono RPC

Hono is a fast, edge-native HTTP framework. Hono RPC extends it with a type-safe client by exporting the router's type and using it to construct a typed fetch client.

Server Definition

// src/server/routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const users = new Hono()
  .get('/:id', async (c) => {
    const id = c.req.param('id')
    const user = await db.user.findUnique({ where: { id } })
    if (!user) return c.json({ message: 'Not found' }, 404)
    return c.json(user, 200)
  })
  .post(
    '/',
    zValidator('json', z.object({
      name: z.string().min(1),
      email: z.string().email(),
    })),
    async (c) => {
      const body = c.req.valid('json')
      const user = await db.user.create({ data: body })
      return c.json(user, 201)
    }
  )

export default users
export type UsersType = typeof users

Client Usage

// src/client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:3000')

// Fully typed
const response = await client.users[':id'].$get({ param: { id: '123' } })
const user = await response.json()  // typed from server return type

// POST
const createResponse = await client.users.$post({
  json: { name: 'Alice', email: 'alice@example.com' }
})
const newUser = await createResponse.json()

Edge Deployment

Hono's primary advantage is deployment anywhere — Cloudflare Workers, Deno Deploy, Bun, Node.js, AWS Lambda Edge — with consistent performance:

// Cloudflare Worker (hono-rpc + edge)
export default {
  fetch: app.fetch,
}

// Deno Deploy
Deno.serve(app.fetch)

// Node.js
serve(app)

No other option in this comparison runs natively on Cloudflare Workers with Hono's 0-overhead request handling.

Feature Comparison

FeaturetRPCts-restHono RPC
Weekly downloads2.5M250K(part of Hono 2.8M)
Type safety mechanismTypeScript inferenceZod contractTypeScript inference
OpenAPI supportVia adapter✅ NativeVia @hono/zod-openapi
Code generation required
WebSocket subscriptionsVia Hono WS
Edge runtime support✅ (limited)✅ Excellent
REST-compatible❌ (custom protocol)
React Query integration✅ Built-inVia @ts-rest/react-queryManual
Status-code typed responsesPartial
Framework agnostic serverHono only

Ecosystem & Community

tRPC has become the standard for type-safe APIs in the Next.js and TanStack ecosystems. The create-t3-app CLI (T3 Stack) includes tRPC as a first-class option alongside NextAuth and Prisma, and T3 Stack has been adopted by hundreds of thousands of developers for building full-stack TypeScript applications. The tRPC Discord is active, the maintainers are responsive, and the documentation has improved substantially since v10. The T3 Oss community around tRPC provides a wealth of real-world usage examples. For the underlying HTTP framework tRPC often runs on, see Hono vs itty-router vs worktop 2026.

ts-rest's community is smaller but specialized. It has attracted teams who need the contract-first approach and particularly teams with external API consumers. The @ts-rest/react-query adapter and the growing list of server adapters (Express, Nest, Fastify, Next.js) make it practical for production use. The maintainers have been consistent about shipping improvements and the roadmap is transparent.

Hono's community is the fastest-growing in this comparison. The framework has attracted significant attention from the Cloudflare Workers community, and the Hono ecosystem now includes middleware for authentication, rate limiting, OpenAPI, WebSockets, and more. Hono's 2.8M weekly downloads include both users of the framework for its HTTP routing capabilities and users of Hono RPC specifically.

Real-World Adoption

tRPC adoption is most visible in the indie developer and startup community using the T3 Stack. Calcom, the open-source Calendly alternative, built their API layer on tRPC. Several Y Combinator startups have publicized their use of tRPC for full-stack type safety. The framework is less commonly adopted in large enterprises — primarily because enterprises often need the OpenAPI documentation and REST compatibility that tRPC doesn't provide natively.

ts-rest has found adoption in teams migrating from REST APIs who want type safety without abandoning REST semantics. It's particularly popular in teams where the backend is consumed by both a web frontend and a mobile app — the OpenAPI output enables generating mobile SDK clients from the same contract definition. Several API-first companies have adopted ts-rest as their internal API standard. For API documentation tooling that works with these type-safe contracts, see Mintlify vs Fern vs ReadMe developer docs 2026.

Hono RPC is popular with teams deploying to Cloudflare Workers, where Hono is the dominant HTTP framework by significant margin. The Workers ecosystem's constraints (no Node.js APIs, size limits, cold start sensitivity) make Hono a natural fit, and using Hono RPC for the client layer is a natural extension for teams already committed to Hono.

Developer Experience Deep Dive

tRPC's developer experience is the most frictionless for TypeScript-first teams. Defining a procedure is 5-10 lines of TypeScript, calling it from a React component is 2 lines, and TypeScript provides full type-checking for the entire call chain. There's no separate step to generate types or update schema files — the types are always derived from the live server code. The TanStack Query integration means you get caching, background refetch, and optimistic updates without any additional configuration.

ts-rest's developer experience requires more upfront investment. Defining contracts is explicit and verbose compared to tRPC's implicit type inference. The trade-off is that the contract becomes a living API specification — it's both documentation and enforcement. For teams with multiple consumers or external stakeholders who need API documentation, this investment pays off. The OpenAPI output integrates with tools like Postman, Insomnia, and API gateway products that accept OpenAPI specs.

Hono RPC's developer experience depends heavily on whether you're already using Hono. For teams already using Hono for its routing and middleware capabilities, adding RPC is a natural extension — export the router type and import it into your client. For teams considering Hono primarily for RPC, the learning curve of Hono's routing API is an additional investment before getting to the type-safe client benefit.

Performance & Benchmarks

tRPC uses a custom protocol over HTTP — requests use batching by default, and the JSON encoding is slightly different from standard REST. The overhead is minimal (under 1ms per request in Node.js), but it means tRPC cannot be consumed by non-tRPC clients without the trpc-openapi adapter. For internal TypeScript-only APIs, this is irrelevant. For externally-exposed APIs, it's a constraint.

ts-rest adds no runtime overhead beyond standard HTTP. The Zod validation at request/response boundaries adds ~0.1-0.5ms per validated payload — the same cost you'd pay with any Zod-based validation. The contract object itself is a pure TypeScript construct with no runtime cost beyond the Zod schemas.

Hono's HTTP handling benchmarks as one of the fastest Node.js/edge frameworks available. The RPC client uses standard fetch under the hood — there's no additional protocol layer. For edge deployments where cold start time and per-request overhead matter, Hono's extremely small runtime footprint (under 30KB) is a meaningful advantage.

When to Choose Each

Choose tRPC if:

  • Your team is TypeScript-first and doesn't need OpenAPI or external API consumers
  • You want the best Next.js/React integration with TanStack Query built in
  • You need WebSocket subscriptions for real-time features
  • DX speed is the priority — tRPC has the fastest path from "define procedure" to "call from client"

Choose ts-rest if:

  • You need OpenAPI generation for mobile clients, third-party consumers, or documentation
  • You want REST semantics (standard HTTP methods and status codes)
  • Your team comes from REST backgrounds and finds tRPC's procedure model unfamiliar
  • You need exhaustive status-code handling at the type level

Choose Hono RPC if:

  • You're deploying to Cloudflare Workers, Deno Deploy, or other edge runtimes
  • Performance overhead is critical (Hono benchmarks as one of the fastest Node.js HTTP frameworks)
  • You already use Hono for its middleware, validation, or edge features
  • You want the smallest possible runtime footprint

Final Verdict 2026

For internal TypeScript APIs consumed only by your own React frontend, tRPC remains the best choice. The zero-friction type safety and TanStack Query integration make it the most productive option for the common case. The T3 Stack ecosystem around tRPC provides extensive tooling and documentation.

For APIs that need external consumers, OpenAPI compatibility, or REST semantics, ts-rest is the right tool. The contract-first model produces better-documented, more maintainable APIs at the cost of more upfront definition work.

For edge deployments or teams already committed to the Hono ecosystem, Hono RPC is the natural choice. The integration between Hono's routing and the type-safe RPC client is clean, and the edge performance story is unmatched.

Methodology

  • npm download data from npmjs.com registry API, March 2026
  • tRPC docs: trpc.io/docs
  • ts-rest docs: ts-rest.com
  • Hono docs: hono.dev
  • Testing with TypeScript 5.4, Node.js 22, Next.js 15

Compare tRPC and related packages on PkgPulse.

Related: TanStack Query v5 vs SWR v3 vs RTK Query 2026 · Hono vs Elysia 2026 · Best WebSocket libraries for Node.js

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.