How to Use oRPC: The Beginner's Guide (First Contract, the Router Lesson, First Server, and the tRPC Switch)
CodexFuse Team · 2026-11-02T00:20:53.736Z · 10 min read min read
What is oRPC, exactly?
oRPC (spelled with a lowercase o) is a TypeScript framework for building APIs as remote procedures with end-to-end type safety. You define each operation once, and both the server and the client derive their types from that single definition, with no code generation step. Think of it as typesafe RPC for TypeScript: every procedure call is checked at compile time. This oRPC tutorial takes the beginner's path: install, contract, router, server, then your first typesafe call.
The core idea is contract-first: a contract describes what each procedure accepts and returns, the server implements it, and the client calls it — and TypeScript checks the whole chain at compile time. If the vocabulary feels new, our what is an API guide covers the basics first.
oRPC is framework-agnostic. The same router runs on Node.js, Bun, Deno, and Cloudflare Workers, and it plugs into Express, Fastify, Next.js, NestJS, and more through adapters. Version 1.0 shipped around January 2026; the stable line was at 1.16.1 when this guide was written, while the official docs (updated November 1, 2026) now track the v2 beta.
How do you install oRPC without picking the wrong version lane?
Pick your version lane first, because the official docs and the stable release are on different tracks right now. The getting-started docs (updated 2026-11-01) install the v2 beta; npm's latest stable tag is 1.16.1. Beginners following the current docs should use the beta-tagged commands below; for production, pin the stable tag instead.
| Lane | What it is | Install shape |
|---|---|---|
| Stable v1 | npm latest tag, verified 1.16.1 on 2026-11-02 | npm install @orpc/server @orpc/client zod |
| v2 beta | What the official docs (updated 2026-11-01) teach | npm install @orpc/server@beta @orpc/client@beta zod |
The verbatim install commands from the official getting-started page are:
npm install @orpc/server@beta @orpc/client@beta zod
pnpm add @orpc/server@beta @orpc/client@beta zod
yarn add @orpc/server@beta @orpc/client@beta zod
bun add @orpc/server@beta @orpc/client@beta zod
The package names are @orpc/server and @orpc/client, plus a schema library — Zod in the guide, but no schema library is mandatory. Zod, Valibot, ArkType, or any Standard Schema library all work, and .input is optional for procedures that take nothing.
Your first 30 minutes with oRPC follow one arc:
- Install the server and client packages plus a schema library.
- Define a contract that describes one procedure's input and output.
- Build a router that implements the contract's procedures.
- Serve the router on Node with the RPC handler.
- Call it from the client and watch the types autocomplete end to end.
What is an oRPC contract, and how do you write your first one?
An oRPC contract is a plain object that declares what each procedure accepts and returns, written with the oc builder from @orpc/contract. It is the single source of truth both sides derive from.
import { oc } from '@orpc/contract'
import * as z from 'zod'
export const contract = {
planet: {
find: oc
.input(z.object({ id: z.number() }))
.output(z.object({ id: z.number(), name: z.string() })),
},
}
On the contract you spell out both sides: .input for what the caller sends, .output for what the server returns. On the server's procedures themselves, you do not need an .output schema — the handler's return type flows through to the client automatically.
How does the oRPC router turn procedures into an API?
An oRPC router is a plain object that groups procedures; the API path is derived from the nesting, so orpc.planet.find({ id: 1 }) on the client reaches the find procedure inside the planet group on the server.
import { os } from '@orpc/server'
import * as z from 'zod'
const findPlanet = os
.input(z.object({ id: z.number() }))
.handler(async ({ input }) => {
return { id: input.id, name: 'Earth' }
})
export const router = {
planet: { find: findPlanet },
}
Middleware attaches once per router with .use, and the context it builds is typed end to end: .use(async ({ context, next }) => next({ context: { user } })). Going contract-first, you implement the contract with implement(contract) from @orpc/server — and a handler whose shape does not match the contract fails to compile, which is the whole point.
How do you serve an oRPC router on Node?
You serve a router with RPCHandler from @orpc/server/node, mounted on a plain Node HTTP server under a prefix like /rpc.
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'
import { router } from './router'
const handler = new RPCHandler(router)
const server = createServer(async (req, res) => {
const { matched } = await handler.handle(req, res, { prefix: '/rpc' })
if (matched) return
res.statusCode = 404
res.end('Not found')
})
server.listen(3000)
The same router is not locked to Node. It runs on Bun and Deno through the Fetch API adapter and on Cloudflare Workers — if you are new to the runtime, our Bun beginner guide gets you started. There is a dedicated Fastify adapter too (verified in the 1.16.1 package exports alongside aws-lambda, websocket, message-port, hibernation, and crossws adapters); Express works through the Node adapter, and our Fastify beginner guide covers that server. Official integrations also cover TanStack Query, SWR, Pinia Colada, Next.js, and NestJS.
How do you call an oRPC API from the client with end-to-end types?
You build the oRPC client with createORPCClient from @orpc/client plus an RPCLink, typed as RouterClient<typeof router> — and from that moment every procedure, input, and return value autocompletes.
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import type { router } from './router'
const link = new RPCLink({
url: 'http://127.0.0.1:3000/rpc',
})
export const orpc: RouterClient<typeof router> = createORPCClient(link)
const planet = await orpc.planet.find({ id: 1 })
// planet is typed { id: number, name: string }
Real JavaScript types survive the wire: Date, BigInt, Set, and Map arrive as themselves, not as strings. File upload needs no manual FormData or base64 juggling — z.file() sends a File up and bytes or a File back. If the caller lives in the same process as the router (a server component, for example), skip HTTP entirely with createRouterClient.
What happens when an oRPC procedure throws?
Errors are typed, not stringly. You declare the error codes a procedure can produce with .errors, throw them with the generated helpers, and the client narrows declared versus undeclared errors with safe() and isDefinedError.
const findPlanet = os
.input(z.object({ id: z.number() }))
.errors({
NOT_FOUND: { message: 'Planet not found' },
RATE_LIMIT: { data: z.object({ retryAfter: z.number() }) },
})
.handler(async ({ input, errors }) => {
throw errors.NOT_FOUND()
})
On the client, safe() plus isDefinedError(error) from @orpc/client tells you whether the failure was one you declared. One security rule from the WorkOS engineering writeup: never put sensitive data in ORPCError.data — it is exposed to clients.
How do you get an OpenAPI document out of an oRPC router?
The oRPC OpenAPI flow starts on the procedure itself. In the current (v2 beta) docs, you annotate a procedure with .meta(openapi({ method: 'GET', path: '/planets/{id}' })), then feed the router to OpenAPIGenerator with a Zod-to-JSON-Schema converter — and out comes a full OpenAPI spec with Scalar or Swagger UI plugins.
import { openapi } from '@orpc/openapi'
import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod'
const findPlanet = os
.meta(openapi({ method: 'GET', path: '/planets/{id}' }))
.input(z.object({ id: z.number() }))
.handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))
const generator = new OpenAPIGenerator({
converters: [new ZodToJsonSchemaConverter()],
})
const spec = await generator.generate(router, {
base: { info: { title: 'Planets API', version: '1.0.0' } },
})
Version-lane note: v1-era articles (LogRocket, March 2026) show the older .route({ method, path }) meta with OpenAPIHandler and ZodSmartCoercionPlugin — do not mix that shape with the v2 beta .meta(openapi(...)) shape above. The same router can also serve AI agents directly as MCP tools, and for poking at the HTTP endpoints by hand, our Postman beginner guide and the REST API tester tool both work against the served prefix.
Coming from tRPC: what actually changes?
oRPC is often called tRPC's younger sibling — same router-and-procedure structure, same end-to-end TypeScript safety, no codegen; the two big names in the typesafe RPC TypeScript space — with the differences concentrated in five places.
| Area | tRPC | oRPC |
|---|---|---|
| Style | Code-first only | Code-first or contract-first |
| OpenAPI | Needs a plugin (trpc-openapi) | Built in: full spec plus interactive docs |
| Errors | Plain JavaScript errors | Typed ORPCError with declared codes |
| Schemas | Commonly Zod | Zod, Valibot, ArkType, any Standard Schema lib |
| Serving | RPC | RPC and REST from one instance |
The practical verdict from the LogRocket comparison: tRPC fits an all-TypeScript single team that wants the simplest setup for internal APIs. oRPC fits teams that need OpenAPI docs or SDKs, non-TypeScript consumers (mobile apps, Python or Go services, external partners), contract-first collaboration, or APIs that may one day go public. Treat published performance numbers as third-party claims, not facts — one launch writeup claims faster type-checking and runtime versus tRPC, and LogRocket cites rough per-process request figures for both; neither is independently verified here.
Seven mistakes every oRPC beginner makes
These come straight from the version-lane split and the API surface above.
| Mistake | What to do instead |
|---|---|
| Mixing v1 and v2 beta API shapes in one project | Pick one lane: the v2 beta docs' .meta(openapi(...)) or the v1-era .route(...), never both |
| Copying the docs' @beta install tags into production | Use the beta lane to learn; pin the stable 1.16.1 tag for production |
| Going code-first on a multi-team API | Write the contract first with oc, then implement(contract) it |
| Putting sensitive data in ORPCError.data | Keep error data client-safe; it is exposed to clients |
| Assuming request batching is on | Batching is opt-in via @orpc/server/plugins |
| Hand-rolling FormData for uploads | Use z.file() — File up, bytes or File back |
| Writing a second REST client for external consumers | Generate the OpenAPI spec from the same router instead |
Sources
oRPC official getting-started docs (orpc.dev, updated 2026-11-01) — install commands, procedure, router, RPCHandler, and client patterns. oRPC homepage (orpc.dev) — v2 beta banner, adapter and integration list, file upload, MCP support. oRPC Next.js adapter docs (orpc.dev/docs/adapters/next). npm registry — @orpc/server and @orpc/client latest stable 1.16.1, checked 2026-11-02. newreleases.io — @orpc/openapi 2.0.0-beta.47 published 2026-10-27. LogRocket, "tRPC vs oRPC: Type-safe RPC" by Temitope Oyedele (March 2026) — v1-era comparison, code samples, and verdict. WorkOS engineering blog, "oRPC: OpenAPI + RPC for type-safe APIs" (April 2025) — contract-first walkthrough and the ORPCError.data security note. DeepWiki, better-cf-alchemy fullstack reference — oRPC router layout with Hono and TanStack Query. europesays.com — v1.0 launch coverage (release timing approximate).
Frequently Asked Questions
Is oRPC a replacement for REST?
Not exactly — it is an alternative way to define the same client-server contract, and it can even emit OpenAPI REST docs from the same router. Use oRPC procedures where end-to-end TypeScript safety helps, and lean on the generated OpenAPI spec wherever REST-shaped docs or non-TypeScript consumers are needed.
Do I need Zod to use oRPC?
No. The official guide uses Zod, but any Standard Schema library works — Valibot, ArkType, and others — and .input is optional for procedures that take no arguments. You only need a schema-to-JSON-Schema converter (like @orpc/zod) when you generate an OpenAPI spec.
Can I use oRPC with an existing Express, Fastify, or Next.js app?
Yes. The same router mounts on Express through the Node adapter, on Fastify through its dedicated adapter, and on Next.js through the App and Pages Router adapters. You do not rewrite the app — you mount the handler at a prefix like /rpc.
oRPC vs tRPC — which should a beginner learn first?
Learn tRPC first if your whole stack is TypeScript and the API stays internal — it is the simpler setup. Learn oRPC first if you want OpenAPI docs, typed errors, contract-first collaboration, or consumers outside TypeScript. The router-and-procedure mental model transfers between the two.
Is oRPC production-ready in 2026?
The stable v1 line (1.16.1 at the time of writing) is the production lane, while the official docs currently teach the v2 beta API — so pin your version and do not mix the two lanes in one project. The framework-agnostic design (Node, Bun, Deno, Workers, Lambda, WebSockets) means you are not betting on a single runtime either.