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.

LaneWhat it isInstall shape
Stable v1npm latest tag, verified 1.16.1 on 2026-11-02npm install @orpc/server @orpc/client zod
v2 betaWhat the official docs (updated 2026-11-01) teachnpm 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:

  1. Install the server and client packages plus a schema library.
  2. Define a contract that describes one procedure's input and output.
  3. Build a router that implements the contract's procedures.
  4. Serve the router on Node with the RPC handler.
  5. 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.

AreatRPCoRPC
StyleCode-first onlyCode-first or contract-first
OpenAPINeeds a plugin (trpc-openapi)Built in: full spec plus interactive docs
ErrorsPlain JavaScript errorsTyped ORPCError with declared codes
SchemasCommonly ZodZod, Valibot, ArkType, any Standard Schema lib
ServingRPCRPC 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.

MistakeWhat to do instead
Mixing v1 and v2 beta API shapes in one projectPick one lane: the v2 beta docs' .meta(openapi(...)) or the v1-era .route(...), never both
Copying the docs' @beta install tags into productionUse the beta lane to learn; pin the stable 1.16.1 tag for production
Going code-first on a multi-team APIWrite the contract first with oc, then implement(contract) it
Putting sensitive data in ORPCError.dataKeep error data client-safe; it is exposed to clients
Assuming request batching is onBatching is opt-in via @orpc/server/plugins
Hand-rolling FormData for uploadsUse z.file() — File up, bytes or File back
Writing a second REST client for external consumersGenerate 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.

Featured on Twelve Tools Launch Llama Tools Launch your MCP