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 integration —
useTRPCQuerynow wraps TanStack Query v5 - oRPC generates OpenAPI 3.1 spec — documentation and SDK generation tRPC can't do
- Hono RPC's typed client
hcworks 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
| Feature | tRPC v11 | oRPC | Hono RPC |
|---|---|---|---|
| OpenAPI output | ❌ | ✅ Native | ❌ |
| React Query integration | ✅ First-class | ✅ | Manual |
| Edge runtime | ✅ | ✅ | ✅ Native |
| Schema libraries | Zod (primary) | Zod, Valibot, ArkType | Zod (validator) |
| Subscriptions/WebSocket | ✅ | Partial | Via Hono WS |
| File upload | Plugin | Plugin | ✅ Native |
| Middleware | ✅ Typed | ✅ Typed | ✅ |
| REST routes mix | ❌ (RPC only) | ✅ | ✅ Natural |
| Bundle size | Medium | Small | ✅ Very small |
| GitHub stars | 35k | 2k | 20k (Hono) |
| Next.js integration | ✅ Official | ✅ | Via Hono Next.js |
| Documentation | ✅ Excellent | Good | ✅ 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
hcclient 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