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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "orpc-guide" with this command: npx skills add vcode-sh/vibe-tools/vcode-sh-vibe-tools-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 , deleteCookie from @orpc/server/helpers

  • Cookie signing: sign , unsign for tamper-proof cookies

  • Encryption: encrypt , decrypt for sensitive data (AES-GCM with PBKDF2)

  • Rate limiting: @orpc/experimental-ratelimit with Memory, Redis, Upstash, and Cloudflare adapters

  • Event publishing: @orpc/experimental-publisher for 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 standalone lazy() for faster type inference

  • SSE auto-reconnect - Standard SSE clients auto-reconnect; use lastEventId to 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/fetch or 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

tanstack-router-guide

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

shadcn-guide

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tanstack-hotkeys-guide

No summary provided by upstream source.

Repository SourceNeeds Review