Skip to main content

Guide

oRPC vs tRPC v11 vs Hono RPC (2026)

oRPC vs tRPC v11 vs Hono RPC compared for type-safe end-to-end APIs in TypeScript. OpenAPI output, edge runtime support, React Query integration, and.

·PkgPulse Team·
0

oRPC vs tRPC v11 vs Hono RPC: Type-Safe APIs in TypeScript 2026

TL;DR

Type-safe RPC frameworks eliminate the manual contract between your TypeScript backend and frontend — the client knows the exact types of every procedure. tRPC v11 is the established leader — deep React Query integration, huge ecosystem of adapters, and the standard choice for Next.js fullstack apps. oRPC is the modern challenger — brings OpenAPI output (tRPC can't), standard schema support (Zod, Valibot, ArkType), and OpenAPI-first design while keeping tRPC-like DX. Hono RPC is the edge-native option — the hc typed client works in Cloudflare Workers, Vercel Edge Functions, and anywhere Hono runs; no overhead for lightweight edge APIs. For Next.js fullstack apps with React Query: tRPC v11. For APIs that need OpenAPI spec output: oRPC. For edge-first APIs on Cloudflare Workers: Hono RPC.

Key Takeaways

  • tRPC v11 released with TanStack Query integrationuseTRPCQuery now wraps TanStack Query v5
  • oRPC generates OpenAPI 3.1 spec — documentation and SDK generation tRPC can't do
  • Hono RPC's typed client hc works in browser, Node.js, and edge runtimes
  • oRPC supports standard schemas — Zod, Valibot, ArkType all work (not Zod-only like tRPC v10)
  • tRPC GitHub stars: 35k — the most adopted type-safe RPC framework
  • Hono's app.route() for RPC composes cleanly with REST routes in the same server
  • oRPC supports middleware with type-safe context — very similar to tRPC middleware patterns

The Type-Safe API Problem

REST APIs without contracts require manual synchronization:

// Without type-safe RPC — fragile
// Backend changes return type
// Frontend doesn't know — silent runtime error

// api/users.ts (backend)
export async function getUser(id: string) {
  return { id, name: "Alice", email: "alice@example.com" }; // Added email
}

// frontend/UserCard.tsx — doesn't know email was added
const user = await fetch(`/api/users/${id}`).then(r => r.json());
// user is 'any' — TypeScript can't help

Type-safe RPC solutions:

// With tRPC — end-to-end types
// Backend return type automatically inferred on client
const user = await trpc.user.get.query({ id }); // user.email is typed

tRPC v11: The Established Standard

tRPC v11 brings first-class TanStack Query v5 integration and server-side calling patterns for Next.js App Router.

Installation

npm install @trpc/server @trpc/client @trpc/react-query @trpc/next
npm install @tanstack/react-query zod

Server Setup

// server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { z } from "zod";

// Context — available in all procedures
export type Context = {
  userId: string | null;
  db: Database;
};

export async function createContext(opts: CreateNextContextOptions): Promise<Context> {
  const token = opts.req.headers.authorization?.split(" ")[1];
  const userId = token ? await verifyJwt(token) : null;
  return { userId, db: getDatabase() };
}

const t = initTRPC.context<Context>().create({
  transformer: superjson,  // Supports Date, Map, Set in serialization
});

// Reusable middleware
const isAuthenticated = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { ...ctx, userId: ctx.userId } });  // Narrows userId to string
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthenticated);

Router and Procedures

// server/routers/users.ts
import { router, publicProcedure, protectedProcedure } from "../trpc";
import { z } from "zod";

export const usersRouter = router({
  // Public procedure
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const user = await ctx.db.users.findById(input.id);
      if (!user) throw new TRPCError({ code: "NOT_FOUND" });
      return user;
    }),

  // Protected procedure — ctx.userId is string (not null)
  updateProfile: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(100),
      bio: z.string().max(500).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.users.update(ctx.userId, input);
    }),

  // Subscription
  onNewMessage: protectedProcedure
    .subscription(({ ctx }) => {
      return observable<Message>((emit) => {
        const unsubscribe = subscribeToMessages(ctx.userId, (message) => {
          emit.next(message);
        });
        return unsubscribe;
      });
    }),
});

// Root router
export const appRouter = router({
  users: usersRouter,
  posts: postsRouter,
});

export type AppRouter = typeof appRouter;

React Client

// app/providers.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createTRPCReact } from "@trpc/react-query";
import { httpBatchLink } from "@trpc/client";
import superjson from "superjson";
import type { AppRouter } from "@/server/routers";

export const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient();
  const trpcClient = trpc.createClient({
    links: [httpBatchLink({ url: "/api/trpc", transformer: superjson })],
  });

  return (
    <QueryClientProvider client={queryClient}>
      <trpc.Provider client={trpcClient} queryClient={queryClient}>
        {children}
      </trpc.Provider>
    </QueryClientProvider>
  );
}

// app/users/[id]/page.tsx
function UserProfile({ id }: { id: string }) {
  const { data: user, isLoading } = trpc.users.getById.useQuery({ id });

  if (isLoading) return <Spinner />;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

oRPC: OpenAPI-First Type-Safe RPC

oRPC (OpenRPC) is a newer framework that adds OpenAPI spec generation to the tRPC-style DX. It supports multiple schema libraries and generates documentation automatically.

Installation

npm install @orpc/server @orpc/client @orpc/react-query
npm install zod  # Or valibot, arktype

Server Setup

// server/orpc.ts
import { os } from "@orpc/server";
import { z } from "zod";

// Create a base procedure
export const pub = os;  // Public procedures

// Middleware for authentication
export const authed = os.use(async ({ context, next }, input) => {
  const userId = await getAuthenticatedUserId(context.req);
  if (!userId) throw new ORPCError({ code: "UNAUTHORIZED" });
  return next({ context: { ...context, userId } });
});

Procedures with OpenAPI Metadata

// server/routers/users.ts
import { os } from "@orpc/server";
import { z } from "zod";

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

export const usersRouter = {
  getById: pub
    .input(z.object({ id: z.string().describe("User ID") }))
    .output(UserSchema)
    // OpenAPI metadata — generates documentation automatically
    .route({ method: "GET", path: "/users/{id}", tags: ["Users"] })
    .handler(async ({ input, context }) => {
      const user = await db.users.findById(input.id);
      if (!user) throw new ORPCError({ code: "NOT_FOUND" });
      return user;
    }),

  create: authed
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }))
    .output(UserSchema)
    .route({ method: "POST", path: "/users", tags: ["Users"] })
    .handler(async ({ input, context }) => {
      return db.users.create({ ...input, createdAt: new Date() });
    }),
};

// Root router
export const appRouter = {
  users: usersRouter,
  posts: postsRouter,
};

export type AppRouter = typeof appRouter;

OpenAPI Spec Generation

// This is oRPC's killer feature — tRPC cannot do this
import { OpenAPIGenerator } from "@orpc/openapi";
import { appRouter } from "./routers";

const generator = new OpenAPIGenerator({
  info: {
    title: "My API",
    version: "1.0.0",
    description: "Auto-generated from oRPC router definitions",
  },
  servers: [{ url: "https://api.example.com" }],
});

const spec = generator.generate(appRouter);
// spec is a full OpenAPI 3.1 JSON document

// Serve as /api/openapi.json
app.get("/api/openapi.json", (req, res) => {
  res.json(spec);
});

// Use with Scalar UI for interactive docs
import { ApiReference } from "@scalar/express-api-reference";
app.use("/api/docs", ApiReference({ url: "/api/openapi.json" }));

React Client (same as tRPC)

import { createORPCReactQueryUtils } from "@orpc/react-query";
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
import type { AppRouter } from "@/server/routers";

const client = createORPCClient<AppRouter>({
  baseURL: "/api/orpc",
});

const orpc = createTanstackQueryUtils(client);

function UserProfile({ id }: { id: string }) {
  const { data: user } = useQuery(orpc.users.getById.queryOptions({ id }));
  return <div>{user?.name}</div>;
}

Hono RPC: Edge-Native Typed Client

Hono's RPC feature (hc) generates a fully typed client from your Hono routes. It's the lightest option and runs natively in Cloudflare Workers.

Installation

npm install hono

Server with Typed Routes

// server/app.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

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

const app = new Hono()
  .get("/users/:id",
    async (c) => {
      const id = c.req.param("id");
      const user = await getUser(id);
      return c.json(user satisfies z.infer<typeof UserSchema>);
    }
  )
  .post("/users",
    zValidator("json", z.object({
      name: z.string().min(1),
      email: z.string().email(),
    })),
    async (c) => {
      const body = c.req.valid("json");  // Fully typed
      const user = await createUser(body);
      return c.json(user, 201);
    }
  )
  .delete("/users/:id", async (c) => {
    await deleteUser(c.req.param("id"));
    return c.json({ success: true });
  });

export type AppType = typeof app;
export default app;

Typed Client with hc

// client/api.ts
import { hc } from "hono/client";
import type { AppType } from "@/server/app";

// Create fully typed client
const client = hc<AppType>("https://api.example.com");

// All calls are typed — TypeScript knows the response shape
const response = await client.users[":id"].$get({ param: { id: "123" } });
const user = await response.json();
// user is typed: { id: string, name: string, email: string }

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

Using Hono RPC in React

import { useQuery, useMutation } from "@tanstack/react-query";
import { client } from "@/client/api";

function UserProfile({ id }: { id: string }) {
  const { data: user } = useQuery({
    queryKey: ["user", id],
    queryFn: async () => {
      const res = await client.users[":id"].$get({ param: { id } });
      return res.json();
    },
  });

  return <div>{user?.name}</div>;
}

Feature Comparison

FeaturetRPC v11oRPCHono RPC
OpenAPI output✅ Native
React Query integration✅ First-classManual
Edge runtime✅ Native
Schema librariesZod (primary)Zod, Valibot, ArkTypeZod (validator)
Subscriptions/WebSocketPartialVia Hono WS
File uploadPluginPlugin✅ Native
Middleware✅ Typed✅ Typed
REST routes mix❌ (RPC only)✅ Natural
Bundle sizeMediumSmall✅ Very small
GitHub stars35k2k20k (Hono)
Next.js integration✅ OfficialVia Hono Next.js
Documentation✅ ExcellentGood✅ Excellent

When to Use Each

Choose tRPC v11 if:

  • You're building a Next.js fullstack app and want the most mature ecosystem
  • TanStack Query integration for caching and optimistic updates is important
  • You're already familiar with tRPC and want the proven path
  • Real-time subscriptions (WebSocket) are in your feature set

Choose oRPC if:

  • You need to expose an OpenAPI spec for external consumers (mobile apps, third-party integrations)
  • Multiple schema libraries (not just Zod) need to work together in your codebase
  • Auto-generated API documentation from router definitions is a priority
  • You're building a public API that other developers will consume

Choose Hono RPC if:

  • You're deploying to Cloudflare Workers, Vercel Edge, or Deno Deploy
  • You need typed REST routes alongside your RPC procedures in the same server
  • Bundle size matters — Hono's hc client is tiny
  • You're already using Hono for your backend and want to add type safety

Testing Type-Safe RPC Endpoints

Testing tRPC procedures, oRPC handlers, and Hono routes each requires a slightly different approach. tRPC's createCallerFactory enables unit testing procedures without HTTP overhead — you call procedures as regular TypeScript functions, passing mock context, and assert on the return value. This makes procedure-level unit tests fast and isolated from network concerns. For integration testing the full HTTP layer, use supertest against the tRPC fetch handler with realistic JSON-RPC payloads. oRPC's handler functions are similarly callable directly in tests without going through HTTP — the .handler() function receives { input, context } and returns the typed output, making it straightforward to test with Vitest or Jest. Hono routes can be tested using Hono's built-in app.request() method, which invokes the route handler without starting a real HTTP server — similar in concept to Supertest but Hono-native. For all three frameworks, mock your database and external service dependencies using MSW or vi.mock() to isolate the RPC layer from infrastructure concerns in your test suite.

Monorepo Architecture and Type Sharing

The core value proposition of all three frameworks — end-to-end TypeScript types — requires that your frontend and backend share type definitions. In a monorepo setup, this is natural: the AppRouter type from tRPC or the appRouter object from oRPC lives in a backend package, and the frontend package imports it for type inference. The critical constraint is that the type-only import must not pull in server-side dependencies (database clients, secret keys, heavy node_modules) into your browser bundle. tRPC achieves this through import type { AppRouter } from '@/server/router' — a type-only import that TypeScript erases at compile time, preventing any server code from reaching the client bundle. oRPC follows the same pattern. Hono RPC's AppType export from your server file is similarly a type-only import. In non-monorepo setups (separate frontend and backend repositories), you can publish a shared types package to npm (or use a local npm link) that contains only the router type definition — no implementation code. Verify that your bundler correctly tree-shakes type-only imports by inspecting the client bundle for any server-side module names.

Versioning and Backward Compatibility

Type-safe RPC frameworks share a challenge with all strongly-typed APIs: changing your procedure signatures or contract definitions is a breaking change for all clients. Unlike REST APIs where you can version by URL prefix (/v2/users), tRPC's procedure-level naming and oRPC's route definitions change for all clients simultaneously. For internal APIs in a monorepo where client and server deploy together, this is not a problem — you update both sides atomically. For APIs consumed by mobile apps or third-party clients that cannot be updated simultaneously, you need an explicit versioning strategy. The cleanest tRPC approach is to namespace procedures by version (v1.users.getById, v2.users.getById) and maintain both until all clients migrate. oRPC's OpenAPI-first design actually helps here — you can generate two OpenAPI specs from two router versions and use standard API gateway versioning patterns. Hono's app.route('/v2', v2App) composition makes version routing natural alongside the typed client. Plan for API versioning from the beginning if any external consumers exist; retrofitting it after multiple client versions are in the wild is significantly more painful.

Edge Runtime Compatibility and Cold Start Behavior

Edge runtime compatibility has become a first-class concern as Next.js App Router and Cloudflare Workers deployments grow. tRPC's fetch adapter works in edge runtimes — it uses the Web Fetch API rather than Node.js's http module — but middleware and context functions must avoid Node.js-specific APIs (filesystem access, process.env in the Cloudflare Workers environment, non-edge-compatible npm packages). oRPC is designed with edge compatibility in mind and avoids Node.js APIs in its core. Hono was built for edge runtimes from the beginning and is the most naturally compatible, running identically in Cloudflare Workers, Vercel Edge Functions, Deno Deploy, and Bun. Cold start performance matters for edge-deployed APIs: Hono's minimal bundle size (under 15 kB) means nearly instantaneous cold starts. tRPC with its React Query integration and superjson transformer adds more to the server bundle. For latency-sensitive applications where even 50 ms of cold start matters (auto-complete APIs, real-time search), Hono's edge-native architecture and tiny bundle are meaningful advantages over the heavier tRPC server setup.

Methodology

Data sourced from GitHub repositories (star counts as of February 2026), official tRPC, oRPC, and Hono documentation, npm weekly download statistics (January 2026), and community discussions from the tRPC Discord, Hono Discord, and TypeScript Weekly newsletter. oRPC comparison data from official oRPC documentation comparison pages.


Related: tRPC v11 vs ts-rest for a deeper tRPC vs contract-first comparison, or Hono vs Elysia vs Nitro for the full Hono ecosystem context.

See also: Fastify vs Hono and Express vs Hono

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.