orpc-guide
oRPC Guide
oRPC is a type-safe RPC framework that combines end-to-end type safety with OpenAPI compliance. It supports procedures, routers, middleware, context injection, error handling, file uploads, streaming (SSE), server actions, and contract-first development across 20+ framework adapters.
Scope: This guide is specifically for the oRPC library (@orpc/* packages). It is not a general RPC/gRPC guide, not for tRPC-only projects (unless migrating to oRPC), and not for generic TypeScript API development without oRPC. For tRPC-to-oRPC migration, see references/contract-first.md.
Quick Start
Install
npm install @orpc/server@latest @orpc/client@latest
For OpenAPI support, also install:
npm install @orpc/openapi@latest
Prerequisites
- Node.js 18+ (20+ recommended) | Bun | Deno | Cloudflare Workers
- TypeScript project with strict mode recommended
- Supports Zod, Valibot, ArkType, and any Standard Schema library
Define Procedures and Router
import { ORPCError, os } from '@orpc/server'
import * as z from 'zod'
const PlanetSchema = z.object({
id: z.number().int().min(1),
name: z.string(),
description: z.string().optional(),
})
export const listPlanet = os
.input(z.object({
limit: z.number().int().min(1).max(100).optional(),
cursor: z.number().int().min(0).default(0),
}))
.handler(async ({ input }) => {
return [{ id: 1, name: 'Earth' }]
})
export const findPlanet = os
.input(PlanetSchema.pick({ id: true }))
.handler(async ({ input }) => {
return { id: 1, name: 'Earth' }
})
export const createPlanet = os
.$context<{ headers: Headers }>()
.use(({ context, next }) => {
const user = parseJWT(context.headers.get('authorization')?.split(' ')[1])
if (user) return next({ context: { user } })
throw new ORPCError('UNAUTHORIZED')
})
.input(PlanetSchema.omit({ id: true }))
.handler(async ({ input, context }) => {
return { id: 1, name: input.name }
})
export const router = {
planet: { list: listPlanet, find: findPlanet, create: createPlanet },
}
Create Server (Node.js)
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'
import { CORSPlugin } from '@orpc/server/plugins'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
plugins: [new CORSPlugin()],
interceptors: [onError((error) => console.error(error))],
})
const server = createServer(async (req, res) => {
const { matched } = await handler.handle(req, res, {
prefix: '/rpc',
context: { headers: new Headers(req.headers as Record<string, string>) },
})
if (!matched) {
res.statusCode = 404
res.end('Not found')
}
})
server.listen(3000)
Create Client
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
url: 'http://127.0.0.1:3000/rpc',
headers: { Authorization: 'Bearer token' },
})
const client: RouterClient<typeof router> = createORPCClient(link)
// Fully typed calls
const planets = await client.planet.list({ limit: 10 })
const planet = await client.planet.find({ id: 1 })
Server-Side Client (No HTTP)
Call procedures directly without HTTP overhead — essential for SSR in Next.js, Nuxt, SvelteKit, etc.
import { call, createRouterClient } from '@orpc/server'
// Single procedure call
const result = await call(router.planet.find, { id: 1 }, { context: {} })
// Router client (multiple procedures)
const serverClient = createRouterClient(router, {
context: async () => ({ headers: await headers() }),
})
const planets = await serverClient.planet.list({ limit: 10 })
Use .callable() for individual procedures:
const getPlanet = os
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => ({ id: input.id }))
.callable({ context: {} })
const result = await getPlanet({ id: '123' })
See references/api-reference.md for full server-side calling patterns.
Core Concepts
Procedure Chain
const example = os
.use(middleware) // Apply middleware
.input(z.object({...})) // Validate input (Zod/Valibot/ArkType)
.output(z.object({...})) // Validate output (recommended for perf)
.handler(async ({ input, context }) => { ... }) // Required
.callable() // Make callable as regular function
.actionable() // Server Action compatibility
Only .handler() is required. All other chain methods are optional.
Router
Routers are plain objects of procedures. They can be nested and support lazy loading:
const router = {
ping: os.handler(async () => 'pong'),
planet: os.lazy(() => import('./planet')), // Code splitting
}
Apply middleware to all procedures in a router:
const router = os.use(authMiddleware).router({ ping, pong })
Middleware
const authMiddleware = os
.$context<{ headers: Headers }>()
.middleware(async ({ context, next }) => {
const user = await getUser(context.headers)
if (!user) throw new ORPCError('UNAUTHORIZED')
return next({ context: { user } })
})
Built-in lifecycle middlewares: onStart, onSuccess, onError, onFinish.
Context
Two types: Initial Context (provided at handler creation) and Execution Context (injected by middleware at runtime). See references/api-reference.md.
Error Handling
// Normal approach
throw new ORPCError('NOT_FOUND', { message: 'Planet not found' })
// Type-safe approach
const base = os.errors({
NOT_FOUND: { message: 'Not found' },
RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },
})
Warning: ORPCError.data is sent to the client. Never include sensitive information.
Event Iterator (SSE/Streaming)
const streaming = os
.output(eventIterator(z.object({ message: z.string() })))
.handler(async function* ({ input, lastEventId }) {
while (true) {
yield { message: 'Hello!' }
await new Promise(r => setTimeout(r, 1000))
}
})
File Upload/Download
const upload = os
.input(z.file())
.handler(async ({ input }) => {
console.log(input.name) // File name
return { success: true }
})
For uploads >100MB, use a dedicated upload solution or extend the body parser.
Built-in Helpers
oRPC provides built-in helpers for common server tasks:
- Cookies:
getCookie,setCookie,deleteCookiefrom@orpc/server/helpers - Cookie signing:
sign,unsignfor tamper-proof cookies - Encryption:
encrypt,decryptfor sensitive data (AES-GCM with PBKDF2) - Rate limiting:
@orpc/experimental-ratelimitwith Memory, Redis, Upstash, and Cloudflare adapters - Event publishing:
@orpc/experimental-publisherfor distributed pub/sub with resume support
See references/helpers.md for full API and examples.
Key Rules and Constraints
- Handler is required -
.handler()is the only required method on a procedure - Output schema recommended - Explicitly specify
.output()for better TypeScript performance - Middleware deduplication - oRPC auto-deduplicates leading middleware; use context guards for manual dedup
- Error data is public - Never put sensitive info in
ORPCError.data - Body parser conflicts - Register framework body parsers AFTER oRPC middleware (Express, Fastify, Elysia)
- RPCHandler vs OpenAPIHandler - RPCHandler uses proprietary protocol (for RPCLink only); OpenAPIHandler is REST/OpenAPI-compatible
- Lazy routers - Use
os.lazy(() => import('./module'))for code splitting; use standalonelazy()for faster type inference - SSE auto-reconnect - Standard SSE clients auto-reconnect; use
lastEventIdto resume streams - File limitations - No chunked/resumable uploads; File/Blob unsupported in AsyncIteratorObject
- React Native - Fetch API has limitations (no File/Blob, no Event Iterator); use
expo/fetchor RPC JSON Serializer workarounds
Handler Setup Pattern
All adapters follow this pattern:
import { RPCHandler } from '@orpc/server/fetch' // or /node, /fastify, etc.
const handler = new RPCHandler(router, {
plugins: [new CORSPlugin()],
interceptors: [onError((error) => console.error(error))],
})
// Handle request with prefix and context
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: {},
})
Client Setup Pattern
import { RPCLink } from '@orpc/client/fetch' // HTTP
import { RPCLink } from '@orpc/client/websocket' // WebSocket
import { RPCLink } from '@orpc/client/message-port' // Message Port
Common Errors
| Error Code | HTTP Status | When |
|---|---|---|
BAD_REQUEST |
400 | Input validation failure |
UNAUTHORIZED |
401 | Missing/invalid auth |
FORBIDDEN |
403 | Insufficient permissions |
NOT_FOUND |
404 | Resource not found |
TIMEOUT |
408 | Request timeout |
TOO_MANY_REQUESTS |
429 | Rate limited |
INTERNAL_SERVER_ERROR |
500 | Unhandled errors |
Non-ORPCError exceptions are automatically converted to INTERNAL_SERVER_ERROR.
Reference Files
- API Reference - Procedures, routers, middleware, context, errors, metadata, event iterators, server actions, file handling
- Adapters - All 20+ framework adapters with setup code (Next.js, Express, Hono, Fastify, WebSocket, Electron, etc.)
- Plugins - All built-in plugins (CORS, batch, retry, compression, CSRF, validation, etc.)
- OpenAPI - OpenAPI spec generation, handler, routing, input/output structure, Scalar UI, OpenAPILink
- Integrations - TanStack Query, React SWR, Pinia Colada, Better Auth, AI SDK, Sentry, Pino, OpenTelemetry
- Advanced - Testing, serialization, TypeScript best practices, publishing clients, body parsing, playgrounds, ecosystem
- Contract-First - Contract-first development, tRPC migration guide, comparison with alternatives
- Helpers - Cookie management, signing, encryption, rate limiting, publisher with event resume
- NestJS - NestJS integration with decorators, dependency injection, contract-first
More from vcode-sh/vibe-tools
tanstack-router-guide
>
10hono-guide
Guide for Hono, an ultrafast web framework built on Web Standards. Use when user asks to "create a Hono app", "build an API with Hono", "add Hono middleware", "deploy Hono to Cloudflare Workers", "use Hono RPC", "add auth to Hono", "validate requests in Hono", "use Hono JSX", or asks about Hono routing, context, streaming, WebSocket, CORS, testing, SSG, or multi-runtime deployment. Do NOT use for Express.js, Fastify, Koa, or Nest.js.
9tanstack-start-guide
>-
9shadcn-guide
Guide for shadcn/ui — the open-code component system for React. Use when user asks to install, configure, theme, or use shadcn/ui components, set up dark mode, create forms, build a registry, configure MCP, or set up RTL/monorepo/JavaScript mode. Covers 58+ components (Base UI and Radix variants), CLI commands, components.json, CSS variable theming, framework installation (Next.js, Vite, Astro, Remix, Laravel, Gatsby, TanStack), registry system, and form patterns. Do NOT use for Chakra UI, Material UI, Ant Design, or vanilla Tailwind without shadcn context.
8tanstack-hotkeys-guide
>-
8base-ui-guide
Guide for Base UI (@base-ui/react), an unstyled React component library for building accessible UIs. Use when user asks to "build a component with Base UI", "create a form with Base UI", "style Base UI components", "animate Base UI popover", "use Base UI dialog", "add Base UI select", "implement Base UI tabs", or asks about Base UI accessibility, composition, customization, styling, animation, TypeScript types, or any of its 35+ components. Covers all components (Dialog, Menu, Popover, Select, Combobox, Tabs, Accordion, Toast, Form, Field, Slider, etc.), styling patterns, animations, composition via render props, event customization, form integration, and utilities. Do NOT use for Radix UI (radix-ui), Material UI (MUI @mui/material), or Shadcn/ui - those are separate libraries with different APIs.
6