Loading...
Loading...
Guide for oRPC — a type-safe RPC framework combining end-to-end type safety with OpenAPI compliance. Use when user explicitly mentions "oRPC" or asks to "create oRPC procedures", "set up oRPC server", "configure oRPC client", "add oRPC middleware", "define oRPC router", "use oRPC with Next.js/Express/Hono/Fastify", "generate OpenAPI spec with oRPC", "integrate oRPC with TanStack Query", "stream with oRPC", "handle errors in oRPC", "set up oRPC contract-first", "migrate from tRPC to oRPC", or asks about oRPC procedures, routers, middleware, context, plugins, adapters, or server actions. Covers server setup, client creation, middleware chains, error handling, OpenAPI generation, file uploads, event iterators (SSE/streaming), server actions, contract-first development, and framework adapter integrations. Do NOT use for generic RPC/gRPC questions, tRPC-only questions (without migration context), or general TypeScript API development without oRPC.
npx skill4agent add vcode-sh/vibe-tools orpc-guide@orpc/*references/contract-first.mdnpm install @orpc/server@latest @orpc/client@latestnpm install @orpc/openapi@latestimport { 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 },
}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)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 })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 }).callable()const getPlanet = os
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => ({ id: input.id }))
.callable({ context: {} })
const result = await getPlanet({ id: '123' })references/api-reference.mdconst 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.handler()const router = {
ping: os.handler(async () => 'pong'),
planet: os.lazy(() => import('./planet')), // Code splitting
}const router = os.use(authMiddleware).router({ ping, pong })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 } })
})onStartonSuccessonErroronFinishreferences/api-reference.md// 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() }) },
})ORPCError.dataconst 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))
}
})const upload = os
.input(z.file())
.handler(async ({ input }) => {
console.log(input.name) // File name
return { success: true }
})getCookiesetCookiedeleteCookie@orpc/server/helperssignunsignencryptdecrypt@orpc/experimental-ratelimit@orpc/experimental-publisherreferences/helpers.md.handler().output()ORPCError.dataos.lazy(() => import('./module'))lazy()lastEventIdexpo/fetchimport { 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: {},
})import { RPCLink } from '@orpc/client/fetch' // HTTP
import { RPCLink } from '@orpc/client/websocket' // WebSocket
import { RPCLink } from '@orpc/client/message-port' // Message Port| Error Code | HTTP Status | When |
|---|---|---|
| 400 | Input validation failure |
| 401 | Missing/invalid auth |
| 403 | Insufficient permissions |
| 404 | Resource not found |
| 408 | Request timeout |
| 429 | Rate limited |
| 500 | Unhandled errors |
INTERNAL_SERVER_ERROR