# oRPC
> Build APIs that are typesafe end to end, with OpenAPI included
# Astro Adapter
Source: https://orpc.dev/docs/adapters/astro
[Astro](https://astro.build/) is a JavaScript web framework optimized for building fast, content-driven websites. Its endpoints follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Basic
```ts title="src/pages/rpc/[...rest].ts"
import type { APIRoute } from 'astro'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export const prerender = false
export const ALL: APIRoute = async ({ request }) => {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
}
```
:::info
`prerender = false` makes this an on-demand route, so building your site requires an [adapter](https://docs.astro.build/en/guides/on-demand-rendering/) such as `@astrojs/node`.
:::
:::warning
Astro's [CSRF protection](https://docs.astro.build/en/reference/configuration-reference/#securitycheckorigin) can reject oRPC requests from non-browser or cross-origin clients, such as file uploads, with a `403` response before the route runs.
:::
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
## Optimize SSR
To reduce HTTP requests and improve latency during SSR, you can use a [server-side client](/docs/client/server-side) during SSR. Below is a quick setup, see [Optimizing SSR](/docs/recipes/optimizing-ssr) for more details.
```ts title="src/lib/orpc.ts"
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
if (import.meta.env.SSR) {
await import('./orpc.server')
}
declare global {
var $client: RouterClient | undefined
}
const link = new RPCLink({
url: '/rpc',
origin: () => {
if (typeof window === 'undefined') {
throw new Error('This link is not allowed on the server side.')
}
return window.location.origin
},
})
/**
* Fall back to a browser client when no SSR client is registered.
*/
export const client: RouterClient = globalThis.$client ?? createORPCClient(link)
```
```ts title="src/lib/orpc.server.ts"
import { createRouterClient } from '@orpc/server'
globalThis.$client = createRouterClient(router, {
context: {} // Provide initial context if needed
})
```
---
# AWS Lambda Adapter
Source: https://orpc.dev/docs/adapters/aws-lambda
:::warning
This adapter requires the Lambda Node.js runtime with [response streaming](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html) enabled, so handlers must be wrapped with `awslambda.streamifyResponse`.
:::
## Server Usage
```ts RPC
import type { APIGatewayProxyEventV2, AwsLambdaGlobal } from '@standard-server/aws-lambda'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/aws-lambda'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
declare const awslambda: AwsLambdaGlobal
const handler = new RPCHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export const rpc = awslambda.streamifyResponse(async (event, responseStream, context) => {
const { matched } = await handler.handle(event, responseStream, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return
}
awslambda.HttpResponseStream.from(responseStream, {
statusCode: 404,
headers: {},
cookies: [],
}).end('Not found')
})
```
```ts OpenAPI
import type { APIGatewayProxyEventV2, AwsLambdaGlobal } from '@standard-server/aws-lambda'
import { OpenAPIHandler } from '@orpc/openapi/aws-lambda'
import { onError } from '@orpc/server'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
declare const awslambda: AwsLambdaGlobal
const handler = new OpenAPIHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export const api = awslambda.streamifyResponse(async (event, responseStream, context) => {
const { matched } = await handler.handle(event, responseStream, {
prefix: '/api',
context: {} // Provide initial context if needed
})
if (matched) {
return
}
awslambda.HttpResponseStream.from(responseStream, {
statusCode: 404,
headers: {},
cookies: [],
}).end('Not found')
})
```
:::warning
To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios,
extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standard-server#how-body-parsing-works). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`:
```ts
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})
```
:::
## Event Stream Options
You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler.
```ts
const handler = new OpenAPIHandler(router, {
sendStandardResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 15000
*/
interval: 15000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
})
```
---
# Browser Adapter
Source: https://orpc.dev/docs/adapters/browser
Enable typesafe communication between browser scripts using the [Message Port Adapter](/docs/adapters/message-port).
## Between Extension Scripts
To set up communication between scripts in a browser extension (e.g. background, content, popup), configure one script to listen for connections and upgrade them, and another to initiate the connection.
:::warning
The browser extension [Message Passing API](https://developer.chrome.com/docs/extensions/develop/concepts/messaging) does not support transferring binary data, which means oRPC features like `File` and `Blob` cannot be used natively. However, you can temporarily work around this limitation by extending the [RPC Serializer](/docs/rpc/serializer) to encode binary data as `base64`.
:::
```ts server
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/message-port'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
browser.runtime.onConnect.addListener((port) => {
handler.upgrade(port, {
context: {} // Provide initial context if needed
})
})
```
```ts client
import { RPCLink } from '@orpc/client/message-port'
const port = browser.runtime.connect()
const link = new RPCLink({
port,
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
## Window to Window
To enable communication between two window contexts (e.g. parent and popup), one must listen and upgrade the port, and the other must initiate the connection.
```ts opener
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/message-port'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
window.addEventListener('message', (event) => {
if (event.data instanceof MessagePort) {
handler.upgrade(event.data, {
context: {} // Provide initial context if needed
})
event.data.start()
}
})
window.open('/example/popup', 'popup', 'width=680,height=520')
```
```ts popup
import { RPCLink } from '@orpc/client/message-port'
const { port1: serverPort, port2: clientPort } = new MessageChannel()
window.opener.postMessage(serverPort, '*', [serverPort])
const link = new RPCLink({
port: clientPort,
})
clientPort.start()
```
## Advanced Relay Pattern
In some advanced cases, direct communication between scripts isn't possible. For example, a content script running in the ["MAIN" world](https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts#world-timings) cannot directly communicate with the background script using `browser.runtime` or `chrome.runtime` APIs.
To work around this, you can use a **relay pattern**: an additional content script running in the **"ISOLATED" (default) world** relays messages between the two contexts, enabling communication where direct access is restricted.
```ts relay
window.addEventListener('message', (event) => {
if (event.data instanceof MessagePort) {
const port = browser.runtime.connect()
// Relay `message` and `close/disconnect` events between the MessagePort and runtime.Port
event.data.addEventListener('message', (event) => {
port.postMessage(event.data)
})
event.data.addEventListener('close', () => {
port.disconnect()
})
port.onMessage.addListener((message) => {
event.data.postMessage(message)
})
port.onDisconnect.addListener(() => {
event.data.close()
})
event.data.start()
}
})
```
```ts server
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/message-port'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
browser.runtime.onConnect.addListener((port) => {
handler.upgrade(port, {
context: {} // Provide initial context if needed
})
})
```
```ts client
import { RPCLink } from '@orpc/client/message-port'
const { port1: serverPort, port2: clientPort } = new MessageChannel()
window.postMessage(serverPort, '*', [serverPort])
const link = new RPCLink({
port: clientPort,
})
clientPort.start()
```
---
# Cloudflare Workers Adapter
Source: https://orpc.dev/docs/adapters/cloudflare-workers
[Cloudflare Workers](https://developers.cloudflare.com/workers/) follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Basic
```ts RPC
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
const handler = new RPCHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export default {
async fetch(request, env, ctx) {
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: { env, ctx } // Provide initial context if needed
})
if (matched) {
return response
}
return new Response('Not found', { status: 404 })
},
} satisfies ExportedHandler
```
```ts OpenAPI
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { onError } from '@orpc/server'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
const handler = new OpenAPIHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export default {
async fetch(request, env, ctx) {
const { matched, response } = await handler.handle(request, {
prefix: '/api',
context: { env, ctx } // Provide initial context if needed
})
if (matched) {
return response
}
return new Response('Not found', { status: 404 })
},
} satisfies ExportedHandler
```
## Compatibility Flags
oRPC forwards `request.signal` to every procedure call so handlers can stop work when the client disconnects. Workers only abort that signal when the [`enable_request_signal`](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-requestsignal-for-incoming-requests) flag is set, so add it to your Wrangler configuration:
```jsonc title="wrangler.jsonc"
{
"compatibility_date": "2026-07-01",
"compatibility_flags": ["enable_request_signal"]
}
```
If your `compatibility_date` is before `2026-03-03`, also add [`unhandled_rejection_after_microtask_checkpoint`](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#defer-unhandled-rejection-processing-to-after-microtask-checkpoint). Without it, Workers can report false `unhandledrejection` errors for promises that are handled a microtask later.
## Traces
Use the [Cloudflare Workers Traces integration](/docs/integrations/cloudflare-traces) to record oRPC spans in Workers Traces.
---
# Electron Adapter
Source: https://orpc.dev/docs/adapters/electron
Establish typesafe communication between processes in [Electron](https://www.electronjs.org/) using the [Message Port Adapter](/docs/adapters/message-port). Before you start, we recommend reading the [MessagePorts in Electron](https://www.electronjs.org/docs/latest/tutorial/message-ports) guide.
## Main Process
Listen for a port sent from the renderer, then upgrade it:
```ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/message-port'
import { app, ipcMain } from 'electron'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
app.whenReady().then(() => {
ipcMain.on('start-orpc-server', async (event) => {
const [serverPort] = event.ports
handler.upgrade(serverPort, {
context: {} // Provide initial context if needed
})
serverPort.start()
})
})
```
:::info
Channel `start-orpc-server` is arbitrary. You can use any name that fits your needs.
:::
## Preload Process
Receive the port from the renderer and forward it to the main process:
```ts
import { ipcRenderer } from 'electron'
window.addEventListener('message', (event) => {
if (event.data === 'start-orpc-client') {
const [serverPort] = event.ports
ipcRenderer.postMessage('start-orpc-server', null, [serverPort])
}
})
```
## Renderer Process
Create a `MessageChannel`, send one port to the preload script, and use the other to initialize the client link:
```ts
import { RPCLink } from '@orpc/client/message-port'
const { port1: clientPort, port2: serverPort } = new MessageChannel()
window.postMessage('start-orpc-client', '*', [serverPort])
const link = new RPCLink({
port: clientPort,
})
clientPort.start()
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
---
# Elysia Adapter
Source: https://orpc.dev/docs/adapters/elysia
[Elysia](https://elysiajs.com/) is a high-performance web framework for [Bun](https://bun.com/) that adheres to the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Basic
```ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { Elysia } from 'elysia'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const app = new Elysia()
.all('/rpc*', async ({ request }: { request: Request }) => {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
}, {
parse: 'none' // Skip Elysia's body parsing; oRPC reads the raw request itself
})
.listen(3000)
console.log('Elysia is running at http://localhost:3000')
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
---
# Expo Adapter
Source: https://orpc.dev/docs/adapters/expo
[Expo](https://expo.dev/) is the supported way to run oRPC in a React Native app. It installs the browser APIs oRPC depends on, which bare React Native does not provide, so JSON calls, [AsyncIteratorObject](/docs/async-iterator-object), and streamed downloads work in a plain Expo project with no polyfill and no Metro configuration. [Some `File` and `Blob` cases](#feature-support) need one extra package.
Two of those matter most:
- **Web Streams.** oRPC needs `ReadableStream` for every procedure call and `TransformStream` for [AsyncIteratorObject](/docs/async-iterator-object); without them every call throws `ReferenceError: ReadableStream is not defined`. Expo installs `ReadableStream`, `WritableStream`, and `TransformStream` as native globals from [SDK 53](https://github.com/expo/expo/pull/36407).
- **A streaming `fetch`.** React Native's built-in `fetch` is an `XMLHttpRequest` polyfill with no `response.body`, which rules out [AsyncIteratorObject](/docs/async-iterator-object) and streamed downloads. [`expo/fetch`](https://docs.expo.dev/versions/latest/sdk/expo/#fetch) is native-backed and streams properly. From **SDK 56** it replaces the global `fetch` on Android and iOS, so there is nothing to wire up.
:::tip
Use **SDK 56 or later** to get everything working out of the box. SDK 53 to 55 work too, but you must pass `expo/fetch` to the link yourself, as shown [below](#expo-sdk-53-to-55).
:::
## Fetch Link
```ts title="utils/orpc.ts"
import { RPCLink } from '@orpc/client/fetch'
export const link = new RPCLink({
origin: process.env.EXPO_PUBLIC_SERVER_URL,
url: '/rpc',
headers: async ({ context }) => ({
'x-api-key': context?.something ?? '',
}),
})
```
:::info
The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
### Expo SDK 53 to 55
On these versions the global `fetch` is still React Native's non-streaming polyfill, so pass `expo/fetch` to the link explicitly. Everything else stays the same.
```ts
import { fetch as expoFetch } from 'expo/fetch'
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
origin: process.env.EXPO_PUBLIC_SERVER_URL,
url: '/rpc',
fetch: (url, init) => expoFetch(url, init),
})
```
:::warning
On SDK 56 and later you can opt back out of `expo/fetch` by setting `EXPO_PUBLIC_USE_RN_FETCH=1`. Doing so restores React Native's `XMLHttpRequest` polyfill and silently disables streaming, so `AsyncIteratorObject` and streamed responses stop working.
:::
## WebSocket Link
Expo inherits React Native's `WebSocket`, which is backed by OkHttp on Android and SocketRocket on iOS and handles binary frames in both directions.
```ts
import { RPCLink } from '@orpc/client/websocket'
const link = new RPCLink({
connect: () => {
const ws = new WebSocket('ws://localhost:3000')
ws.binaryType = 'arraybuffer'
return ws
},
})
```
:::warning
Set `binaryType` to `'arraybuffer'` and never `'blob'`. React Native's `Blob` has no `arrayBuffer()`, `text()`, or `stream()` method, so oRPC cannot read the bytes back out of a blob message. Every binary frame is also base64-encoded to cross the native bridge, so prefer many small messages over one large payload.
Sending or receiving a `File` or `Blob` over this link additionally needs the [Blob polyfill](#blob-polyfill).
:::
## Feature Support
oRPC treats binary data two different ways, and the difference decides what works on Expo:
- **Root level**, where a `File` or `Blob` is the entire input or output. oRPC sends it as the raw body, with no multipart involved.
- **Nested**, where a `File` or `Blob` sits inside an object or array. oRPC packs it into a `FormData`, which travels as `multipart/form-data`.
Expo never parses multipart, and the WebSocket link cannot even produce it, which is why every nested row below fails somewhere. Everything else depends on whether you install the [Blob polyfill](#blob-polyfill).
| Feature | Fetch | Fetch + polyfill | WebSocket | WebSocket + polyfill |
| --- | --- | --- | --- | --- |
| [AsyncIteratorObject](/docs/async-iterator-object) download | Yes | Yes | Yes | Yes |
| `AsyncIteratorObject` upload | Buffered | Buffered | Yes | Yes |
| [`ReadableStream`](/docs/binary-data#readablestreamuint8array) download | Yes | Yes | Yes | Yes |
| `ReadableStream` upload | Buffered | Buffered | Yes | Yes |
| Root-level [`File` or `Blob`](/docs/binary-data#file-and-blob) download | No | Yes | No | Yes |
| Root-level `File` or `Blob` upload | Yes | Yes | No | Yes |
| Nested `File` or `Blob` download | No | No | No | No |
| Nested `File` or `Blob` upload | No | Yes | No | No |
Two things are worth knowing beyond the table:
- **`expo/fetch` buffers every upload.** It accepts a streamed request body, whether that is an `AsyncIteratorObject` or a `ReadableStream`, but drains it into one buffer before the request starts. Nothing reaches the server until the stream finishes, and the whole payload sits in memory meanwhile. Downloads stream properly. The WebSocket link has no such limit: each chunk is its own frame, sent immediately, so uploads genuinely stream. It is the one place the WebSocket link beats fetch.
- **The WebSocket link handles nested binary worse.** It runs `FormData` through the global `Response` in both directions, and Expo leaves that as React Native's implementation, which can neither read a `FormData` body as a blob nor parse a multipart one. The fetch link only has the decode half of that problem.
### Blob Polyfill
React Native's `Blob` has no `arrayBuffer()`, `text()`, `bytes()`, or `stream()`, and `new Blob([arrayBuffer])` throws, so oRPC cannot read bytes back out of a downloaded blob. [`expo-blob`](https://www.npmjs.com/package/expo-blob) is Expo's spec-compliant implementation. It fixes every root-level case in the table above, plus nested uploads on the fetch link.
```package-install
npx expo install expo-blob
```
It exports only `Blob`, so declare `File` yourself and install both as globals from your entry file:
```ts title="polyfill.ts"
import { Blob, type BlobPart } from 'expo-blob'
class File extends Blob {
name: string
lastModified: number
webkitRelativePath = ''
constructor(fileBits: BlobPart[] | Iterable, fileName: string, options?: FilePropertyBag) {
super(fileBits, options)
this.name = String(fileName)
this.lastModified = options?.lastModified ?? Date.now()
}
override toString(): string {
return '[object File]'
}
}
Object.assign(globalThis, { Blob, File })
```
:::warning
`File` must extend the same class you install as `globalThis.Blob`, because oRPC classifies binary values with `instanceof Blob`. Keep `name` an own writable property, as above, so Expo's `FormData` can set a filename on it.
Replacing the global `Blob` also affects code outside oRPC. `xhr.responseType = 'blob'` and `WebSocket` with `binaryType = 'blob'` still produce React Native blobs, which are no longer `instanceof Blob`. `URL.createObjectURL` and `FileReader` go the other way: they read React Native's internal blob data, so they throw when handed a polyfilled one.
:::
### Why nested binary fails
A nested `File` or `Blob` is packed into a `FormData` and sent as `multipart/form-data`. Reading it back needs `Response.formData()`, and no `Response` on Expo can do that. Both `expo/fetch` and React Native handle only the simpler `application/x-www-form-urlencoded` format.
Writing multipart works, but only on the fetch link, where `expo/fetch` does the encoding. The WebSocket link uses `Response` to write it too, so that link fails in both directions.
| Link | Nested upload | Nested download |
| --- | --- | --- |
| Fetch | Works with the [Blob polyfill](#blob-polyfill) | Fails |
| WebSocket | Fails | Fails |
**The easy fix is to avoid multipart.** Keep files at the root of a procedure's input or output instead of inside an object. Or extend the [RPC JSON Serializer](/docs/rpc/serializer) to carry binary as `base64`.
**The full fix is a `Response` that handles multipart.** For the WebSocket link, replace `globalThis.Response` with one that can both write and read it. For the fetch link only reading is missing, so extend the response that `expo/fetch` returns.
## Bare React Native
oRPC does not support bare React Native out of the box. Without Expo you get neither the Web Streams globals oRPC needs nor a streaming `fetch`. You can get close by adding [web-streams-polyfill](https://github.com/MattiasBuelens/web-streams-polyfill) and loading it from your entry file, but `AsyncIteratorObject` and streamed downloads will still not work over the built-in `fetch`. React Native 0.78 is also a hard floor: oRPC uses a class static initialization block, Metro does not transpile it, and older Hermes cannot parse it. See [Requirements](/docs/requirements#react-native) for the full list of APIs oRPC needs.
---
# Express.js Adapter
Source: https://orpc.dev/docs/adapters/express
[Express.js](https://expressjs.com/) is a popular Node.js framework for building web applications. oRPC integrates with it through the [Node HTTP Adapter](/docs/adapters/node-http).
:::warning
Express's [body-parser](https://expressjs.com/en/resources/middleware/body-parser.html) handles common request body types, and oRPC will use the parsed body if available. However, it doesn't support features like [Bracket Notation](/docs/openapi/bracket-notation), and in case you upload a file with `application/json`, it may be parsed as plain JSON instead of a `File`. To avoid these issues, register any body-parsing middleware **after** your oRPC middleware or only on routes that don't use oRPC.
:::
## Basic
```ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/node'
import cors from 'cors'
import express from 'express'
const app = express()
app.use(cors())
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
app.use('/rpc{/*path}', async (req, res, next) => {
const { matched } = await handler.handle(req, res, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return
}
next()
})
app.listen(3000, () => console.log('Server listening on port 3000'))
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
---
# Fastify Adapter
Source: https://orpc.dev/docs/adapters/fastify
## Server Usage
```ts RPC
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fastify'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
import Fastify from 'fastify'
const handler = new RPCHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const app = Fastify()
app.all('/rpc/*', async (req, reply) => {
const { matched } = await handler.handle(req, reply, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return reply
}
return reply.status(404).send('Not found')
})
app.listen({ port: 3000 }).then(() => console.log('Listening on port 3000'))
```
```ts OpenAPI
import { OpenAPIHandler } from '@orpc/openapi/fastify'
import { onError } from '@orpc/server'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
import Fastify from 'fastify'
const handler = new OpenAPIHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const app = Fastify()
app.all('/api/*', async (req, reply) => {
const { matched } = await handler.handle(req, reply, {
prefix: '/api',
context: {} // Provide initial context if needed
})
if (matched) {
return reply
}
return reply.status(404).send('Not found')
})
app.listen({ port: 3000 }).then(() => console.log('Listening on port 3000'))
```
:::tip
Fastify only accepts content types it has a registered parser for, and parses request bodies itself. For the best oRPC experience, register a catch-all parser with `app.addContentTypeParser('*', ...)` so every content type is supported, and call `app.removeAllContentTypeParsers()` so every body is parsed by oRPC instead of Fastify:
```ts
// Optional, let oRPC parse all content types
app.removeAllContentTypeParsers()
// Optional, support all content types
app.addContentTypeParser('*', (request, payload, done) => {
done(null, undefined)
})
```
:::
:::warning
To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios,
extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standard-server#how-body-parsing-works). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`:
```ts
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})
```
:::
## Event Stream Options
You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler.
```ts
const handler = new OpenAPIHandler(router, {
sendStandardResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 15000
*/
interval: 15000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
})
```
---
# Fetch API Adapter
Source: https://orpc.dev/docs/adapters/fetch-api
## Server Usage
```ts RPC
import { RPCHandler } from '@orpc/server/fetch'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export async function fetch(request: Request): Promise {
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return response
}
return new Response('Not found', { status: 404 })
}
```
```ts OpenAPI
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
import { onError } from '@orpc/server'
const handler = new OpenAPIHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export async function fetch(request: Request): Promise {
const { matched, response } = await handler.handle(request, {
prefix: '/api',
context: {} // Provide initial context if needed
})
if (matched) {
return response
}
return new Response('Not found', { status: 404 })
}
```
:::info
The actual usage of `fetch` depends on the runtime environment or library you use:
```ts Bun
Bun.serve({
fetch,
})
```
```ts Deno
Deno.serve(fetch)
```
:::
:::warning
To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios,
extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standard-server#how-body-parsing-works). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`:
```ts
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})
```
:::
## Client Usage
```ts RPC
import { RPCLink } from '@orpc/client/fetch'
import { onError } from '@orpc/client'
const link = new RPCLink({
origin: 'https://api.example.com', // accepts async function, defaults to current origin
url: '/rpc', // accepts async function
headers: { authorization: 'bearer token' }, // accept async function
interceptors: [
onError((error) => {
console.error(error)
}),
],
fetch: (request, init) => { // <- override fetch if needed
return globalThis.fetch(request, {
...init,
credentials: 'include', // Include cookies on cross-origin requests
})
},
})
```
```ts OpenAPI
import { OpenAPILink } from '@orpc/openapi/fetch'
import { onError } from '@orpc/client'
const link = new OpenAPILink(contract, {
origin: 'https://api.example.com', // accepts async function, defaults to current origin
url: '/rpc', // accepts async function
headers: { authorization: 'bearer token' }, // accept async function
interceptors: [
onError((error) => {
console.error(error)
}),
],
fetch: (request, init) => { // <- override fetch if needed
return globalThis.fetch(request, {
...init,
credentials: 'include', // Include cookies on cross-origin requests
})
},
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients).
:::
## Event Stream Options
You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `toFetchResponse.eventStream` options when creating the handler.
```ts
const handler = new OpenAPIHandler(router, {
toFetchResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 15000
*/
interval: 15000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
})
```
:::info
You can also configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed from client to server using `toFetchRequest.eventStream` options when creating the link.
:::
---
# H3 Adapter
Source: https://orpc.dev/docs/adapters/h3
[H3](https://h3.dev/) is a universal, tiny, and fast web framework built on top of web standards, so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Basic
```ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { H3, serve } from 'h3'
const app = new H3()
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
app.use('/rpc/**', async (event) => {
const { matched, response } = await handler.handle(event.req, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return response
}
})
serve(app, { port: 3000 })
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
---
# Hono Adapter
Source: https://orpc.dev/docs/adapters/hono
[Hono](https://hono.dev/) is a high-performance web framework built on top of the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Basic
```ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { Hono } from 'hono'
const app = new Hono()
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
app.use('/rpc/*', async (c, next) => {
const { matched, response } = await handler.handle(c.req.raw, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return c.newResponse(response.body, response)
}
await next()
})
export default app
```
If Hono middleware reads the request body before the oRPC handler processes it, an error will occur. You can solve this by using a proxy to intercept the request body parsers with Hono parsers.
```ts
const BODY_PARSER_METHODS = new Set(['arrayBuffer', 'blob', 'formData', 'json', 'text'] as const)
type BodyParserMethod = typeof BODY_PARSER_METHODS extends Set ? T : never
app.use('/rpc/*', async (c, next) => {
const request = new Proxy(c.req.raw, {
get(target, prop) {
if (prop === 'bodyUsed') {
return false // Hono can still provide the body from its parser cache
}
if (BODY_PARSER_METHODS.has(prop as BodyParserMethod)) {
return () => c.req[prop as BodyParserMethod]()
}
return Reflect.get(target, prop, target)
}
})
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return c.newResponse(response.body, response)
}
await next()
})
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
---
# Message Port Adapter
Source: https://orpc.dev/docs/adapters/message-port
## Basic Usage
Message Ports work by establishing two endpoints that can communicate with each other:
```ts Bridge
const channel = new MessageChannel()
const serverPort = channel.port1
const clientPort = channel.port2
```
```ts Server
import { RPCHandler } from '@orpc/server/message-port'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
handler.upgrade(serverPort, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial upgrade request.
*/
context: request => ({}),
})
serverPort.start()
```
```ts Client
import { RPCLink } from '@orpc/client/message-port'
import { onError } from '@orpc/client'
const link = new RPCLink({
port: clientPort,
interceptors: [
onError((error) => {
console.error(error)
}),
],
/**
* Optional headers to attach to each per-call request.
* These can be accessed in the server context or via the Request Headers Plugin.
*/
headers: () => ({})
})
clientPort.start()
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
## Transfer
By default, oRPC serializes request/response messages to string/binary data before sending over message port. If needed, you can define the `transfer` option to utilize full power of [MessagePort: postMessage() method](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage), such as transferring ownership of objects to the other side or support unserializable objects like `OffscreenCanvas`.
```ts handler
const handler = new RPCHandler(router, {
experimental_transfer: (message, port) => {
const transfer = deepFindTransferableObjects(message) // implement your own logic
return transfer.length ? transfer : null // only enable when needed
}
})
```
```ts link
const link = new RPCLink({
experimental_transfer: (message) => {
const transfer = deepFindTransferableObjects(message) // implement your own logic
return transfer.length ? transfer : null // only enable when needed
}
})
```
:::info
When `transfer` returns an array, messages are sent using [the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), which doesn't support all data types. If you need to support additional data types, consider customizing your [RPC Serializer](/docs/rpc/serializer).
:::
---
# Next.js Adapter
Source: https://orpc.dev/docs/adapters/next
[Next.js](https://nextjs.org/) is a leading React framework for server-rendered apps. oRPC works with both the [App Router](https://nextjs.org/docs/app/getting-started/installation) and [Pages Router](https://nextjs.org/docs/pages/getting-started/installation) through the [Fetch API Adapter](/docs/adapters/fetch-api) and [Node HTTP Adapter](/docs/adapters/node-http) respectively.
:::info
oRPC also supports [Next.js server functions](/docs/integrations/next) through the dedicated `@orpc/next` integration.
:::
## Server
You set up an oRPC server inside Next.js using its [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers).
```ts title="app/rpc/[[...rest]]/route.ts"
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
async function handleRequest(request: Request) {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
}
export const HEAD = handleRequest
export const GET = handleRequest
export const POST = handleRequest
export const PUT = handleRequest
export const PATCH = handleRequest
export const DELETE = handleRequest
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
```ts title="pages/api/rpc/[[...rest]].ts"
import type { NextApiRequest, NextApiResponse } from 'next'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/node'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export const config = {
api: {
bodyParser: false,
},
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { matched } = await handler.handle(req, res, {
prefix: '/api/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return
}
res.statusCode = 404
res.end('Not found')
}
```
:::warning
Next.js [body parser](https://nextjs.org/docs/pages/building-your-application/routing/api-routes#custom-config) may handle common request body types, and oRPC will use the parsed body if available. However, it doesn't support features like [Bracket Notation](/docs/openapi/bracket-notation), and in case you upload a file with `application/json`, it may be parsed as plain JSON instead of a `File`. To avoid these issues, disable the body parser with `config.api.bodyParser = false` as shown above.
:::
## Client
By leveraging `headers` from `next/headers`, you can configure the link to work seamlessly in both browser and server environments:
```ts title="lib/orpc.ts"
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
url: '/rpc',
origin: typeof window === 'undefined' ? 'http://localhost:3000' : undefined, // defaults to the current origin in the browser
headers: async () => {
if (typeof window !== 'undefined') {
return {}
}
const { headers } = await import('next/headers')
return await headers()
},
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients).
:::
## Optimize SSR
To reduce HTTP requests and improve latency during SSR, you can use a [server-side client](/docs/client/server-side) during SSR. Below is a quick setup, see [Optimizing SSR](/docs/recipes/optimizing-ssr) for more details.
```ts title="lib/orpc.ts"
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
if (import.meta.env.SSR) {
await import('./orpc.server')
}
declare global {
var $client: RouterClient | undefined
}
const link = new RPCLink({
url: '/rpc',
origin: () => {
if (typeof window === 'undefined') {
throw new Error('This link is not allowed on the server side.')
}
return window.location.origin
},
})
/**
* Fall back to a browser client when no SSR client is registered.
*/
export const client: RouterClient = globalThis.$client ?? createORPCClient(link)
```
```ts title="lib/orpc.server.ts"
import { createRouterClient } from '@orpc/server'
import { headers } from 'next/headers'
globalThis.$client = createRouterClient(router, {
/**
* Provide initial context if needed.
*
* Because this client instance is shared across all requests,
* only include context that's safe to reuse globally.
* For per-request context, use middleware context or pass a function as the initial context.
*/
context: async () => ({
headers: await headers(), // provide headers if initial context required
}),
})
```
:::warning
`import.meta.env.SSR` requires [Turbopack](https://nextjs.org/docs/app/api-reference/turbopack), the default bundler since Next.js 16. On webpack builds, guard with `typeof window === 'undefined'` instead, which Next.js also replaces at build time. Either way, do not add `import 'server-only'` to `orpc.server.ts`: it would fail the build.
:::
---
# Node HTTP Adapter
Source: https://orpc.dev/docs/adapters/node-http
## Server Usage
```ts RPC
import { createServer } from 'node:http' // or 'node:https' or 'node:http2'
import { RPCHandler } from '@orpc/server/node'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const server = createServer(async (req, res) => {
const { matched } = await handler.handle(req, res, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (matched) {
return
}
res.statusCode = 404
res.end('Not found')
})
server.listen(3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000'))
```
```ts OpenAPI
import { createServer } from 'node:http' // or 'node:https' or 'node:http2'
import { OpenAPIHandler } from '@orpc/openapi/node'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
import { onError } from '@orpc/server'
const handler = new OpenAPIHandler(router, {
plugins: [
new CORSHandlerPlugin()
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const server = createServer(async (req, res) => {
const { matched } = await handler.handle(req, res, {
prefix: '/api',
context: {} // Provide initial context if needed
})
if (matched) {
return
}
res.statusCode = 404
res.end('Not found')
})
server.listen(3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000'))
```
:::warning
To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios,
extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standard-server#how-body-parsing-works). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`:
```ts
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})
```
:::
## Event Stream Options
You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler.
```ts
const handler = new OpenAPIHandler(router, {
sendStandardResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 15000
*/
interval: 15000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
})
```
---
# Nuxt Adapter
Source: https://orpc.dev/docs/adapters/nuxt
[Nuxt](https://nuxt.com/) is a popular Vue.js framework for building server-side applications. Its server engine follows web standards, so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Server
You set up an oRPC server inside Nuxt using its [Server Routes](https://nuxt.com/docs/guide/directory-structure/server#server-routes).
```ts title="server/routes/rpc/[...].ts"
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export default defineEventHandler(async (event) => {
const request = toWebRequest(event)
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
if (response) {
return response
}
setResponseStatus(event, 404, 'Not Found')
return 'Not found'
})
```
```ts title="server/routes/rpc/index.ts"
export { default } from './[...]'
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
## Client
To make the oRPC client compatible with SSR, set it up inside a [Nuxt Plugin](https://nuxt.com/docs/guide/directory-structure/plugins).
```ts title="app/plugins/orpc.ts"
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
export default defineNuxtPlugin(() => {
const event = useRequestEvent()
const requestURL = useRequestURL()
const link = new RPCLink({
url: '/rpc',
origin: typeof window === 'undefined' ? requestURL.origin : undefined, // defaults to the current origin in the browser
headers: () => event?.headers ?? {},
})
const client: RouterClient = createORPCClient(link)
return {
provide: {
client,
},
}
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients).
:::
## Optimize SSR
To reduce HTTP requests and improve latency during SSR, you can use a [server-side client](/docs/client/server-side) during SSR. Below is a quick setup, see [Optimizing SSR](/docs/recipes/optimizing-ssr) for more details.
```ts title="app/plugins/orpc.client.ts"
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
export default defineNuxtPlugin(() => {
const link = new RPCLink({
url: '/rpc',
})
const client: RouterClient = createORPCClient(link)
return {
provide: {
client,
},
}
})
```
```ts title="app/plugins/orpc.server.ts"
import { createRouterClient } from '@orpc/server'
export default defineNuxtPlugin(() => {
const event = useRequestEvent()
const client = createRouterClient(router, {
context: {
headers: event?.headers, // provide headers if initial context required
},
})
return {
provide: {
client,
},
}
})
```
---
# React Router Adapter
Source: https://orpc.dev/docs/adapters/react-router
[React Router](https://reactrouter.com/) is a multi-strategy router for React, and the successor to [Remix](https://remix.run/). In framework mode, its resource routes follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Basic
```ts title="app/routes/rpc.ts"
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
async function handleRequest(request: Request) {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
}
export async function loader({ request }: LoaderFunctionArgs) {
return handleRequest(request)
}
export async function action({ request }: ActionFunctionArgs) {
return handleRequest(request)
}
```
```ts title="app/routes.ts"
import type { RouteConfig } from '@react-router/dev/routes'
import { index, route } from '@react-router/dev/routes'
export default [
index('routes/home.tsx'),
route('rpc/*', 'routes/rpc.ts'),
] satisfies RouteConfig
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
:::info
In a Remix v2 project, the same `loader` and `action` exports work in a splat route (`app/routes/rpc.$.ts`) with the types imported from `@remix-run/node`.
:::
---
# SolidStart Adapter
Source: https://orpc.dev/docs/adapters/solid-start
[SolidStart](https://start.solidjs.com/) is a full stack JavaScript framework for building web applications with SolidJS. Its API routes follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Server
```ts title="src/routes/rpc/[...rest].ts"
import type { APIEvent } from '@solidjs/start/server'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
async function handle({ request }: APIEvent) {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
}
export const HEAD = handle
export const GET = handle
export const POST = handle
export const PUT = handle
export const PATCH = handle
export const DELETE = handle
```
```ts title="src/routes/rpc/index.ts"
export { DELETE, GET, HEAD, PATCH, POST, PUT } from './[...rest]'
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
## Client
On the client, use `getRequestEvent` to resolve the request origin and forward headers during SSR. This enables usage in both server and browser environments.
```ts
import { RPCLink } from '@orpc/client/fetch'
import { getRequestEvent } from 'solid-js/web'
const link = new RPCLink({
url: '/rpc',
// Resolve the origin from the incoming request during SSR; defaults to the current origin in the browser.
origin: () => {
const event = getRequestEvent()
return event ? new URL(event.request.url).origin : undefined
},
headers: () => getRequestEvent()?.request.headers ?? {},
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients).
:::
## Optimize SSR
To reduce HTTP requests and improve latency during SSR, you can use a [server-side client](/docs/client/server-side) during SSR. Below is a quick setup, see [Optimizing SSR](/docs/recipes/optimizing-ssr) for more details.
```ts title="src/lib/orpc.ts"
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
if (import.meta.env.SSR) {
await import('./orpc.server')
}
declare global {
var $client: RouterClient | undefined
}
const link = new RPCLink({
url: '/rpc',
origin: () => {
if (typeof window === 'undefined') {
throw new Error('This link is not allowed on the server side.')
}
return window.location.origin
},
})
/**
* Fall back to a browser client when no SSR client is registered.
*/
export const client: RouterClient = globalThis.$client ?? createORPCClient(link)
```
```ts title="src/lib/orpc.server.ts"
import { createRouterClient } from '@orpc/server'
import { getRequestEvent } from 'solid-js/web'
if (typeof window !== 'undefined') {
throw new Error('This file should not be imported in the browser')
}
globalThis.$client = createRouterClient(router, {
/**
* Provide initial context if needed.
*
* Because this client instance is shared across all requests,
* only include context that's safe to reuse globally.
* For per-request context, use middleware context or pass a function as the initial context.
*/
context: async () => {
const headers = getRequestEvent()?.request.headers
return {
headers, // provide headers if initial context required
}
},
})
```
:::warning
Guard the import with `import.meta.env.SSR`, which Vite replaces at build time, so the server module is stripped from client bundles. A `typeof window` check is not enough: the bundler would still emit your router as a publicly downloadable client chunk.
:::
---
# SvelteKit Adapter
Source: https://orpc.dev/docs/adapters/svelte-kit
[SvelteKit](https://svelte.dev/docs/kit/introduction) is a framework for rapidly developing robust, performant web applications using Svelte. Its endpoints follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Server
```ts title="src/routes/rpc/[...rest]/+server.ts"
import type { RequestHandler } from './$types'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const handle: RequestHandler = async ({ request }) => {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
}
export const GET = handle
export const POST = handle
export const PUT = handle
export const PATCH = handle
export const DELETE = handle
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
## Client
During SSR, use [SvelteKit's `fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests), which forwards the `cookie` and `authorization` headers and calls the endpoint directly without an HTTP round trip.
```ts title="src/lib/orpc.ts"
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
url: '/rpc',
fetch: async (url, init) => {
if (import.meta.env.SSR) {
const { getRequestEvent } = await import('$app/server')
return getRequestEvent().fetch(url, init)
}
return fetch(url, init)
},
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients).
:::
## Optimize SSR
SvelteKit's `fetch` already skips the HTTP round trip, but requests are still serialized and deserialized. To remove that overhead as well, use a [server-side client](/docs/client/server-side) during SSR as described in [Optimizing SSR](/docs/recipes/optimizing-ssr#using-server-side-client-directly).
---
# TanStack Start Adapter
Source: https://orpc.dev/docs/adapters/tanstack-start
[TanStack Start](https://tanstack.com/start) is a full-stack React framework built on [Vite](https://vite.dev/) and the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), so oRPC integrates through the [Fetch API Adapter](/docs/adapters/fetch-api).
## Server
You set up an oRPC server inside TanStack Start using its [Server Routes](https://tanstack.com/start/latest/docs/framework/react/guide/server-routes).
```ts title="src/routes/api/rpc.$.ts"
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { createFileRoute } from '@tanstack/react-router'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export const Route = createFileRoute('/api/rpc/$')({
server: {
handlers: {
ANY: async ({ request }) => {
const { response } = await handler.handle(request, {
prefix: '/api/rpc',
context: {} // Provide initial context if needed
})
return response ?? new Response('Not found', { status: 404 })
},
},
},
})
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or another custom handler.
:::
## Client
Use `createIsomorphicFn` to configure the link with environment-specific settings for both browser and SSR environments:
```ts
import { RPCLink } from '@orpc/client/fetch'
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequest, getRequestHeaders } from '@tanstack/react-start/server'
const getClientLink = createIsomorphicFn()
.client(() => new RPCLink({
url: '/api/rpc',
}))
.server(() => new RPCLink({
url: '/api/rpc',
origin: () => new URL(getRequest().url).origin, // resolve from the incoming request
headers: () => getRequestHeaders(),
}))
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients).
:::
## Optimize SSR
To reduce HTTP requests and improve latency during SSR, you can use a [server-side client](/docs/client/server-side) during SSR. Below is a quick setup, see [Optimizing SSR](/docs/recipes/optimizing-ssr) for more details.
```ts title="src/lib/orpc.ts"
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { createRouterClient } from '@orpc/server'
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
const getORPCClient = createIsomorphicFn()
.server(() => createRouterClient(router, {
/**
* Provide initial context if needed.
*
* Because this client instance is shared across all requests,
* only include context that's safe to reuse globally.
* For per-request context, use middleware context or pass a function as the initial context.
*/
context: async () => ({
headers: getRequestHeaders(), // provide headers if initial context required
}),
}))
.client((): RouterClient => {
const link = new RPCLink({
url: '/api/rpc',
})
return createORPCClient(link)
})
export const client: RouterClient = getORPCClient()
```
---
# Web Workers Adapter
Source: https://orpc.dev/docs/adapters/web-workers
[Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker) allow JavaScript code to run in background threads, separate from the main thread of a web page. This prevents blocking the UI while performing computationally intensive tasks. Web Workers are also supported in modern runtimes like [Bun](https://bun.com/docs/api/workers), [Deno](https://docs.deno.com/examples/web_workers/), etc.
With oRPC, you can establish typesafe communication channels between your main thread and Web Workers through the [Message Port Adapter](/docs/adapters/message-port).
## Web Worker
Configure your Web Worker to handle oRPC requests by upgrading it with a message port handler:
```ts
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/message-port'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
handler.upgrade(self, {
context: {} // Provide initial context if needed
})
```
## Main Thread
Create a link to communicate with your Web Worker:
```ts
import { RPCLink } from '@orpc/client/message-port'
export const link = new RPCLink({
port: new Worker('some-worker.js')
})
```
You can leverage the [Vite Web Workers feature](https://vite.dev/guide/features.html#web-workers) for streamlined development:
```ts
import { RPCLink } from '@orpc/client/message-port'
import SomeWorker from './some-worker.ts?worker'
export const link = new RPCLink({
port: new SomeWorker()
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
---
# WebSocket Adapters
Source: https://orpc.dev/docs/adapters/websocket
## Server Adapters
| Adapter | Target |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `websocket` | [MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), [ws](https://github.com/websockets/ws), [Bun's WebSocket](https://bun.com/docs/runtime/http/websockets), [Deno's WebSocket](https://docs.deno.com/examples/http_server_websocket/), [Cloudflare Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/), [uWebSockets](https://github.com/uNetworking/uWebSockets.js/) |
| `crossws` | [crossws](https://github.com/h3js/crossws) |
```ts ws
import { WebSocketServer } from 'ws'
import { RPCHandler } from '@orpc/server/websocket'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (ws) => {
handler.upgrade(ws, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial WebSocket upgrade request.
*/
context: request => ({}),
})
})
```
```ts crossws
import { createServer } from 'node:http'
import { experimental_RPCHandler as RPCHandler } from '@orpc/server/crossws'
import { onError } from '@orpc/server'
// any crossws adapter is supported
import crossws from 'crossws/adapters/node'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const ws = crossws({
hooks: {
message: async (peer, message) => {
await handler.message(peer, message, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial WebSocket upgrade request.
*/
context: request => ({}),
})
},
close: async (peer) => {
await handler.close(peer)
},
},
})
const server = createServer((req, res) => {
res.end(`Hello World`)
}).listen(3000)
server.on('upgrade', (req, socket, head) => {
if (req.headers.upgrade === 'websocket') {
ws.handleUpgrade(req, socket, head)
}
})
```
```ts Bun
import { RPCHandler } from '@orpc/server/websocket'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
Bun.serve({
fetch(req, server) {
if (server.upgrade(req)) {
return
}
return new Response('Upgrade failed', { status: 500 })
},
websocket: {
async message(ws, message) {
await handler.message(ws, message, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial WebSocket upgrade request.
*/
context: request => ({}),
})
},
async close(ws) {
await handler.close(ws)
},
}
})
```
```ts Deno
import { RPCHandler } from '@orpc/server/websocket'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
Deno.serve((req) => {
if (req.headers.get('upgrade') !== 'websocket') {
return new Response(null, { status: 501 })
}
const { socket, response } = Deno.upgradeWebSocket(req)
handler.upgrade(socket, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial WebSocket upgrade request.
*/
context: request => ({}),
})
return response
})
```
```ts Cloudflare
import { RPCHandler } from '@orpc/server/websocket'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
export class ChatRoom extends DurableObject {
async fetch(): Promise {
const { '0': client, '1': server } = new WebSocketPair()
this.ctx.acceptWebSocket(server)
return new Response(null, {
status: 101,
webSocket: client,
})
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise {
await handler.message(ws, message, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial WebSocket upgrade request.
*/
context: request => ({}),
})
}
async webSocketClose(ws: WebSocket): Promise {
await handler.close(ws)
}
}
```
```ts uWebSockets
import { App } from 'uWebSockets.js'
import { RPCHandler } from '@orpc/server/websocket'
import { onError } from '@orpc/server'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
const app = App()
.ws('/*', {
async message(ws, message, isBinary) {
await handler.message(ws, message, {
/**
* Provide initial context if needed. The context can be an async function
* that receives the per-call request as its first argument, and is **not**
* related to the initial WebSocket upgrade request.
*/
context: request => ({}),
})
},
async close(ws, code, message) {
await handler.close(ws)
},
})
.listen(3000, (token) => {
if (token) {
console.log('Listening to port 3000')
}
})
```
## Client Adapters
| Adapter | Target |
| ----------- | ------------------------------------------------------------------------------- |
| `websocket` | [MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) |
```ts
import { RPCLink } from '@orpc/client/websocket'
const link = new RPCLink({
connect: info => new WebSocket('ws://localhost:3000'),
/**
* Whether to connect immediately on initialization, instead of waiting
* for the first call. Reduces latency for the first request.
*
* @default false
*/
connectOnInit: true,
/**
* Optional headers to attach to each per-call request.
* These can be accessed in the server context or via the Request Headers Plugin.
*/
headers: () => ({})
})
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
### Auto Reconnect
The client adapter has built-in support for reconnecting when the connection is lost. You can configure reconnect behavior with the `reconnect` option when creating the link.
```ts
const link = new RPCLink({
reconnect: {
/**
* Whether to automatically reconnect when the connection is lost.
*
* @default false
*/
enabled: true,
/**
* Delay before a (re)connect attempt, in milliseconds.
*
* @default info => info.attempt === 1 ? 0 : 2_000
*/
delay: info => info.attempt === 1 ? 0 : 2_000,
/**
* Maximum number of consecutive failed attempts before giving up.
* When exceeded, `getConnectedPeer` throws instead of retrying.
* Should greater than 1
*
* @default Infinity
*/
maxAttempt: Infinity,
onClose: {
/**
* Whether to proactively reconnect right after the socket closes,
* rather than waiting for the next call to trigger reconnection.
* Reduces latency for the next request.
*
* @default false
*/
enabled: false,
/**
* Delay before reconnecting after the socket closes, in milliseconds.
*
* @default 0
*/
delay: 0
}
}
})
```
---
# Worker Threads Adapter
Source: https://orpc.dev/docs/adapters/worker-threads
Use [Node.js Worker Threads](https://nodejs.org/api/worker_threads.html) with oRPC for typesafe inter-thread communication via the [Message Port Adapter](/docs/adapters/message-port).
## Worker Thread
Listen for a `MessagePort` sent from the main thread and upgrade it:
```ts
import { parentPort } from 'node:worker_threads'
import { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/message-port'
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
})
parentPort?.on('message', (message) => {
if (message instanceof MessagePort) {
handler.upgrade(message, {
context: {} // Provide initial context if needed
})
message.start()
}
})
```
## Main Thread
Create a `MessageChannel`, send one port to the worker thread, and use the other to initialize the client link:
```ts
import { MessageChannel, Worker } from 'node:worker_threads'
import { RPCLink } from '@orpc/client/message-port'
const { port1: clientPort, port2: serverPort } = new MessageChannel()
const worker = new Worker(new URL('./some-worker.js', import.meta.url))
worker.postMessage(serverPort, [serverPort])
const link = new RPCLink({
port: clientPort
})
clientPort.start()
```
:::info
The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients).
:::
---
# API Reference
Source: https://orpc.dev/docs/api-reference
Each package name links to its reference on [npmx](https://npmx.dev): every export with its signature and description, generated from the TypeScript declarations the package publishes. The guides beside it show those APIs in use.
:::tip
For questions the reference does not answer, [oRPC on DeepWiki](https://deepwiki.com/middleapi/orpc) maps out how the codebase fits together and lets you ask an AI assistant about any part of it.
:::
## Core
| Package | Purpose | Related Guides |
| ------- | ------- | -------------- |
| [@orpc/contract](https://npmx.dev/package-docs/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](/docs/contract/procedure), [Contract Router](/docs/contract/router), [Implementation](/docs/contract/implementation), [Generate from OpenAPI](/docs/contract/generate-from-openapi) |
| [@orpc/server](https://npmx.dev/package-docs/@orpc/server) | Build APIs or implement contracts. | [Procedure](/docs/procedure), [Router](/docs/router), [Middleware](/docs/middleware), [Context](/docs/context), [RPC Handler](/docs/rpc/handler) |
| [@orpc/client](https://npmx.dev/package-docs/@orpc/client) | Consume APIs with end-to-end type safety. | [Client-Side Clients](/docs/client/client-side), [Server-Side Clients](/docs/client/server-side), [RPC Link](/docs/rpc/link), [Error Handling](/docs/client/error-handling) |
| [@orpc/openapi](https://npmx.dev/package-docs/@orpc/openapi) | Add OpenAPI compatibility to APIs. | [OpenAPI Handler](/docs/openapi/handler), [OpenAPI Link](/docs/openapi/link), [Specification](/docs/openapi/specification), [Scalar](/docs/openapi/scalar) |
## Schema Validation
| Package | Purpose | Related Guides |
| ------- | ------- | -------------- |
| [@orpc/zod](https://npmx.dev/package-docs/@orpc/zod) | Integrate with Zod. | [Zod](/docs/integrations/zod) |
| [@orpc/valibot](https://npmx.dev/package-docs/@orpc/valibot) | Integrate with Valibot. | [Valibot](/docs/integrations/valibot) |
| [@orpc/arktype](https://npmx.dev/package-docs/@orpc/arktype) | Integrate with ArkType. | [ArkType](/docs/integrations/arktype) |
## Built-in Features
| Package | Purpose | Related Guides |
| ------- | ------- | -------------- |
| [@orpc/publisher](https://npmx.dev/package-docs/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](/docs/helpers/publisher) |
| [@orpc/ratelimit](https://npmx.dev/package-docs/@orpc/ratelimit) | Rate limiting with memory, Redis, and Upstash adapters. | [Rate Limit](/docs/helpers/ratelimit) |
| [@orpc/hibernation](https://npmx.dev/package-docs/@orpc/hibernation) | Leverage Hibernation APIs like Cloudflare's WebSocket Hibernation. | [Hibernation](/docs/integrations/hibernation) |
| [@orpc/json-schema](https://npmx.dev/package-docs/@orpc/json-schema) | Smart coercion for OpenAPI requests. | [Smart Coercion](/docs/plugins/smart-coercion) |
## Framework and Ecosystem Integrations
| Package | Purpose | Related Guides |
| ------- | ------- | -------------- |
| [@orpc/next](https://npmx.dev/package-docs/@orpc/next) | Integrate with Next.js Server Functions. | [Next.js](/docs/integrations/next) |
| [@orpc/ai-sdk](https://npmx.dev/package-docs/@orpc/ai-sdk) | Turn contracts and procedures into AI SDK tools. | [AI SDK](/docs/integrations/ai-sdk) |
| [@orpc/tanstack-query](https://npmx.dev/package-docs/@orpc/tanstack-query) | Integrate with TanStack Query. | [TanStack Query](/docs/integrations/tanstack-query) |
| [@orpc/pinia-colada](https://npmx.dev/package-docs/@orpc/pinia-colada) | Integrate with Pinia Colada. | [Pinia Colada](/docs/integrations/pinia-colada) |
| [@orpc/swr](https://npmx.dev/package-docs/@orpc/swr) | Integrate with SWR. | [SWR](/docs/integrations/swr) |
| [@orpc/experimental-effect](https://npmx.dev/package-docs/@orpc/experimental-effect) | Integrate with Effect. | [Effect](/docs/integrations/effect) |
| [@orpc/experimental-msw](https://npmx.dev/package-docs/@orpc/experimental-msw) | Mock procedures at the network level with typed MSW request handlers. | [MSW](/docs/integrations/msw) |
| [@orpc/nest](https://npmx.dev/package-docs/@orpc/nest) | Implement your contract with NestJS. | [NestJS](/docs/integrations/nest) |
| [@orpc/node](https://npmx.dev/package-docs/@orpc/node) | Node.js plugins for static file serving and large uploads. | [Static File](/docs/plugins/static-file), [Tmp File Upload](/docs/plugins/tmp-file-upload), [Batch Response Compression](/docs/plugins/batch-response-compression) |
| [@orpc/bun](https://npmx.dev/package-docs/@orpc/bun) | Bun Redis adapters for Publisher and Rate Limit. | [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit) |
| [@orpc/cloudflare](https://npmx.dev/package-docs/@orpc/cloudflare) | Adapters for Cloudflare Workers. | [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit), [Traces](/docs/integrations/cloudflare-traces) |
| [@orpc/trpc](https://npmx.dev/package-docs/@orpc/trpc) | Reuse existing tRPC routers within oRPC. | [tRPC](/docs/integrations/trpc) |
## Observability
| Package | Purpose | Related Guides |
| ------- | ------- | -------------- |
| [@orpc/opentelemetry](https://npmx.dev/package-docs/@orpc/opentelemetry) | Distributed tracing with OpenTelemetry. | [OpenTelemetry](/docs/integrations/opentelemetry) |
| [@orpc/pino](https://npmx.dev/package-docs/@orpc/pino) | Logging with Pino. | [Pino](/docs/integrations/pino) |
| [@orpc/evlog](https://npmx.dev/package-docs/@orpc/evlog) | Logging with Evlog. | [Evlog](/docs/integrations/evlog) |
---
# AsyncIteratorObject (SSE)
Source: https://orpc.dev/docs/async-iterator-object
## Overview
An `AsyncIteratorObject` is implemented as an [asynchronous generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) (or a compatible implementation). In the example below, the handler emits a new event every second:
```ts
const example = os
.handler(async function* ({ input, signal, lastEventId }) {
while (true) {
signal?.throwIfAborted()
yield { message: 'Hello, world!' }
await new Promise(resolve => setTimeout(resolve, 1000))
}
})
```
:::info
Learn how to consume an `AsyncIteratorObject` from the client in the [client guide](/docs/client/async-iterator-object).
:::
## Validating Events
Use the built‑in `asyncIteratorObject` schema that works with any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate events.
```ts
import { asyncIteratorObject } from '@orpc/server'
const example = os
.output(asyncIteratorObject(z.object({ message: z.string() })))
.handler(async function* ({ input, signal, lastEventId }) {
while (true) {
signal?.throwIfAborted()
yield { message: 'Hello, world!' }
await new Promise(resolve => setTimeout(resolve, 1000))
}
})
```
## Last Event ID & Event Metadata
Using the `withEventMeta` helper, you can attach [additional event metadata](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) (such as an event ID or retry interval) to each event. When the client reconnects properly, the last received event ID is sent back to the server in `lastEventId`, allowing the stream to resume from where it left off.
:::info
When used with the [Retry Plugin](/docs/plugins/retry) or [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), reconnection with the last event ID is handled automatically.
:::
```ts
import { withEventMeta } from '@orpc/server'
const example = os
.handler(async function* ({ input, signal, lastEventId }) {
if (lastEventId) {
// Resume streaming from lastEventId
}
else {
while (true) {
signal?.throwIfAborted()
yield withEventMeta(
{ message: 'Hello, world!' },
{ id: 'some-id', retry: 10_000 }
)
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
})
```
## Stop AsyncIteratorObject
To end the stream, use either a `return` or `throw` statement. oRPC marks the stream as completed when the handler returns.
:::warning
This behavior is specific to oRPC. Standard [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) clients, such as [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), do not recognize this completion signal and will automatically attempt to reconnect. For details, see the [Standard Server documentation](https://github.com/middleapi/standard-server#event-stream-body).
:::
```ts
const example = os
.handler(async function* ({ input, signal, lastEventId }) {
while (true) {
signal?.throwIfAborted()
if (done) {
return
}
yield { message: 'Hello, world!' }
await new Promise(resolve => setTimeout(resolve, 1000))
}
})
```
## Signal and Side-Effects
When the client closes the connection or an unexpected error occurs, oRPC aborts the provided `signal`. Use it to exit loops and avoid resource leaks. Put cleanup logic in a `finally` block so it runs whether the stream ends normally, errors, or is cancelled.
```ts
const example = os
.handler(async function* ({ input, signal, lastEventId }) {
try {
while (true) {
signal?.throwIfAborted()
yield { message: 'Hello, world!' }
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
finally {
console.log('Cleanup logic here')
}
})
```
## Publisher Helper
You can combine the [AsyncIteratorObject](/docs/async-iterator-object) with the [Publisher Helper](/docs/helpers/publisher) to build real-time features like chat, notifications, or live updates with resume support.
```ts
const publisher = new MemoryPublisher<{
'something-updated': {
id: string
}
}>()
const live = os
.handler(async function* ({ input, signal, lastEventId }) {
const iterator = publisher.subscribe('something-updated', { signal, lastEventId })
for await (const payload of iterator) {
// Handle payload here or yield directly to client
yield payload
}
})
const publish = os
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => {
await publisher.publish('something-updated', { id: input.id })
})
```
---
# Binary Data
Source: https://orpc.dev/docs/binary-data
[File](https://developer.mozilla.org/en-US/docs/Web/API/File), [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob), and [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) are supported by the [RPC Serializer](/docs/rpc/serializer) and [OpenAPI Serializer](/docs/openapi/serializer). Use them to handle binary data in your procedures.
:::warning
To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios,
extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standard-server#how-body-parsing-works). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`:
```ts
const cors = new CORSHandlerPlugin({
allowHeaders: ['Content-Disposition', 'Standard-Server'],
exposeHeaders: ['Content-Disposition', 'Standard-Server'],
})
```
:::
## `File` and `Blob`
Procedures can accept `File` and `Blob` as input and return them directly or inside nested structures.
:::warning
`File` and `Blob` are buffered in memory by default. For large files on Node, use the [Tmp File Upload Plugin](/docs/plugins/tmp-file-upload) to stream uploads into temporary files instead.
:::
```ts twoslash
import { os } from '@orpc/server'
import * as z from 'zod'
// ---cut---
const example = os
.input(z.file())
.output(z.object({ anyFieldName: z.instanceof(File) }))
.handler(async ({ input }) => {
const file = input
console.log(file.name)
return {
anyFieldName: new File(['Hello World'], 'hello.txt', { type: 'text/plain' }),
}
})
```
## `ReadableStream`
Procedures can return `ReadableStream` to stream binary responses. The example below uses the [Response Headers Plugin](/docs/plugins/response-headers) to set the appropriate `Content-Type` header.
```ts twoslash
import { os } from '@orpc/server'
import { ResponseHeadersHandlerPluginContext } from '@orpc/server/plugins'
import * as z from 'zod'
interface ServerContext extends ResponseHeadersHandlerPluginContext {}
const base = os.$context()
// ---cut---
const example = base
.output(z.instanceof(ReadableStream))
.handler(async ({ context }) => {
context.resHeaders?.set('Content-Type', 'text/plain')
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('Hello World'))
controller.close()
}
})
return stream
})
```
---
# AsyncIteratorObject in Client
Source: https://orpc.dev/docs/client/async-iterator-object
## Basic Usage
Await a call to a procedure that returns an [AsyncIteratorObject](/docs/async-iterator-object), then iterate over the events as they arrive, just like an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
```ts twoslash
import { asyncIteratorObject, oc, RouterContractClient } from '@orpc/contract'
import { z } from 'zod'
const contract = {
streaming: oc.output(asyncIteratorObject(z.object({ message: z.string() })))
}
declare const client: RouterContractClient
// ---cut---
const iterator = await client.streaming()
for await (const event of iterator) {
console.log(event.message)
}
```
## Stopping the Stream
Use an `AbortSignal` or call `.return` to stop the iterator.
```ts
const controller = new AbortController()
const iterator = await client.streaming(undefined, { signal: controller.signal })
// Stop the stream after 1 second
setTimeout(async () => {
controller.abort()
// Or call `await iterator.return()` if you already have the iterator instance.
}, 1000)
for await (const event of iterator) {
console.log(event.message)
}
```
## Error Handling
:::info
Unlike traditional SSE, AsyncIteratorObjects do not retry automatically after an error. To add retries, use the [Retry Plugin](/docs/plugins/retry#event-source-simulation).
:::
```ts
const iterator = await client.streaming()
try {
for await (const event of iterator) {
console.log(event.message)
}
}
catch (error) {
if (error instanceof ORPCError) {
// Handle the error here
}
}
```
## Event Metadata
Use `getEventMeta` to read [event metadata](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) for each item, such as the event ID and retry interval.
```ts
import { getEventMeta } from '@orpc/client'
const iterator = await client.streaming()
for await (const event of iterator) {
const meta = getEventMeta(event)
console.log(event.message, meta?.id, meta?.retry)
}
```
## Using `consumeAsyncIterator`
Use `consumeAsyncIterator` to consume an `AsyncIterator` with lifecycle callbacks. It accepts either an iterator or a promise that resolves to one.
```ts
import { consumeAsyncIterator } from '@orpc/client'
const cancel = consumeAsyncIterator(client.streaming(), {
onEvent: (event) => {
console.log(event.message)
},
onError: (error) => {
console.error(error)
},
onSuccess: (value) => {
console.log(value)
},
onFinish: (state) => {
console.log(state)
},
})
setTimeout(async () => {
// Stop the stream after 1 second
await cancel()
}, 1000)
```
---
# Client-Side Clients
Source: https://orpc.dev/docs/client/client-side
## Installation
```package-install
npm install @orpc/client@beta
```
## Creating a Client
To create a client, first set up a link that defines how the client communicates with the server. This can be an [RPC Link](/docs/rpc/link), an [OpenAPI Link](/docs/openapi/link), or any custom link. Then create a client for your [router](/docs/router) or [contract](/docs/contract/router) using `createORPCClient`.
```ts
import { createORPCClient } from '@orpc/client'
import { RouterContractClient } from '@orpc/contract'
import { RouterClient } from '@orpc/server'
// if you are following contract-first approach
const contractClient: RouterContractClient = createORPCClient(link)
// if you are following normal approach
const normalClient: RouterClient = createORPCClient(link)
```
:::tip
You can export `RouterClient` or `RouterContractClient` from the server to avoid importing the contract or router in the client.
:::
## Calling Procedures
Once your client is set up, you can call your [procedures](/docs/procedure) as if they were local functions.
```ts twoslash
import * as z from 'zod'
import { os, RouterClient } from '@orpc/server'
const router = {
ping: os.handler(() => 'pong'),
pong: os.handler(() => 'ping'),
}
declare const client: RouterClient
// ---cut---
const pong = await client.ping()
client.ping
// ^|
//
//
```
## Client Context
Client context lets you pass values with each call, such as auth tokens or cache hints.
```ts
interface ClientContext {
token?: string
}
// if you are following contract-first approach
const client: RouterContractClient = createORPCClient(link)
// if you are following normal approach
const client: RouterClient = createORPCClient(link)
const output = await client.someProcedure(input, {
context: {
token: 'abc123',
},
})
```
## Interceptors
Interceptors let you wrap client calls. They are similar to interceptors in links, but are more typesafe because the exact input, output, and error types of each client are known. You can provide per-client interceptors with `scoped`.
```ts
import { isDefinedError, safe } from '@orpc/client'
const client: RouterClient = createORPCClient(link, {
interceptors: [
async ({ context, path, next }) => {
const [error, data] = await safe(next())
if (error) {
if (isDefinedError(error)) {
// handle typesafe errors
}
throw error
}
return data
}
],
scoped: {
planet: {
find: {
interceptors: [ // <- these interceptors only apply to client.planet.find
async ({ context, path, next }) => {
return next()
}
]
}
}
}
})
```
:::info
You can use [`safe` and `isDefinedError`](/docs/client/error-handling#using-safe-and-isdefinederror) together for typesafe error handling in interceptors.
:::
## Merging Clients
In oRPC, a client is just an object-like structure. To merge multiple clients, assign each client to a property on a new object:
```ts
const clientA: RouterClient = createORPCClient(linkA)
const clientB: RouterClient = createORPCClient(linkB)
const clientC: RouterClient = createORPCClient(linkC)
export const orpc = {
a: clientA,
b: clientB,
c: clientC,
}
```
## Utilities
:::info
These utilities can also be used for [server-side clients](/docs/client/server-side) and are not specific to client-side clients.
:::
### Infer Client Inputs
Infers input types for each procedure in a client.
```ts
import type { InferClientInputs } from '@orpc/client'
type Inputs = InferClientInputs
type FindPlanetInput = Inputs['planet']['find']
```
### Infer Client Body Inputs
Infers body input types for each procedure in a client. If an endpoint's input includes `{ body: ... }`, only the `body` portion is extracted. Otherwise, the entire input type is used.
```ts
import type { InferClientBodyInputs } from '@orpc/client'
type BodyInputs = InferClientBodyInputs
type FindPlanetBodyInput = BodyInputs['planet']['find']
```
### Infer Client Outputs
Infers output types for each procedure in a client.
```ts
import type { InferClientOutputs } from '@orpc/client'
type Outputs = InferClientOutputs
type FindPlanetOutput = Outputs['planet']['find']
```
### Infer Client Body Outputs
Infers body output types for each procedure in a client. If an endpoint's output includes `{ body: ... }`, only the `body` portion is extracted. Otherwise, the entire output type is used.
```ts
import type { InferClientBodyOutputs } from '@orpc/client'
type BodyOutputs = InferClientBodyOutputs
type FindPlanetBodyOutput = BodyOutputs['planet']['find']
```
### Infer Client Errors
Infers the errors each procedure in a client can throw when using [type-safe error handling](/docs/error-handling#typesafe-errors).
```ts
import type { InferClientErrors } from '@orpc/client'
type Errors = InferClientErrors
type FindPlanetError = Errors['planet']['find']
```
### Infer Client Error
Infers all possible errors the entire client can throw. This is useful with [type-safe error handling](/docs/error-handling#typesafe-errors).
```ts
import type { InferClientError } from '@orpc/client'
type ClientError = InferClientError
```
### Infer Client Context
Infers the [client context](#client-context) type from a client.
```ts
import type { InferClientContext } from '@orpc/client'
type Context = InferClientContext
```
---
# DynamicLink
Source: https://orpc.dev/docs/client/dynamic-link
## Example
```ts twoslash
import { os, RouterClient } from '@orpc/server'
import { RPCLink } from '@orpc/client/fetch'
const router = {
ping: os.handler(() => 'pong'),
pong: os.handler(() => 'ping'),
}
// ---cut---
import { createORPCClient, DynamicLink } from '@orpc/client'
interface ClientContext {
cache?: boolean
}
const cacheLink = new RPCLink({
origin: 'https://cache.example.com',
})
const noCacheLink = new RPCLink({
origin: 'https://example.com',
})
const link = new DynamicLink((options, path, input) => {
if (options.context?.cache) {
return cacheLink
}
return noCacheLink
})
const client: RouterClient = createORPCClient(link)
```
:::info
This example uses two [RPC Link](/docs/rpc/link) instances, but `DynamicLink` works with any other link.
:::
---
# Client Error Handling
Source: https://orpc.dev/docs/client/error-handling
## Using `try/catch`
For most calls, use regular `try/catch`.
```ts
try {
const data = await client.doSomething({ id: '123' })
}
catch (error) {
// handle error
}
```
## Using `safe` and `isDefinedError`
When working with [Typesafe Errors](/docs/error-handling#typesafe-errors), use `safe` to preserve error type inference. It behaves like `try/catch`, but returns the typesafe result instead of throwing.
```ts twoslash
import { call, os } from '@orpc/server'
import * as z from 'zod'
// ---cut---
import { isDefinedError, safe } from '@orpc/client'
const exampleProcedure = os
.input(z.object({ id: z.string() }))
.errors({
RATE_LIMIT_EXCEEDED: {
data: z.object({ retryAfter: z.number() })
}
})
.handler(async ({ input, errors }) => {
throw errors.RATE_LIMIT_EXCEEDED({ data: { retryAfter: 1000 } })
})
// or { error, data, definedError }
const [error, data, definedError] = await safe(
call(exampleProcedure, { id: '123' })
)
if (isDefinedError(error)) { // or definedError
// handle defined error
// or definedError.data.retryAfter
console.log(error.data.retryAfter)
}
else if (error) {
// handle unknown error
}
else {
// handle success
console.log(data)
}
```
:::info
`safe` supports both tuple and object forms:
- `[error, data, definedError]`
- `{ error, data, definedError }`
`definedError` is the same value as `error` when `isDefinedError(error)` returns `true`; otherwise it is `null`.
:::
## Safe Client
If you use `safe` often, `createSafeClient` can reduce repetition by wrapping entire client calls with `safe`.
```ts
import { createSafeClient } from '@orpc/client'
const safeClient = createSafeClient(client)
const [error, data] = await safeClient.doSomething({ id: '123' })
```
---
# Server-Side Clients
Source: https://orpc.dev/docs/client/server-side
## One-Off Calls
Use `call` when you need to invoke a single procedure without creating a client instance.
```ts twoslash
import * as z from 'zod'
const exampleProcedure = os
.input(z.string())
.handler(async ({ input }) => ({ id: input }))
// ---cut---
import { call, os } from '@orpc/server'
const result = await call(exampleProcedure, 'input', {
context: {} // <- provide initial context if needed
})
```
## Router Clients
Use `createRouterClient` to create a client for your [router](/docs/router). This is useful when you want to call multiple procedures.
```ts twoslash
import * as z from 'zod'
import { os } from '@orpc/server'
const router = {
ping: os.handler(() => 'pong'),
pong: os.handler(() => 'ping'),
}
// ---cut---
import { createRouterClient } from '@orpc/server'
const client = createRouterClient(router, {
context: {}, // <- provide initial context if needed, can be async function
interceptors: [
async ({ next, path }) => {
console.time(path.join('.'))
try {
return await next()
}
catch (err) {
console.error(`${path.join('.')}:`, err)
throw err
}
finally {
console.timeEnd(path.join('.'))
}
}
]
})
const result = await client.ping()
```
### Client Context
Client context is passed with each call. Use it to switch between contexts, such as different users or tenants, without creating multiple client instances.
```ts twoslash
import * as z from 'zod'
import { createRouterClient, os } from '@orpc/server'
const router = {
ping: os.handler(() => 'pong'),
pong: os.handler(() => 'ping'),
}
// ---cut---
interface ClientContext {
cache?: boolean
}
const client = createRouterClient(router, {
context: ({ cache }: ClientContext) => { // [!code highlight]
if (cache) {
return {} // <- context when cache enabled
}
return {}
}
})
const result = await client.ping(undefined, { context: { cache: true } })
```
### Interceptors
Interceptors let you observe or modify an entire call. Common use cases include logging, error handling, and metrics collection.
```ts
const client = createRouterClient(router, {
interceptors: [
async ({ next, path, context }) => {
console.time(path.join('.'))
try {
const output = await next()
return output
}
catch (err) {
console.error(`${path.join('.')}:`, err)
throw err
}
finally {
console.timeEnd(path.join('.'))
}
}
]
})
```
## `.callable` extension
Import `@orpc/server/extensions/callable` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds a `.callable` method to the decorated procedure, allowing you to call it directly like a regular function while still using it as a regular procedure.
```ts usage
const ping = base
.input(z.object({ name: z.string(), }))
.handler(async ({ input }) => `Hello ${input.name}!`)
.callable({
context: async () => ({}), // <- provide initial context if needed, can be async function
interceptors: [], // <- client interceptors
})
const router = {
ping, // <- still use it as a regular procedure
}
const message = await ping({ name: 'World' }) // <- or call it directly
```
```ts setup
import '@orpc/server/extensions/callable'
import { os } from '@orpc/server'
export const base = os
```
## Lifecycle
The diagram below shows how a call flows through a server-side client. By default, middlewares registered before `.input` run before input validation, and the rest run after it:
```mermaid
sequenceDiagram
actor Caller
participant Validator as Input/Output Validator
participant Handler
Caller ->> Validator: input, signal, lastEventId, ...
Note over Validator: interceptors
Note over Validator: middlewares registered before .input
Validator ->> Validator: validate input
Validator -->> Caller: if invalid input
Note over Validator: middlewares registered after .input
Validator ->> Handler: validated input
Handler ->> Handler: execute handler
Handler ->> Validator: output or error
Validator ->> Validator: validate output
Validator ->> Caller: output or error
```
---
# Comparison
Source: https://orpc.dev/docs/comparison
oRPC, [tRPC](https://trpc.io/), and [Hono](https://hono.dev/) all deliver end-to-end typesafe APIs in TypeScript, but they take different approaches. tRPC focuses on RPC for full-stack TypeScript apps, Hono is a general-purpose web framework with an RPC feature, and oRPC combines typesafe RPC with first-class OpenAPI support.
## Features
- ✅ First-class, built-in support
- 🟡 Lacks features or requires third-party integrations
- 🛑 Not supported or not documented
### Type Safety
| Feature | oRPC | tRPC | Hono |
| --- | --- | --- | --- |
| End-to-end typesafe input/output | ✅ | ✅ | ✅ |
| [End-to-end typesafe errors](/docs/client/error-handling) | ✅ | 🟡 | ✅ |
| [End-to-end typesafe File/Blob](/docs/binary-data) | ✅ | 🟡 | 🟡 |
| [End-to-end typesafe Server-Sent Events](/docs/async-iterator-object) | ✅ | ✅ | 🛑 |
| [End-to-end typesafe `ReadableStream`](/docs/binary-data#readablestreamuint8array) | ✅ | 🛑 | 🛑 |
| [Typesafe at scale](#type-checking-performance) | ✅ | ✅ | 🛑 |
oRPC types errors per procedure or globally: throw a plain `ORPCError`, [declare errors](/docs/error-handling) on a single procedure or a shared builder, or define them once and reuse them everywhere via an [error factory](/docs/error-handling#error-factory). tRPC exposes one global error shape, and Hono only types errors returned with an explicit status code. oRPC is also the only one that types binary data in both directions: tRPC and Hono cover uploads but not typed binary responses.
### API Design & OpenAPI
| Feature | oRPC | tRPC | Hono |
| --- | --- | --- | --- |
| Implementation-first approach | ✅ | ✅ | ✅ |
| [Contract-first approach](/docs/contract-first) | ✅ | 🛑 | ✅ |
| [OpenAPI spec generation](/docs/openapi/specification) | ✅ | 🟡 | ✅ |
| [Standard Schema (Zod, Valibot, ArkType, ...)](/docs/integrations/standard-schema) | ✅ | ✅ | 🟡 |
| [Native types (Date, URL, Set, Map, ...)](/docs/rpc/serializer#supported-data-types) | ✅ | 🟡 | 🛑 |
| [Custom serializers](/docs/rpc/serializer#custom-serializers) | ✅ | ✅ | 🛑 |
| [Bracket notation](/docs/openapi/bracket-notation) | ✅ | 🛑 | 🛑 |
| [Lazy router (code splitting, faster cold starts)](/docs/router#lazy-router) | ✅ | ✅ | 🛑 |
oRPC serves the same procedures over its [RPC protocol](/docs/rpc/protocol) and RESTful [OpenAPI endpoints](/docs/openapi/handler) and generates the OpenAPI spec natively, while tRPC and Hono depend on extra packages for OpenAPI. Native types like `Date` or `Map` work out of the box in oRPC; tRPC needs a transformer, and Hono has no serialization layer: what you pass to `c.json()` is what you get.
### Integrations & Ecosystem
| Feature | oRPC | tRPC | Hono |
| --- | --- | --- | --- |
| [WebSockets](/docs/adapters/websocket) | ✅ | ✅ | 🟡 |
| [Hibernation APIs (Cloudflare WebSocket Hibernation, ...)](/docs/integrations/hibernation) | ✅ | 🛑 | 🛑 |
| [Message Port (Electron, browser extensions, workers, ...)](/docs/adapters/message-port) | ✅ | 🟡 | 🛑 |
| [Batch requests](/docs/plugins/batch) | ✅ | ✅ | 🛑 |
| [Pub/Sub publisher (Redis, Upstash, Durable Objects, ...)](/docs/helpers/publisher) | ✅ | 🟡 | 🛑 |
| [Rate limiting (Redis, Upstash, Cloudflare, ...)](/docs/helpers/ratelimit) | ✅ | 🛑 | 🟡 |
| [Client plugins and interceptors](/docs/rpc/link) | ✅ | ✅ | 🛑 |
| [TanStack Query integration (React)](/docs/integrations/tanstack-query) | ✅ | ✅ | 🟡 |
| [TanStack Query integration (Vue, Solid, Svelte, Angular)](/docs/integrations/tanstack-query) | ✅ | 🟡 | 🛑 |
| [SWR and Pinia Colada integrations](/docs/integrations/swr) | ✅ | 🟡 | 🟡 |
| [AI SDK integration](/docs/integrations/ai-sdk) | ✅ | 🛑 | 🛑 |
| [Next.js Server Actions support](/docs/integrations/next) | ✅ | ✅ | 🛑 |
| [Built-in plugins (CORS, CSRF, Retry, ...)](/docs/plugins/cors) | ✅ | 🛑 | ✅ |
| [Built-in helpers (cookie, encryption, signing, ...)](/docs/helpers/cookie) | ✅ | 🛑 | ✅ |
| [NestJS integration](/docs/integrations/nest) | ✅ | 🟡 | 🛑 |
| [OpenTelemetry integration](/docs/integrations/opentelemetry) | ✅ | 🛑 | 🟡 |
Every oRPC integration above is first-party, while tRPC leaves several to community packages and Hono's typed client is a bare fetch wrapper you wire into query libraries yourself.
:::info
Coming from tRPC? Follow the [migration guide](/docs/migrations/from-trpc), or [use both together](/docs/integrations/trpc) during a gradual migration.
:::
## Performance
:::warning
Treat these numbers as reference measurements, not a reason to switch frameworks. All three are fast enough for production. The per-request difference is tiny next to your business logic, database, and network, and results vary across environments and workloads.
:::
All numbers come from [middleapi/orpc-benchmarks](https://github.com/middleapi/orpc-benchmarks) and were measured on the same machine with oRPC 2.0.0-beta.35, tRPC 11.18.0, and Hono 4.13.1. Handlers are no-ops with pass-through validation, so the results measure framework overhead only.
### Throughput
| Scenario | oRPC | tRPC | Hono |
| --- | --- | --- | --- |
| RPC over HTTP (req/s avg) | 18,845 | 4,299 | n/a |
| RPC over WebSocket (req/s avg) | 23,815 | 5,054 | n/a |
| OpenAPI (RESTful) over HTTP (req/s avg) | 18,014 | n/a | 16,932 |
oRPC handles about 4.4x as many requests as tRPC over HTTP and 4.7x over WebSocket. oRPC and Hono are close on RESTful throughput, with oRPC about 6% ahead.
### Clinic Doctor Profiles
Every run also profiles the server with [Clinic.js Doctor](https://clinicjs.org/doctor/):
| Run | CPU (avg) | Memory (RSS) | Detected issues | Full report |
| --- | --- | --- | --- | --- |
| RPC over HTTP · oRPC | ~104% | 73-91 MB | none | [view](https://htmlpreview.github.io/?https://github.com/middleapi/orpc-benchmarks/blob/main/benchmarks/rpc-orpc/.clinic/report.clinic-doctor.html) |
| RPC over HTTP · tRPC | ~113% | 73-247 MB | none | [view](https://htmlpreview.github.io/?https://github.com/middleapi/orpc-benchmarks/blob/main/benchmarks/rpc-trpc/.clinic/report.clinic-doctor.html) |
| RPC over WebSocket · oRPC | ~104% | 71-109 MB | none | [view](https://htmlpreview.github.io/?https://github.com/middleapi/orpc-benchmarks/blob/main/benchmarks/ws-orpc/.clinic/report.clinic-doctor.html) |
| RPC over WebSocket · tRPC | ~41% | 67-75 MB | cpu: performance | [view](https://htmlpreview.github.io/?https://github.com/middleapi/orpc-benchmarks/blob/main/benchmarks/ws-trpc/.clinic/report.clinic-doctor.html) |
| OpenAPI over HTTP · oRPC | ~104% | 73-92 MB | none | [view](https://htmlpreview.github.io/?https://github.com/middleapi/orpc-benchmarks/blob/main/benchmarks/openapi-orpc/.clinic/report.clinic-doctor.html) |
| OpenAPI over HTTP · Hono | ~103% | 71-95 MB | none | [view](https://htmlpreview.github.io/?https://github.com/middleapi/orpc-benchmarks/blob/main/benchmarks/openapi-hono/.clinic/report.clinic-doctor.html) |
The profiles show where the throughput gap comes from. Over HTTP, oRPC serves over 4x tRPC's requests at similar CPU usage (~104% vs ~113%), so its per-request CPU cost is several times lower, and its event loop stays responsive: 0.04 ms average delay versus tRPC's 0.70 ms. Memory stays flat between 71 and 109 MB in every oRPC run, while tRPC's HTTP run climbs to 247 MB, more than double oRPC's highest reading, a sign of per-request allocation pressure. oRPC and Hono profile nearly identically, in line with their close throughput.
tRPC's low WebSocket CPU is not an advantage: the `cpu: performance` issue means the server cannot keep the CPU busy, so throughput is capped elsewhere in the stack.
### Type-Checking Performance
On a large, fully typed project with 3,000 procedures across 1,501 routers, oRPC type-checks about 32% faster than tRPC, uses about 31% less memory, and needs about 19% fewer type instantiations:
| Metric | oRPC | tRPC |
| --- | --- | --- |
| Total time | 1.67s | 2.47s |
| Memory used | 469 MB | 677 MB |
| Instantiations | 2,198,327 | 2,719,560 |
Hono has no column because it does not scale to this size. Its RPC types hit [known limits](https://hono.dev/docs/guides/rpc#known-issues) well before this point and the project becomes effectively unusable, an issue oRPC avoids by design.
### Bundle Size
Bundle sizes for a minimal client and server pair, measured with the same versions. tRPC includes `superjson` to match oRPC's built-in native types, and Hono includes `@hono/node-server` to match the Node.js servers in the other bundles.
| Metric | oRPC | tRPC | Hono |
| --- | --- | --- | --- |
| Minified | 46.4 kB | 83.3 kB | 43.3 kB |
| Minified + gzip | 14.5 kB | 25.6 kB | 16.4 kB |
Measure yourself: [oRPC](https://bundlejs.com/?q=%40orpc%2Fclient%402.0.0-beta.35%2C%40orpc%2Fclient%402.0.0-beta.35%2Ffetch%2C%40orpc%2Fserver%402.0.0-beta.35%2C%40orpc%2Fserver%402.0.0-beta.35%2Fnode&treeshake=%5B%7B+createORPCClient+%7D%5D%2C%5B%7B+RPCLink+%7D%5D%2C%5B%7B+os+%7D%5D%2C%5B%7B+RPCHandler+%7D%5D) · [tRPC](https://bundlejs.com/?q=%40trpc%2Fclient%4011.18.0%2C%40trpc%2Fserver%4011.18.0%2C%40trpc%2Fserver%4011.18.0%2Fadapters%2Fstandalone%2Csuperjson%402.2.6&treeshake=%5B%7B+createTRPCClient%2ChttpLink%2ChttpSubscriptionLink%2CsplitLink+%7D%5D%2C%5B%7B+initTRPC+%7D%5D%2C%5B%7B+createHTTPServer+%7D%5D%2C%5B%7B+default+as+SuperJSON+%7D%5D) · [Hono](https://bundlejs.com/?q=hono%404.13.1%2Chono%404.13.1%2Fclient%2C%40hono%2Fnode-server%402.1.0&treeshake=%5B%7B+Hono+%7D%5D%2C%5B%7B+hc+%7D%5D%2C%5B%7B+serve+%7D%5D)
---
# Context
Source: https://orpc.dev/docs/context
## Initial Context
Use initial context for values that come from the environment. Declare it with `.$context`, then provide it when executing the procedure:
```ts twoslash
import { os } from '@orpc/server'
// ---cut---
const base = os.$context<{ env: { DB_URL: string } }>()
export const getting = base
.handler(async ({ context }) => {
console.log(context.env)
})
```
:::info
When a procedure requires initial context when calling, you must manually pass it:
```ts twoslash
import { call, os } from '@orpc/server'
const base = os.$context<{ env: { DB_URL: string } }>()
const getting = base.handler(async ({ context }) => {})
// ---cut---
const output = await call(getting, undefined, {
context: { // <- initial context must be passed when calling
env: { DB_URL: 'postgres://...' },
},
})
```
:::
### Default Initial Context
To avoid repeating `.$context` declarations, you can define a default initial context type globally.
```ts
declare module '@orpc/server' {
export interface DefaultInitialContext {
env: { DB_URL: string }
}
}
```
## Injected Context
Injected context is injected at runtime through [middleware](/docs/middleware#middleware-context):
```ts twoslash
import { os } from '@orpc/server'
declare const env: { DB_URL: string }
// ---cut---
const base = os.use(async ({ next }) => next({
context: {
env: { DB_URL: env.DB_URL },
},
}))
export const getting = base.handler(async ({ context }) => {
console.log(context.env)
})
```
:::info
When you use middleware context, you do not need to pass context manually when calling:
```ts twoslash
import { call, os } from '@orpc/server'
declare const env: { DB_URL: string }
const base = os.use(async ({ next }) => next({
context: {
env: { DB_URL: env.DB_URL },
},
}))
const getting = base.handler(async ({ context }) => {})
// ---cut---
// no need to pass context manually when calling
const output = await call(getting)
```
:::
## Combining Initial and Injected Context
In many cases, you will use both. Use initial context for environment-specific values, such as database URLs, and injected context for runtime data, such as authenticated users.
```ts twoslash
import { ORPCError, os } from '@orpc/server'
declare function parseJWT(token: string | undefined, secret: string): { userId: number } | null
// ---cut---
const base = os.$context<{ headers: Headers, env: { JWT_SECRET: string } }>()
const requireAuth = base.middleware(async ({ context, next }) => {
const user = parseJWT(
context.headers.get('authorization')?.split(' ')[1],
context.env.JWT_SECRET
)
if (!user) {
throw new ORPCError('UNAUTHORIZED')
}
return next({ context: { user } })
})
const getting = base
.use(requireAuth)
.handler(async ({ context }) => {
console.log(context.env)
console.log(context.user)
})
```
---
# Contract-First
Source: https://orpc.dev/docs/contract-first
In the [Getting Started](/docs/getting-started) guide, each procedure is defined and implemented in one place, and the client's types come from the server's code. Contract-first splits this into two steps. First you write a **contract**: a description of every procedure, its input, and its output, with no logic inside. Then you implement the contract, and TypeScript checks that the implementation matches it exactly.
This is worth the extra step when:
- You want to agree on the API design before writing code, so frontend and backend work can start at the same time.
- The client lives in another repository or team, and it should depend on the API's shape, not on server code.
- You want one source of truth the implementation can never drift away from.
This guide follows the same path as Getting Started, in contract-first order:
1. Define a contract that describes your API.
2. Implement the contract on the server and serve it over HTTP.
3. Call it from a fully typed client built from the contract alone.
## Installation
Contract-first adds one package to the usual setup: `@orpc/contract`, which holds the contract builder. As before, you also need a schema library such as [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), [ArkType](https://arktype.io/), or any other [Standard Schema](https://standardschema.dev/) library.
```package-install
npm install @orpc/contract@beta @orpc/server@beta @orpc/client@beta zod
```
## Define a Contract
A contract describes a procedure without implementing it. Build one with the `oc` builder (short for oRPC contract): declare the input with `.input`, the output with `.output`, and stop there. A contract has no `.handler`. Group contracts into a plain object, just like procedures form a router.
```ts twoslash
import { oc } from '@orpc/contract'
import * as z from 'zod'
const PlanetSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string().optional(),
})
export const listPlanetsContract = oc
.output(z.array(PlanetSchema))
export const findPlanetContract = oc
.input(z.object({ id: z.number() }))
.output(PlanetSchema)
export const createPlanetContract = oc
.input(z.object({ name: z.string(), description: z.string().optional() }))
.output(PlanetSchema)
export const contract = {
planet: {
list: listPlanetsContract,
find: findPlanetContract,
create: createPlanetContract,
},
}
```
A few things to notice:
- `.output` matters here. In Getting Started, the client's result type flows from the handler's return value. A contract has no handler, so `.output` is where that type comes from. Skip it and the result type becomes `unknown`.
- `.output` is not just a type: once implemented, the server validates every response against it at runtime.
- Contracts can also declare typed [errors](/docs/error-handling#typesafe-errors) and [metadata](/docs/metadata). Learn more in the [Procedure Contract documentation](/docs/contract/procedure).
Keep this file free of server code. That is what lets the client import it safely later.
## Implement the Contract
The `implement` function turns your contract into a builder that already knows every procedure's shape. We name it `os` because it works just like the `os` builder from Getting Started, except it is locked to your contract.
```ts twoslash
import { contract } from './shared/contract-first'
// ---cut---
import { implement } from '@orpc/server'
const os = implement(contract)
export const listPlanets = os.planet.list
.handler(async () => {
// replace with your database query
return [
{ id: 1, name: 'Earth' },
{ id: 2, name: 'Mars' },
]
})
export const findPlanet = os.planet.find
.handler(async ({ input }) => {
// replace with your database query
return { id: input.id, name: 'Earth' }
})
export const createPlanet = os.planet.create
.handler(async ({ input }) => {
// replace with your database insert
return { id: 3, ...input }
})
export const router = os.router({
planet: {
list: listPlanets,
find: findPlanet,
create: createPlanet,
},
})
```
The contract does the heavy lifting:
- `os.planet.find` already knows its input and output types, so you only write the logic. Input is still validated before the handler runs.
- Return the wrong shape from a handler and TypeScript reports an error immediately.
- `os.router` checks completeness: forget to implement a procedure, or put it under the wrong key, and the code does not compile.
Implementations can use [middleware](/docs/middleware) and [context](/docs/context) as usual. Learn more in the [Contract Implementation documentation](/docs/contract/implementation).
## Create a Server
Serving the router works exactly as in [Getting Started](/docs/getting-started#create-a-server): `RPCHandler` matches each request to a procedure, validates the input, runs your handler, and sends the result back.
```ts twoslash
///
import { router } from './shared/contract-first'
// ---cut---
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'
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, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000'))
```
:::info
To serve the same contract as a REST API, add [routing metadata](/docs/openapi/routing) such as `.meta(openapi({ method: 'GET', path: '/planets' }))` to the contract itself, serve it with the [OpenAPI Handler](/docs/openapi/handler), and generate an [OpenAPI specification](/docs/openapi/specification) straight from the contract.
:::
## Create a Client
Here is the payoff. Type the client with `RouterContractClient`: it needs only the contract, which contains no business logic, so no server code can leak into the client bundle.
```ts twoslash
import type { contract } from './shared/contract-first'
// ---cut---
import type { RouterContractClient } from '@orpc/contract'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
origin: 'http://127.0.0.1:3000',
url: '/rpc', // <- must match the server's prefix
})
export const orpc: RouterContractClient = createORPCClient(link)
```
:::tip
Because the contract is plain data plus schemas, you can move it into a shared package that both sides depend on, or even [publish a typed client to npm](/docs/recipes/publish-client-to-npm) for third parties. Learn more in the [Client-Side Clients documentation](/docs/client/client-side).
:::
## Call a Procedure
Calling procedures feels exactly the same as in Getting Started:
```ts twoslash
import { client as orpc } from './shared/contract-first'
// ---cut---
const planets = await orpc.planet.list()
const planet = await orpc.planet.find({ id: 1 })
orpc.planet.create
// ^|
//
//
```
Both sides now answer to the contract. The server cannot ship a response the contract does not allow, the client cannot send input the contract rejects, and changing the contract surfaces every affected handler and call site as a compile error.
## Next Steps
- Learn contracts in depth: [Procedure Contract](/docs/contract/procedure) and [Router Contract](/docs/contract/router)
- Add middleware and context to implementations in [Contract Implementation](/docs/contract/implementation)
- Already have an OpenAPI spec? [Generate a contract from it](/docs/contract/generate-from-openapi)
- Keep type checking fast in large codebases with the [Contract Client Factory](/docs/contract/client-factory)
- Expose the contract as a REST API with the [OpenAPI Handler](/docs/openapi/handler) and share it via an [OpenAPI specification](/docs/openapi/specification)
---
# Contract Client Factory
Source: https://orpc.dev/docs/contract/client-factory
A single root [client](/docs/client/client-side) is a great way to get started. As your project grows, though, it can lead to type performance issues and tangled dependencies. Splitting the client into smaller service-level clients can help for a while, but very large codebases can still outgrow that approach.
## Requirements
This pattern depends on one consistency rule: every [procedure contract](/docs/contract/procedure) must define `meta.path`, and that path must exactly match the procedure's location in the root contract.
```ts
import { meta, oc } from '@orpc/contract'
export const procedure = oc
.meta(meta.path(['real', 'path', 'to', 'procedure']))
.input(z.object({ name: z.string() }))
.output(z.object({ message: z.string() }))
```
If you use `['real', 'path', 'to', 'procedure']` as the path, the procedure must be mounted at `real.path.to.procedure` in the root contract. This is required for the pattern to work correctly:
```ts
import { procedure } from './path/to/procedure'
const router = {
real: {
path: {
to: {
procedure,
},
},
}
}
```
## Contract Client Factory
This pattern does not require a single root client. Instead, you configure a client factory that communicates with the server. `createContractClientFactory` accepts an [RPC Link](/docs/rpc/link), an [OpenAPI Link](/docs/openapi/link), or a custom link. It also accepts options similar to [`createORPCClient`](/docs/client/client-side), but with less typesafe because the full contract is not known up front:
```ts
import { createContractClientFactory } from '@orpc/contract'
export const createClient = createContractClientFactory(link, { /** options */})
```
:::warning
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedClientFactory` from `@orpc/openapi` instead of `createContractClientFactory`.
:::
You can then create a client by importing a procedure contract directly in the client:
```ts
import { procedure } from './path/to/procedure'
const client = createClient(procedure)
const output = await client(input, {/** options */})
```
The factory also accepts a [router contract](/docs/contract/router), returning a client that mirrors its shape. Procedure paths follow the router shape, prefixed with a base path derived from the first procedure that defines `meta.path`, so passing a sub-router of the root contract works too:
```ts
import { router } from './path/to/router'
const client = createClient(router)
const output = await client.path.to.procedure(input, {/** options */})
```
### `contractRef`
Some integrations still need a root contract. For example, [OpenAPI Link](/docs/openapi/link) and some plugins depend on one. In those cases, `contractRef` can help:
```ts
import { RouterContract } from '@orpc/contract'
const contractRef: RouterContract = {}
const link = new OpenAPILink(contractRef, {
plugins: [
new PluginRequireContract(contractRef)
]
})
export const createClient = createContractJsonifiedClientFactory(link, { contractRef })
```
The idea behind `contractRef` is simple: every time `createClient` is used, the factory automatically registers every procedure contract inside the passed contract into `contractRef` at its resolved path.
:::info
Some features may not support `contractRef` well. In those cases, import the root contract instead and cast it with `as any` when needed.
:::
## TanStack Query Integration
[TanStack Query Integration](/docs/integrations/tanstack-query) also supports this pattern. First, create a factory that accepts a [contract client factory](#contract-client-factory) and options similar to the [TanStack Query interceptor options](/docs/integrations/tanstack-query#interceptors), but with less type safety because the full contract is not known up front:
```ts
import { createContractUtilsFactory } from '@orpc/tanstack-query'
export const createUtils = createContractUtilsFactory(createClient, { /** options */})
```
:::warning
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/tanstack-query` instead of `createContractUtilsFactory`.
:::
You can then create utilities for each procedure contract:
```ts
import { procedure } from './path/to/procedure'
const utils = createUtils(procedure)
const query = useQuery(utils.queryOptions({/** options */}))
```
Like the [contract client factory](#contract-client-factory), it also accepts a [router contract](/docs/contract/router), returning utilities that mirror its shape:
```ts
import { router } from './path/to/router'
const utils = createUtils(router)
const query = useQuery(utils.path.to.procedure.queryOptions({/** options */}))
```
## Pinia Colada Integration
[Pinia Colada Integration](/docs/integrations/pinia-colada) also supports this pattern. First, create a factory that accepts a [contract client factory](#contract-client-factory) and options similar to the [Pinia Colada interceptor options](/docs/integrations/pinia-colada#interceptors), but with less type safety because the full contract is not known up front:
```ts
import { createContractUtilsFactory } from '@orpc/pinia-colada'
export const createUtils = createContractUtilsFactory(createClient, { /** options */})
```
:::warning
If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/pinia-colada` instead of `createContractUtilsFactory`.
:::
You can then create utilities for each procedure contract:
```ts
import { procedure } from './path/to/procedure'
const utils = createUtils(procedure)
const query = useQuery(utils.queryOptions({/** options */}))
```
Like the [contract client factory](#contract-client-factory), it also accepts a [router contract](/docs/contract/router), returning utilities that mirror its shape:
```ts
import { router } from './path/to/router'
const utils = createUtils(router)
const query = useQuery(utils.path.to.procedure.queryOptions({/** options */}))
```
---
# Generate Contract from OpenAPI
Source: https://orpc.dev/docs/contract/generate-from-openapi
## Overview
If you already have an [OpenAPI specification](https://swagger.io/specification/), you can generate the contract with [Hey API](https://heyapi.dev/)'s `orpc` plugin instead of defining it manually. Each operation in the specification becomes a [procedure contract](/docs/contract/procedure) with its route, input, and output.
:::warning
The Hey API `orpc` plugin is currently beta and may introduce breaking changes while the integration stabilizes. Until the next stable Hey API release, oRPC v2 output requires the `next` release tag.
:::
## Example
Install Hey API:
```package-install
npm install -D @hey-api/openapi-ts@next
```
Create an `openapi-ts.config.ts` file pointing at your specification. It can be a local file or a URL:
```ts openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'https://get.heyapi.dev/hey-api/backend',
output: 'src/contract',
plugins: [
{
name: 'orpc',
compatibilityVersion: '2',
validator: 'zod',
},
],
})
```
Then run:
```bash
npx @hey-api/openapi-ts
```
This writes `orpc.gen.ts` and `zod.gen.ts` to `src/contract`, with one procedure contract per operation and a `contract` router combining them all. In this example, `zod` generates the validation schemas:
```ts src/contract/orpc.gen.ts
import { oc } from '@orpc/contract'
import { openapi } from '@orpc/openapi'
import * as z from 'zod'
import { zAddPetBody, zAddPetResponse } from './zod.gen'
export const addPet = oc
.meta(openapi({
inputStructure: 'detailed',
method: 'POST',
path: '/pet',
tags: ['pet'],
}))
.input(z.object({ body: zAddPetBody }))
.output(zAddPetResponse)
export const contract = {
addPet,
// ...every other operation
}
```
The generated files import `@orpc/contract`, `@orpc/openapi`, and `zod`, so install them if you have not already:
```package-install
npm install @orpc/contract@beta @orpc/openapi@beta zod
```
For all configuration options and plugin behavior, see the [Hey API `orpc` plugin documentation](https://heyapi.dev/docs/openapi/typescript/plugins/orpc/v2).
## What To Do Next
Once the contract is generated, what you do next depends on how you want to use it:
- Implement the contract on your own server with [Contract Implementation](/docs/contract/implementation).
- Call an existing OpenAPI-compliant server through a typesafe client with [OpenAPI Link](/docs/openapi/link).
---
# Contract Implementation
Source: https://orpc.dev/docs/contract/implementation
## Implementer
The `implement` function turns a contract into an implementer. Use it to build procedures, routers, and create middleware with full type safety.
```ts twoslash
import { contract } from './shared/planet'
// ---cut---
import { implement } from '@orpc/server'
const implementer = implement(contract)
.$context<{ something?: string }>() // <- define initial context
implementer.planet.list
// ^|
//
//
//
//
//
```
### Initial Context
Use `.$context` to declare the initial context required for a procedure to execute.
Learn more in the [Context Documentation](/docs/context).
## Implementing Procedures
Define a `.handler` for a procedure contract to provide its business logic.
```ts twoslash
import { contract } from './shared/planet'
import { implement } from '@orpc/server'
const implementer = implement(contract)
const requireAuth = implementer.middleware(({ next }) => next())
// ---cut---
const listPlanet = implementer.planet.list
.use(requireAuth) // <- Apply authentication middleware
.handler(({ input }) => {
// Your logic for listing planets
return []
})
```
:::info
If middleware needs to wrap validation, apply it at the router level instead. In this example, use `implementer.use` to apply it globally or `implementer.planet.use` to apply it to the `planet` router before `.list`.
```ts
const listPlanet = implementer
.planet
.use(requireAuth) // <- middleware wraps validation
.list
.handler(({ input }) => {
// Your logic for listing planets
return []
})
```
:::
## Implementing Routers
Create the root router with `.router` to assemble your API. This enables full type-checking and runtime contract enforcement.
```ts
const router = implementer.router({
planet: {
list: listPlanet,
find: findPlanet,
create: createPlanet,
},
})
```
### Extending Router
Like a normal [router](/docs/router), an implementer router can also be extended with shared behavior. For example, you can apply authentication middleware to every procedure:
```ts
const router = implementer.use(requireAuth).router({
planet: {
list: listPlanet,
find: findPlanet,
create: createPlanet,
},
})
```
:::danger
If you apply middleware with `.use` at both the router and procedure levels, it may run more than once. That duplication can hurt performance. To avoid redundant middleware execution, see the [Dedupe Middleware](/docs/recipes/dedupe-middleware) recipe.
:::
## Creating Middleware
The implementer can also create [middleware](/docs/middleware). Middleware created this way can infer the contract's [typesafe errors](/docs/error-handling#typesafe-errors). If not all contracts define the same errors, use the `in` operator to check that an error exists before using it.
```ts
const ratelimit = implementer.middleware(async ({ next, errors }) => {
if ('TOO_MANY_REQUESTS' in errors) {
// Apply rate limiting only when TOO_MANY_REQUESTS is defined by the contract.
if (isRatelimitReached) {
throw errors.TOO_MANY_REQUESTS()
}
}
return next()
})
```
:::info
You do not have to create middleware from the implementer. Any type-compatible middleware can be used.
:::
## Reusability
Each implementer call creates a new instance, which avoids reference issues and makes contracts easy to reuse and extend.
```ts
const pub = implementer // Base setup for procedures that publish
const authed = implementer.use(requireAuth) // Extends 'pub' with authentication
const listPlanets = pub.planet.list.handler(({ input }) => {
// Your logic for listing planets without authentication
return []
})
const createPlanet = authed.planet.create.handler(({ input }) => {
// Your logic for creating planets with authentication
return { }
})
```
This pattern helps prevent duplication while maintaining flexibility.
---
# Procedure Contract
Source: https://orpc.dev/docs/contract/procedure
## Overview
```ts twoslash
import { z } from 'zod'
import type { AnyMetaPlugin } from '@orpc/contract'
declare const someMeta: AnyMetaPlugin
// ---cut---
import { oc } from '@orpc/contract'
const example = oc
.meta(someMeta) // <- attach metadata
.errors({ NOT_FOUND: {} }) // <- define errors
.input(z.object({ id: z.number(), name: z.string() })) // <- input validation
.output(z.object({ id: z.number(), name: z.string() })) // <- output validation
```
:::info
All of these chains are optional. You can create an empty contract with just `oc`.
:::
## Metadata
Use `.meta` to attach metadata to a contract. Middleware and plugins can read it later when you implement the contract. Learn more in the [Metadata documentation](/docs/metadata).
## Typesafe Errors
Use `.errors` to define the errors a contract can produce. These errors can be thrown from handlers or middleware when you implement the contract and remain properly typed on the client. Learn more in the [Typesafe Error Handling documentation](/docs/error-handling#typesafe-errors).
## Input/Output Validation
oRPC supports [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), [Arktype](https://arktype.io/), and any other [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library for validation.
:::info
Unlike a [procedure](/docs/procedure), a contract has no `.handler` chain. If you want the client to infer the output type correctly, define `.output`. Otherwise, the output type will be `unknown`.
:::
### Multiple Schemas
`.input` and `.output` can be called multiple times. Each call adds another schema instead of replacing an earlier one, and the value must satisfy all of them.
```ts
const base = oc
.input(z.object({ name: z.string() }))
.output(z.looseObject({ name: z.string() }))
const example = base
.input(z.object({ id: z.number() }))
.output(z.looseObject({ id: z.number() }))
```
Object input schemas compose into a single flat value instead of being piped into each other, so a contract can extend a base contract without repeating its fields. Output schemas are still piped. Learn more in the [Procedure documentation](/docs/procedure#multiple-schemas).
### `type` Utility
For simple use cases without external libraries, use oRPC's built-in `type` utility. It takes a mapping function as its first argument:
```ts
import { type } from '@orpc/contract'
const example = oc
.input(type<{ value: number }>())
.output(type<{ value: number }, number>(({ value }) => value))
```
## Reusability
Each builder call creates a new instance, which avoids reference issues and makes contracts easy to reuse and extend.
```ts
const pub = oc // Base setup for procedures that publish
const authed = pub.meta(requireAuthMeta) // Extends 'pub' with authentication
const pubExample = pub
.input(z.object({ name: z.string() }))
const authedExample = authed
.input(z.object({ id: z.number() }))
```
This pattern helps prevent duplication while maintaining flexibility.
---
# Router Contract
Source: https://orpc.dev/docs/contract/router
:::info
A standalone [procedure contract](/docs/contract/procedure) is also a router contract, so you can use the same features with individual procedure contracts.
:::
## Overview
Define a router contract as a plain JavaScript object where each key maps to a procedure contract:
```ts twoslash
import { z } from 'zod'
// ---cut---
import { oc } from '@orpc/contract'
const ping = oc.output(z.string())
const pong = oc.output(z.string())
export const router = {
ping,
pong,
nested: { ping, pong }
}
```
:::warning
For compatibility, do not use these router keys: `then`, `bind`, `call`, `apply`, `valueOf`, `toString`, `toJSON`.
:::
## Extending Router
You can extend a router contract with shared configuration, such as attaching metadata to every procedure:
```ts
const router = oc.meta(requireAuthMeta).router({
ping,
pong,
nested: {
ping,
pong,
}
})
```
## Router to Contract
A normal [router](/docs/router) can be used as a contract router as long as it does not include a [lazy router](/docs/router#lazy-router). If necessary, use `unlazyRouter` to fully resolve it and make it contract-compatible.
```ts
import { unlazyRouter } from '@orpc/server'
const compatibleContract = await unlazyRouter(router)
```
### Safely Importing Router on the Client
Sometimes you need to import the contract on the client, for example when using [OpenAPI Link](/docs/openapi/link). If you derive the contract from a [router](/docs/router), importing it directly can be heavy and may expose internal logic. To avoid this, follow the steps below to safely minify and export the contract.
1. **Minify the Contract Router and Export to JSON**
```ts
import fs from 'node:fs'
import { unlazyRouter } from '@orpc/server'
import { minifyRouterContract } from '@orpc/contract'
const compatibleContract = await unlazyRouter(router)
const minifiedRouter = minifyRouterContract(compatibleContract)
fs.writeFileSync('./contract.json', JSON.stringify(minifiedRouter))
```
::: info
`minifyRouterContract` preserves only the metadata needed by the client; all other data is stripped out.
:::
2. **Import the Contract JSON on the Client Side**
```ts
import contract from './contract.json' // [!code highlight]
const link = new OpenAPILink(contract as typeof router)
```
::: info
Cast `contract` to `typeof router` to preserve type safety, since standard schema types cannot be serialized to JSON and must be cast manually.
:::
:::tip
Instead of regenerating the JSON file manually whenever the router changes, you can run the same steps in a build-time macro. See [OpenAPI Link Without Runtime Imports](/docs/openapi/link-without-runtime-imports).
:::
## Utilities
:::info
A standalone [procedure contract](/docs/contract/procedure) is also a router contract, so these utilities work with individual procedure contracts too.
:::
### Infer Router Contract Inputs
Infers the input type of each procedure contract in a router contract.
```ts twoslash
import type { contract } from './shared/planet'
// ---cut---
import type { InferRouterContractInputs } from '@orpc/contract'
export type Inputs = InferRouterContractInputs
type FindPlanetInput = Inputs['planet']['find']
```
### Infer Router Contract Outputs
Infers the output type of each procedure contract in a router contract.
```ts twoslash
import type { contract } from './shared/planet'
// ---cut---
import type { InferRouterContractOutputs } from '@orpc/contract'
export type Outputs = InferRouterContractOutputs
type FindPlanetOutput = Outputs['planet']['find']
```
### Infer Router Contract Error Map
Collects the error maps from every procedure contract in a router contract into a single type.
```ts twoslash
import type { contract } from './shared/planet'
// ---cut---
import type { InferRouterContractErrorMap } from '@orpc/contract'
export type ErrorMap = InferRouterContractErrorMap
```
### Infer Router Contract Errors
Infers the throwable errors each procedure contract in a router contract can describe.
```ts twoslash
import type { contract } from './shared/planet'
// ---cut---
import type { InferRouterContractErrors } from '@orpc/contract'
export type Errors = InferRouterContractErrors
type FindPlanetError = Errors['planet']['find']
```
### Infer Router Contract Error
Infers all possible throwable errors the entire router contract can describe. This is useful when you want a single type for contract-wide error handling.
```ts twoslash
import type { contract } from './shared/planet'
// ---cut---
import type { InferRouterContractError } from '@orpc/contract'
export type ContractError = InferRouterContractError
```
---
# Ecosystem
Source: https://orpc.dev/docs/ecosystem
:::info
Built something on top of oRPC? [Open a pull request](https://github.com/middleapi/orpc/edit/main/apps/content/docs/ecosystem.mdx) to add it here. These packages are maintained by their authors, not by the oRPC core team, so report issues in their own repositories.
:::
## AI
## Tooling
---
# Error Handling
Source: https://orpc.dev/docs/error-handling
## `ORPCError` Class
`ORPCError` is the standard error type in oRPC. It includes a `code`, plus optional `message` and `data` fields.
:::danger
`message` and `data` are sent to the client. Do not include sensitive information in either field.
:::
```ts twoslash
declare const notFound: boolean
// ---cut---
import { ORPCError, os } from '@orpc/server'
const rateLimitMiddleware = os.middleware(async ({ next }) => {
throw new ORPCError('RATE_LIMITED', {
message: 'You are being rate limited',
data: { retryAfter: 60 }
})
return next()
})
const example = os
.use(rateLimitMiddleware)
.handler(async ({ input }) => {
if (notFound) {
throw new ORPCError('NOT_FOUND')
}
})
```
## Typesafe Errors
For end-to-end type safety, define your errors with `.errors`. This lets the client infer each error's shape and handle it safely. You can use any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate error data.
:::danger
`message` and `data` are sent to the client. Do not include sensitive information in either field.
:::
```ts twoslash
import { os } from '@orpc/server'
import * as z from 'zod'
declare const notFound: boolean
// ---cut---
const rateLimitMiddleware = os
.errors({
RATE_LIMITED: {
data: z.object({
retryAfter: z.number(),
}),
},
})
.middleware(async ({ next, errors }) => {
throw errors.RATE_LIMITED({
message: 'You are being rate limited',
data: { retryAfter: 60 }
})
return next()
})
const exampleProcedure = os
.use(rateLimitMiddleware)
.errors({
NOT_FOUND: {
message: 'The resource was not found', // <- default message
},
})
.handler(async ({ input, errors }) => {
if (notFound) {
throw errors.NOT_FOUND()
}
})
```
:::tip
You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like `UNAUTHORIZED` or `RATE_LIMITED`, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance.
:::
### ORPCError Compatibility
If you cannot access the `errors` object, for example in a utility function or another module, you can still throw `ORPCError`. oRPC will try to convert it to the matching typesafe error when its `code` and `data` match a defined error. If no match is found, it is treated as an unknown error.
```ts
const exampleProcedure = os
.errors({
NOT_FOUND: {
message: 'The resource was not found',
},
})
.handler(async ({ errors }) => {
throw errors.NOT_FOUND()
// Treated as errors.NOT_FOUND because the code and data match
throw new ORPCError('NOT_FOUND')
// Treated as an unknown error because it does not match any defined error
throw new ORPCError('BAD_REQUEST')
})
```
## Error Factory
An error factory lets you define an error once and reuse it anywhere, keeping error handling consistent across your project.
```ts
import { error } from '@orpc/server'
const RateLimitedError = error('RATE_LIMITED', {
/**
* Optional default message, can be overridden when constructing an error.
*/
message: 'You are being rate limited',
/**
* Optional schema used to type and validate the error data.
* Must be a synchronous schema.
*/
data: z.object({
retryAfter: z.number(),
}),
})
const procedure = os
.handler(async () => {
throw new RateLimitedError({ data: { retryAfter: 60 } })
})
```
:::tip
You can also register error factories in `.errors`. This makes them part of the [typesafe errors](#typesafe-errors) flow and visible in generated specifications.
```ts
const procedure = os
.errors({
[RateLimitedError.code]: RateLimitedError,
})
```
:::
### `instanceof` Support
An error factory class supports `instanceof` checks with full type narrowing. It matches any `ORPCError` with the same `code` whose `data` passes the schema.
```ts
if (err instanceof RateLimitedError) {
console.log(err.data.retryAfter)
}
```
## ORPC Error Codes
By default, oRPC allows any string as an error code and suggests common HTTP codes like `NOT_FOUND` and `UNAUTHORIZED`. You can override this with your own set of allowed error codes for better type safety and consistency.
```ts
declare module '@orpc/server' { // or '@orpc/client'
interface Registry {
ORPCErrorCode: 'NOT_FOUND' | 'UNAUTHORIZED' | 'RATE_LIMITED' | 'MY_CUSTOM_ERROR' | (string & {})
}
}
```
With this configuration, only `NOT_FOUND`, `UNAUTHORIZED`, `RATE_LIMITED`, and `MY_CUSTOM_ERROR` will be suggested as error codes. The `(string & {})` fallback ensures you can still use any string value when needed.
## Using Custom Error Classes
You do not have to use `ORPCError` directly in your business logic. You can throw your own error classes and convert them to `ORPCError` in middleware or interceptors.
:::info
By default, oRPC can convert non-`ORPCError` into an `ORPCError` with code `INTERNAL_SERVER_ERROR`, or leave them unchanged depending on the client you are using.
:::
```ts
class MyCustomError extends Error {
}
const customErrorConverterMiddleware = os.middleware(async ({ next }) => {
try {
return await next()
}
catch (err) {
if (err instanceof MyCustomError) {
throw new ORPCError('MY_CUSTOM_ERROR', { message: err.message, cause: err })
}
throw err
}
})
```
## Client Error Handling
To learn how to handle errors on the client side, see the [Client Error Handling documentation](/docs/client/error-handling).
---
# Getting Started
Source: https://orpc.dev/docs/getting-started
Building an API usually means defining HTTP endpoints on the server, calling them from the client, and keeping both sides' types in sync by hand. oRPC removes that gap: you write plain TypeScript functions on the server, and clients call them like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step.
This guide takes the shortest path through oRPC:
1. Define procedures (the functions of your API) and group them into a router.
2. Serve the router over HTTP.
3. Call it from a fully typed client.
:::tip[Prefer a running example?]
Open one of the [playgrounds](/docs/playgrounds) in StackBlitz and follow along in a complete project.
:::
## Installation
Install the server and client packages, plus a schema library for validating input at runtime. This guide uses [Zod](https://zod.dev/), but [Valibot](https://valibot.dev/), [ArkType](https://arktype.io/), and any other [Standard Schema](https://standardschema.dev/) library work the same way.
```package-install
npm install @orpc/server@beta @orpc/client@beta zod
```
## Define a Router
A procedure is a function that clients can call remotely. Build one with the `os` builder (short for oRPC server): optionally describe the input it accepts with a schema, then implement it with `.handler`. A router is a plain object that groups procedures and gives each one its calling path, like `planet.list`.
```ts twoslash
import { os } from '@orpc/server'
import * as z from 'zod'
export const listPlanets = os
.handler(async () => {
// replace with your database query
return [
{ id: 1, name: 'Earth' },
{ id: 2, name: 'Mars' },
]
})
export const findPlanet = os
.input(z.object({ id: z.number() }))
.handler(async ({ input }) => {
// replace with your database query
return { id: input.id, name: 'Earth' }
})
export const createPlanet = os
.input(z.object({ name: z.string(), description: z.string().optional() }))
.handler(async ({ input }) => {
// replace with your database insert
return { id: 3, ...input }
})
export const router = {
planet: {
list: listPlanets,
find: findPlanet,
create: createPlanet,
},
}
```
A few things to notice:
- `.input` validates each call before your handler runs and types `input` inside it. `listPlanets` skips it: a procedure without `.input` simply takes no arguments.
- No `.output` schema is needed: the client's result type flows straight from the handler's return type.
- Procedures can do much more: share [middleware](/docs/middleware), require [context](/docs/context) such as an authenticated user, and declare typed [errors](/docs/error-handling). Learn more in the [Procedure documentation](/docs/procedure).
## Create a Server
Clients reach your router through an HTTP server. `RPCHandler` does the translation: it matches each incoming request to a procedure, validates the input, runs your handler, and sends the result back. This example uses [Node's built-in HTTP module](/docs/adapters/node-http). The same router also runs on Bun and Deno through the [Fetch API adapter](/docs/adapters/fetch-api), and on [Cloudflare Workers](/docs/adapters/cloudflare-workers).
```ts twoslash
///
import { router } from './shared/getting-started'
// ---cut---
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'
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, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000'))
```
Every procedure is now reachable under the `/rpc` prefix. Requests that are not oRPC calls fall through, so you can handle them yourself, here with a plain 404. For CORS, logging, and other options, see the [RPC Handler documentation](/docs/rpc/handler).
:::info
oRPC can also serve the same router as a REST API. Add [routing metadata](/docs/openapi/routing) to your procedures, serve them with the [OpenAPI Handler](/docs/openapi/handler), and generate an [OpenAPI specification](/docs/openapi/specification) from the same definitions.
:::
## Create a Client
On the client, `RPCLink` is the counterpart of `RPCHandler`: it turns your function calls into HTTP requests. Pass it to `createORPCClient`, and type the result with `RouterClient` so the client knows every procedure, its input, and its output.
```ts twoslash
import type { router } from './shared/getting-started'
// ---cut---
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
origin: 'http://127.0.0.1:3000',
url: '/rpc', // <- must match the server's prefix
})
export const orpc: RouterClient = createORPCClient(link)
```
:::tip
The client only needs the router's type. Import it with `import type`, or export the `RouterClient` type from the server, so no server code ends up in your client bundle. Learn more in the [Client-Side Clients documentation](/docs/client/client-side).
:::
When the caller runs in the same process as the server, for example during server-side rendering, skip HTTP entirely with a [server-side client](/docs/client/server-side).
## Call a Procedure
That is the whole setup. Call your procedures like local functions and let your editor do the rest:
```ts twoslash
import { client as orpc } from './shared/getting-started'
// ---cut---
const planets = await orpc.planet.list()
const planet = await orpc.planet.find({ id: 1 })
orpc.planet.create
// ^|
//
//
```
`planet` is typed from the handler's return value, invalid input is rejected before your handler runs, and renaming a procedure on the server is a compile error in the client. There is no generated code to keep in sync.
## Next Steps
- Learn the building blocks in depth: [Procedure](/docs/procedure) and [Router](/docs/router)
- Add authentication and logging with [Middleware](/docs/middleware) and [Context](/docs/context), and reject calls with typed [errors](/docs/error-handling)
- Stream typed events over Server-Sent Events (SSE) with [AsyncIteratorObject](/docs/async-iterator-object)
- Expose the same router as a REST API with the [OpenAPI Handler](/docs/openapi/handler) and generate its [OpenAPI specification](/docs/openapi/specification)
- Define your API as a [contract first](/docs/contract-first), then let TypeScript enforce the implementation
- Integrate with your stack: [TanStack Query](/docs/integrations/tanstack-query), [SWR](/docs/integrations/swr), [Pinia Colada](/docs/integrations/pinia-colada), [Next.js](/docs/integrations/next), and [NestJS](/docs/integrations/nest)
---
# Base64Url Helpers
Source: https://orpc.dev/docs/helpers/base64url
## Basic Usage
```ts twoslash
import { decodeBase64url, encodeBase64url } from '@orpc/server/helpers'
const originalText = 'Hello World'
const textBytes = new TextEncoder().encode(originalText)
const encodedData = encodeBase64url(textBytes)
const decodedBytes = decodeBase64url(encodedData)
const decodedText = new TextDecoder().decode(decodedBytes) // 'Hello World'
```
:::info
The `decodeBase64url` accepts `undefined` or `null` as encoded value and returns `undefined` for invalid inputs, enabling seamless handling of optional data.
:::
---
# Cookie Helpers
Source: https://orpc.dev/docs/helpers/cookie
## Basic Usage
```ts twoslash
import { deleteCookie, getCookie, setCookie } from '@orpc/server/helpers'
const reqHeaders = new Headers()
const resHeaders = new Headers()
setCookie(resHeaders, 'sessionId', 'abc123', {
secure: true,
maxAge: 3600
})
deleteCookie(resHeaders, 'sessionId')
const sessionId = getCookie(reqHeaders, 'sessionId')
```
:::info
Both helpers accept `undefined` as headers for seamless integration with plugins like [Request Headers](/docs/plugins/request-headers) or [Response Headers](/docs/plugins/response-headers).
:::
## Security with Signing and Encryption
Combine cookies with [signing](/docs/helpers/signing) or [encryption](/docs/helpers/encryption) for enhanced security:
```ts twoslash
import { getCookie, setCookie, sign, unsign } from '@orpc/server/helpers'
const secret = 'your-secret-key'
const reqHeaders = new Headers()
const resHeaders = new Headers()
setCookie(resHeaders, 'sessionId', await sign('abc123', secret), {
httpOnly: true,
secure: true,
maxAge: 3600
})
const signedSessionId = await unsign(getCookie(reqHeaders, 'sessionId'), secret)
```
---
# Encryption Helpers
Source: https://orpc.dev/docs/helpers/encryption
:::warning
Encryption secures data content but has performance trade-offs compared to [signing](/docs/helpers/signing). It requires more CPU resources and processing time. For edge runtimes like [Cloudflare Workers](https://developers.cloudflare.com/workers/), ensure you have sufficient CPU time budget (recommend >200ms per request) for encryption operations.
:::
## Basic Usage
```ts twoslash
import { decrypt, encrypt } from '@orpc/server/helpers'
const secret = 'your-encryption-key'
const sensitiveData = 'user-email@example.com'
const encryptedData = await encrypt(sensitiveData, secret)
// 'Rq7wF8...' (base64url encoded, unreadable)
const decryptedData = await decrypt(encryptedData, secret)
// 'user-email@example.com'
```
:::info
The `decrypt` helper accepts `undefined` or `null` as encrypted value and returns `undefined` for invalid inputs, enabling seamless handling of optional data.
:::
---
# Form Data Helpers
Source: https://orpc.dev/docs/helpers/form-data
## `parseFormData`
Parses HTML form data using [bracket notation](/docs/openapi/bracket-notation) to deserialize complex nested objects and arrays.
```ts twoslash
import { parseFormData } from '@orpc/openapi/helpers'
const form = new FormData()
form.append('name', 'John')
form.append('user[email]', 'john@example.com')
form.append('user[hobbies][]', 'reading')
form.append('user[hobbies][]', 'gaming')
const parsed = parseFormData(form)
// Result:
// {
// name: 'John',
// user: {
// email: 'john@example.com',
// hobbies: ['reading', 'gaming']
// }
// }
```
## `getIssueMessage`
Extracts validation error messages from [standard schema](https://standardschema.dev/) issues using [bracket notation](/docs/openapi/bracket-notation) paths.
```ts twoslash
import { getIssueMessage } from '@orpc/openapi/helpers'
const error = {
data: {
issues: [
{
path: ['user', 'email'],
message: 'Invalid email format'
}
]
}
}
const emailError = getIssueMessage(error, 'user[email]')
// Returns: 'Invalid email format'
const tagError = getIssueMessage(error, 'user[tags][]')
// Returns error message for any array item
const anyError = getIssueMessage('anything', 'path')
// Returns undefined if cannot find issue
```
:::warning
The `getIssueMessage` utility works with any data type but requires validation errors to follow the [standard schema issue format](https://standardschema.dev/#the-specifications). It looks for issues in the `data.issues` property. If you [customize validation errors](/docs/recipes/validation-customization#custom-validation-errors), store them elsewhere, or modify the issue format, `getIssueMessage` may not work as expected.
:::
## Usage Example
```tsx
import { getIssueMessage, parseFormData } from '@orpc/openapi/helpers'
export function ContactForm() {
const [error, setError] = useState()
const handleSubmit = (form: FormData) => {
try {
const data = parseFormData(form)
// Process structured data
}
catch (error) {
setError(error)
}
}
return (
)
}
```
---
# Publisher Helpers
Source: https://orpc.dev/docs/helpers/publisher
## Installation
```package-install
npm install @orpc/publisher@beta
```
## Basic Usage
The core concept is the `Publisher` interface, which defines a standard way to publish events and subscribe to them. You can create your own publisher or use one of the provided adapters for popular storage backends. The `publish` method accepts an event name and payload, while `subscribe` lets you listen to specific events using either callback or iterator styles.
```ts twoslash
import { MemoryPublisher } from '@orpc/publisher/memory'
import { os } from '@orpc/server'
import * as z from 'zod'
// ---cut---
const publisher = new MemoryPublisher<{
'something-updated': {
id: string
}
}>()
const live = os
.handler(async function* ({ input, signal, lastEventId }) {
const iterator = publisher.subscribe('something-updated', { signal, lastEventId })
for await (const payload of iterator) {
// Handle payload here or yield directly to client
yield payload
}
})
const publish = os
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => {
await publisher.publish('something-updated', { id: input.id })
})
```
:::tip
The publisher supports both static and dynamic event names.
```ts
const publisher = new MemoryPublisher>()
```
:::
## Adapters
| Name | Resume Support | Adapter for |
| ------------------- | -------------- | -------------------------------------------------------------------------------- |
| `MemoryPublisher` | ✅ | In-memory storage |
| `RedisPublisher` | ✅ | [Redis](https://github.com/redis/redis) |
| `UpstashPublisher` | ✅ | [Upstash Redis](https://github.com/upstash/redis-js) |
| `BunRedisPublisher` | ✅ | [Bun's Redis](https://bun.com/docs/runtime/redis) |
| `DurablePublisher` | ✅ | [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) |
```ts memory
import { MemoryPublisher } from '@orpc/publisher/memory'
const publisher = new MemoryPublisher({
resume: {
/**
* Whether event resume support is enabled.
*
* When enabled, published events are temporarily stored so new
* subscribers can resume from a previous position using `lastEventId`.
*
* @default false
*/
enabled: false,
/**
* How long (in seconds) to retain events for resume.
*
* Expired events are cleaned up lazily for performance reasons, so
* some events may remain available slightly longer than this period.
*
* @default 300 (5 min)
*/
seconds: 300
}
})
```
```ts redis
import { createClient } from 'redis'
import { RedisPublisher } from '@orpc/publisher/redis'
const client = createClient({ url: 'redis://localhost:6379' })
// RedisPublisher lazily connects to Redis when needed.
// You can still call `client.connect()` manually, but it is optional.
await client.connect()
const publisher = new RedisPublisher(client, {
/**
* Redis subscriber instance.
* Pub/Sub takes over the connection, so a client with subscriptions
* cannot execute commands and must use a dedicated connection.
*
* @default client.duplicate()
*/
subscriber: client.duplicate(),
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Serializer for serialize and deserialize payloads.
*
* @default RPCJsonSerializer
*/
serializer: undefined,
resume: {
/**
* Whether event resume support is enabled.
*
* When enabled, published events are temporarily stored so new
* subscribers can resume from a previous position using `lastEventId`.
*
* @default false
*/
enabled: false,
/**
* How long (in seconds) to retain events for resume.
*
* Expired events are cleaned up lazily for performance reasons, so
* some events may remain available slightly longer than this period.
*
* @default 300 (5 min)
*/
seconds: 300
}
})
```
```ts upstash
import { Redis } from '@upstash/redis'
import { UpstashPublisher } from '@orpc/publisher/upstash'
const redis = Redis.fromEnv()
const publisher = new UpstashPublisher(redis, {
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Serializer for serialize and deserialize payloads.
*
* @default RPCJsonSerializer
*/
serializer: undefined,
resume: {
/**
* Whether event resume support is enabled.
*
* When enabled, published events are temporarily stored so new
* subscribers can resume from a previous position using `lastEventId`.
*
* @default false
*/
enabled: false,
/**
* How long (in seconds) to retain events for resume.
*
* Expired events are cleaned up lazily for performance reasons, so
* some events may remain available slightly longer than this period.
*
* @default 300 (5 min)
*/
seconds: 300
}
})
```
```ts bun
import { BunRedisPublisher } from '@orpc/bun'
import { redis } from 'bun'
const publisher = new BunRedisPublisher(redis, {
/**
* Redis subscriber instance.
* Pub/Sub takes over the connection, so a client with subscriptions
* cannot execute commands and must use a dedicated connection.
*
* @default redis.duplicate() (lazily created on first listen)
*/
subscriber: redis.duplicate(),
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Serializer for serialize and deserialize payloads.
*
* @default RPCJsonSerializer
*/
serializer: undefined,
resume: {
/**
* Whether event resume support is enabled.
*
* When enabled, published events are temporarily stored so new
* subscribers can resume from a previous position using `lastEventId`.
*
* @default false
*/
enabled: false,
/**
* How long (in seconds) to retain events for resume.
*
* Expired events are cleaned up lazily for performance reasons, so
* some events may remain available slightly longer than this period.
*
* @default 300 (5 min)
*/
seconds: 300
}
})
```
```ts cloudflare
import { DurablePublisher, DurablePublisherObject } from '@orpc/cloudflare'
export class PublisherDO extends DurablePublisherObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env, {
resume: {
/**
* Whether event resume support is enabled.
*
* When enabled, published events are temporarily stored so new
* subscribers can resume from a previous position using `lastEventId`.
*
* @default false
*/
enabled: false,
/**
* How long (in seconds) to retain events for resume.
*
* Expired events are cleaned up lazily for performance reasons, so
* some events may remain available slightly longer than this period.
*
* @default 300 (5 min)
*/
seconds: 300,
/**
* Interval (in seconds) between cleanup checks for the Durable Object.
*
* At each interval, verify whether the Durable Object is inactive
* (no active WebSocket connections and no stored events). If inactive, all
* data is deleted to free resources; otherwise, another check is scheduled.
*
* @default 6 * 60 * 60 (6 hours)
*/
cleanupIntervalSeconds: 6 * 60 * 60,
/**
* Prefix for the resume storage table schema.
* Used to avoid naming conflicts with other tables in the same Durable Object.
*
* @default 'orpc:'
*/
schemaPrefix: 'orpc:'
}
})
}
}
export default {
async fetch(request, env) {
const publisher = new DurablePublisher(env.PUBLISHER_DON, {
/**
* Prefix for events, to avoid naming conflicts with other publishers in the same Durable Object Namespace.
*
* @default ''
*/
prefix: '',
/**
* Serializer for serialize and deserialize payloads.
*
* @default RPCJsonSerializer
*/
serializer: undefined,
/**
* Custom function to get the Durable Object stub for publishing.
*
* @default ((namespace, event) => namespace.getByName(event))
*/
getStubByName: (namespace, event) => namespace.getByName(event)
})
},
}
```
## Resume Missing Events
Some adapters can resume events missed while a subscriber is offline. This feature is usually disabled by default, but you can enable it when creating the publisher. When enabled, the publisher automatically manages event ids and attempts to deliver events since the last event id provided by the subscriber.
```ts
const publisher = new MemoryPublisher({
resume: {
enabled: true, // Enable resuming missed events
seconds: 60 * 5, // TTL in seconds
}
})
const iterator = publisher.subscribe('something-updated', {
signal,
lastEventId, // The publisher will attempt to deliver missed events since this event id
})
```
:::warning
When resume is enabled, the publisher manages event ids automatically. This means:
- Any event id provided during publishing is ignored
- When subscribing, you must preserve and forward the event id when yielding custom payloads
```ts
import { getEventMeta, withEventMeta } from '@orpc/server'
const live = os
.handler(async function* ({ input, signal, lastEventId }) {
const iterator = publisher.subscribe('something-updated', { signal, lastEventId })
for await (const payload of iterator) {
// Preserve event id when yielding custom payloads
const id = getEventMeta(payload)?.id
yield withEventMeta({ custom: 'value' }, { id })
}
})
const publish = os
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => {
// The event id 'this-will-be-ignored' will be replaced by the publisher
await publisher.publish(
'something-updated',
withEventMeta({ id: input.id }, { id: 'this-will-be-ignored' })
)
})
```
:::
### Client Reconnection
On the client, you can use the [Retry Plugin](/docs/plugins/retry), which automatically controls and passes `lastEventId` to the server when reconnecting. Alternatively, you can manage `lastEventId` manually:
```ts
import { getEventMeta } from '@orpc/client'
let lastEventId: string | undefined
while (true) {
try {
const iterator = await client.live('input', { lastEventId })
for await (const payload of iterator) {
lastEventId = getEventMeta(payload)?.id // Update lastEventId
console.log(payload)
}
}
catch {
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second before retrying
}
}
```
---
# Rate Limit Helpers
Source: https://orpc.dev/docs/helpers/ratelimit
## Installation
```package-install
npm install @orpc/ratelimit@beta
```
## Basic Usage
The core concept is the `RateLimiter` interface, which defines a standard way to check and enforce rate limits. You can create your own custom limiter or use one of the provided adapters for popular storage backends. The `limit` method accepts a key and an optional `weight` value, which defaults to `1`, so a single request can consume multiple points.
```ts twoslash
import { MemoryRateLimiter } from '@orpc/ratelimit/memory'
// ---cut---
import { ORPCError } from '@orpc/server'
const limiter = new MemoryRateLimiter({
maxRequests: 5,
window: 60000,
})
const result = await limiter.limit('user:123', { weight: 2 })
if (!result.success) {
throw new ORPCError('TOO_MANY_REQUESTS', {
data: {
limit: result.limit,
remaining: result.remaining,
reset: result.reset,
},
})
}
```
## Adapters
The package includes adapters for multiple storage backends and runtimes.
Each adapter might require `maxRequests` and `window` to configure the limit, along with adapter specific options.
| Name | Blocking Mode | Adapter for |
| ----------------------- | ------------- | --------------------------------------------------------------------------------------------------------- |
| `MemoryRateLimiter` | ✅ | In-memory storage |
| `RedisRateLimiter` | ✅ | [Redis](https://github.com/redis/redis) |
| `UpstashRateLimiter` | ✅ | [Upstash Rate Limit](https://www.npmjs.com/package/@upstash/ratelimit) |
| `BunRedisRateLimiter` | ✅ | [Bun's Redis](https://bun.com/docs/runtime/redis) |
| `CloudflareRateLimiter` | ❌ | [Cloudflare's Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) |
```ts memory
import { MemoryRateLimiter } from '@orpc/ratelimit/memory'
const limiter = new MemoryRateLimiter({
/**
* Maximum number of requests allowed within the window.
*/
maxRequests: 10,
/**
* The duration of the fixed window in milliseconds.
*/
window: 60000,
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
})
```
```ts redis
import { RedisRateLimiter } from '@orpc/ratelimit/redis'
import { createClient } from 'redis'
const client = createClient({ url: 'redis://localhost:6379' })
// RedisRateLimiter lazily connects to Redis when needed.
// You can still call `client.connect()` manually, but it is optional.
await client.connect()
const limiter = new RedisRateLimiter(client, {
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Maximum number of requests allowed within the window.
*/
maxRequests: 10,
/**
* The duration of the fixed window in milliseconds.
*/
window: 60000,
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
})
```
````ts upstash
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { UpstashRateLimiter } from '@orpc/ratelimit/upstash'
const redis = Redis.fromEnv()
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '60 s'),
prefix: 'orpc:', // Optional key prefix
})
const limiter = new UpstashRateLimiter(ratelimit, {
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
/**
* For the MultiRegion setup we do some synchronizing in the background, after returning the current limit.
* Or when analytics is enabled, we send the analytics asynchronously after returning the limit.
* In most case you can simply ignore this.
*
* On Vercel Edge or Cloudflare workers, you might need `.bind` before assign:
* ```ts
* const ratelimiter = new UpstashRateLimiter(ratelimit, {
* waitUntil: ctx.waitUntil.bind(ctx),
* })
* ```
*/
waitUntil: undefined
})
````
```ts bun
import { BunRedisRateLimiter } from '@orpc/bun'
import { redis } from 'bun'
const limiter = new BunRedisRateLimiter(redis, {
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Maximum number of requests allowed within the window.
*/
maxRequests: 10,
/**
* The duration of the fixed window in milliseconds.
*/
window: 60000,
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
})
```
```ts cloudflare
import { CloudflareRateLimiter } from '@orpc/cloudflare'
export default {
async fetch(request, env) {
const limiter = new CloudflareRateLimiter(env.MY_RATE_LIMITER, {
/**
* The prefix to use for cloudflare ratelimit.
*
* @default ''
*/
prefix: ''
})
}
}
```
### Blocking Mode
Some adapters support blocking mode, which waits until capacity becomes available instead of rejecting requests immediately.
```ts
const limiter = new MemoryRateLimiter({
maxRequests: 10,
window: 60000,
blockingUntilReady: {
enabled: true, // Disabled by default
timeout: 5000, // Wait up to 5 seconds
},
})
```
## Ratelimit Middleware
The `ratelimit` helper creates middleware that enforces rate limits for [procedures](/docs/procedure).
```ts
import { ratelimit, RateLimiter } from '@orpc/ratelimit'
const procedure = os
.$context<{ ratelimiter: RateLimiter }>()
.input(z.object({ email: z.email() }))
.use(
ratelimit({
limiter: ({ context }) => context.ratelimiter,
key: ({ context }, input) => `login:${input.email}`,
weight: 1, // Optional weight for each request, default is 1
}),
)
.handler(({ input }) => {
return { success: true }
})
const ratelimiter = new MemoryRateLimiter({
maxRequests: 10,
window: 60000,
})
const result = await call(
procedure,
{ email: 'user@example.com' },
{ context: { ratelimiter } }
)
```
:::info[Automatic Deduplication]
When the same `limiter` and `key` combination is used multiple times in a single request chain, the `ratelimit` middleware performs the rate limit check only once. This behavior follows the [Dedupe Middleware](/docs/recipes/dedupe-middleware) recipe. To disable deduplication, set `dedupe: false`.
:::
:::tip[Conditional Limiter]
You can choose different limiters dynamically based on the request context:
```ts
const premiumLimiter = new MemoryRateLimiter({
maxRequests: 100,
window: 60000,
})
const standardLimiter = new MemoryRateLimiter({
maxRequests: 10,
window: 60000,
})
const result = await call(
procedure,
{ email: 'user@example.com' },
{
context: {
ratelimiter: isPremiumUser ? premiumLimiter : standardLimiter,
},
},
)
```
:::
## Handler Plugin
The `RateLimitHandlerPlugin` automatically adds HTTP rate limiting headers (`RateLimit-*` and `Retry-After`) to responses when used with [Ratelimit Middleware](#ratelimit-middleware). This lets clients inspect the current limit state and know when they can retry after hitting a limit.
```ts
import { RateLimitHandlerPlugin } from '@orpc/ratelimit'
const handler = new RPCHandler(router, {
plugins: [
new RateLimitHandlerPlugin(),
],
})
```
:::info
You can combine this plugin with [Retry After Plugin](/docs/plugins/retry-after) to enable automatic client-side retries based on server rate limiting headers.
:::
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one.
:::
---
# Signing Helpers
Source: https://orpc.dev/docs/helpers/signing
:::info
Signing is faster than [encryption](/docs/helpers/encryption) but users can view the original data.
:::
## Basic Usage
```ts twoslash
import { getSignedValue, sign, unsign } from '@orpc/server/helpers'
const secret = 'your-secret-key'
const userData = 'user123'
const signedValue = await sign(userData, secret)
// 'user123.oneQsU0r5dvwQFHFEjjV1uOI_IR3gZfkYHij3TRauVA'
// ↑ Original data is visible to users
const verifiedValue = await unsign(signedValue, secret) // 'user123'
// Extract value without verification
const extractedValue = getSignedValue(signedValue) // 'user123'
```
:::info
The `unsign` and `getSignedValue` helpers accept `undefined` or `null` as signed value and return `undefined` for invalid inputs, enabling seamless handling of optional data.
:::
---
# AI SDK Integration
Source: https://orpc.dev/docs/integrations/ai-sdk
:::warning
This documentation requires AI SDK v7.0.0 or later. For a refresher, review the [AI SDK documentation](https://ai-sdk.dev/docs).
:::
## Transport
Use oRPC as the transport for AI SDK streams, sending them as either an [AsyncIteratorObject](/docs/async-iterator-object) or a [`ReadableStream`](/docs/binary-data#readablestreamuint8array). The examples below use the `AsyncIteratorObject` approach.
### Server
Use `streamToAsyncIteratorObject` to convert AI SDK streams into [AsyncIteratorObject](/docs/async-iterator-object)s.
```ts
import { os, streamToAsyncIteratorObject, type } from '@orpc/server'
import { convertToModelMessages, streamText, toUIMessageStream, UIMessage } from 'ai'
import { google } from '@ai-sdk/google'
export const chat = os
.input(type<{ chatId: string, messages: UIMessage[] }>())
.handler(async ({ input }) => {
const result = streamText({
model: google('gemini-2.5-flash'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(input.messages),
})
return streamToAsyncIteratorObject(
toUIMessageStream(result),
)
})
```
### Client
On the client side, convert the `AsyncIteratorObject` back to a stream using `asyncIteratorToUnproxiedDataStream` or `asyncIteratorToStream`.
```tsx
import { useState } from 'react'
import { useChat } from '@ai-sdk/react'
import { asyncIteratorToUnproxiedDataStream } from '@orpc/client'
import { client } from './client'
export function Example() {
const { messages, sendMessage, status } = useChat({
transport: {
async sendMessages(options) {
return asyncIteratorToUnproxiedDataStream(await client.chat({
chatId: options.chatId,
messages: options.messages,
}, { signal: options.abortSignal }))
},
reconnectToStream(options) {
throw new Error('Unsupported')
},
},
})
const [input, setInput] = useState('')
return (
<>
{messages.map(message => (
))}
>
)
}
```
:::info
The `reconnectToStream` function is not supported by default, which is fine for most use cases. If you need reconnection support, implement it similar to `sendMessages` with custom reconnection logic.
:::
:::info
Prefer `asyncIteratorToUnproxiedDataStream` over `asyncIteratorToStream`.
AI SDK internally uses `structuredClone`, which doesn't support proxied data.
oRPC may proxy events for [metadata](/docs/client/async-iterator-object#event-metadata), so unproxy before passing to AI SDK.
:::
## Tool Implementer
Implements a [procedure contract](/docs/contract/procedure) as an [AI SDK Tool](https://ai-sdk.dev/docs/foundations/tools) by leveraging existing contract definitions.
```ts
import { aiSdkTool, implementToolFactory } from '@orpc/ai-sdk'
const getWeatherContract = oc
.meta(aiSdkTool({ // Base AI SDK tool options
description: 'Get the weather in a location',
metadata: { source: 'weather-service' }
}))
.input(z.object({
location: z.string().describe('The location to get the weather for'),
}))
.output(z.object({
location: z.string().describe('The location the weather is for'),
temperature: z.number().describe('The temperature in Celsius'),
}))
const implementTool = implementToolFactory()
const getWeatherTool = implementTool(getWeatherContract, {
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
// ...add any additional AI SDK tool options or overrides here
})
```
:::info
Standard [procedures](/docs/procedure) are also compatible with [procedure contracts](/docs/contract/procedure).
:::
:::info
The `aiSdkTool` [metadata](/docs/metadata) attaches base AI SDK tool options that every tool created from the procedure/contract inherits. If applied multiple times, later calls override matching keys from earlier ones.
:::
## Tool Factory
Converts a [procedure](/docs/procedure) into an [AI SDK Tool](https://ai-sdk.dev/docs/foundations/tools) by leveraging existing procedure definitions.
```ts
import { aiSdkTool, createToolFactory } from '@orpc/ai-sdk'
import { os } from '@orpc/server'
import { z } from 'zod'
const getWeatherProcedure = os
.meta(aiSdkTool({ // Base AI SDK tool options
description: 'Get the weather in a location',
metadata: { source: 'weather-service' }
}))
.input(z.object({
location: z.string().describe('The location to get the weather for'),
}))
.output(z.object({
location: z.string().describe('The location the weather is for'),
temperature: z.number().describe('The temperature in Celsius'),
}))
.handler(async ({ input }) => ({
location: input.location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}))
const createTool = createToolFactory({
context: {}, // provide initial context if needed
interceptors: [], // oRPC interceptors if needed
})
const getWeatherTool = createTool(getWeatherProcedure, {
// ...add any additional AI SDK tool options or overrides here
})
```
### Streaming Tool Outputs
When a procedure outputs an [AsyncIteratorObject](/docs/async-iterator-object), either validated with `asyncIteratorObject` or produced by an `async function*` handler, the resulting tool streams every event as a [preliminary tool result](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#preliminary-tool-results): each event replaces the tool output in the UI, and the last event becomes the final tool result sent to the model.
```ts
import { asyncIteratorObject, os } from '@orpc/server'
const deployProcedure = os
.input(z.object({ app: z.string() }))
.output(asyncIteratorObject(
z.object({
status: z.string(),
url: z.string().optional().describe('Available once the deploy finishes'),
}),
))
.handler(async function* ({ input }) {
yield { status: 'building' }
yield { status: 'uploading' }
yield { status: 'ready', url: `https://${input.app}.example.com` }
})
const deployTool = createTool(deployProcedure)
```
---
# ArkType Integration
Source: https://orpc.dev/docs/integrations/arktype
:::info
[ArkType](https://arktype.io/) implements [Standard Schema](/docs/integrations/standard-schema), so procedures accept ArkType types without any converter. The converter below is only needed by tools that consume JSON Schema, such as OpenAPI generation and Smart Coercion.
:::
## Installation
```package-install
npm install @orpc/arktype@beta arktype
```
## JSON Schema Converter
`ArkTypeToJsonSchemaConverter` wraps [ArkType's built-in toJsonSchema](https://arktype.io/docs/type-api#tojsonschema) and adds support for additional types such as `bigint` and `Date`. Use it with tools such as the [OpenAPI Generator](/docs/openapi/specification#openapi-generator) and [Smart Coercion](/docs/plugins/smart-coercion). It accepts the same options as ArkType's `toJsonSchema`, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/arktype/src/converter.ts) and ArkType's [JSON Schema configuration docs](https://arktype.io/docs/configuration#tojsonschema) for implementation details.
```ts
import { OpenAPIGenerator } from '@orpc/openapi'
import { ArkTypeToJsonSchemaConverter } from '@orpc/arktype'
const generator = new OpenAPIGenerator({
converters: [new ArkTypeToJsonSchemaConverter()],
})
```
:::tip
Enable the `cache` option to reuse conversion results when the same schema instance is converted repeatedly. When enabled, repeated conversions return the same JSON schema object, so treat the results as immutable.
```ts
const converter = new ArkTypeToJsonSchemaConverter({ cache: true })
```
:::
### Reusable Types
A common pattern is defining reusable or recursive types using scopes. The converter preserves them in `$defs`, which `OpenAPIGenerator` can then [hoist](/docs/openapi/specification#hoisting-defs) into `components.schemas`.
```ts
import { scope } from 'arktype'
const types = scope({
Planet: {
name: 'string',
neighbors: 'Planet[]',
},
})
const PlanetSchema = types.export().Planet
```
---
# Better Auth Integration
Source: https://orpc.dev/docs/integrations/better-auth
Use your [Better Auth](https://better-auth.com/) instance with oRPC's [context](/docs/context) and [middleware](/docs/middleware). No extra package is needed.
:::tip
You may need to forward Better Auth's [response headers](https://better-auth.com/docs/concepts/api#getting-headers), such as a refreshed session cookie. The [Response Headers Plugin](/docs/plugins/response-headers) can help unless you use the [Batch Plugin](/docs/plugins/batch).
:::
## Resolve the Session in Middleware
The [Request Headers Plugin](/docs/plugins/request-headers) exposes request headers as `context.reqHeaders`. The middleware loads the session from them and rejects unauthenticated calls. Public procedures use the base directly. Each protected call performs its own lookup, including every sub-request of a [batch](/docs/plugins/batch).
```ts
import type { RequestHeadersHandlerPluginContext } from '@orpc/server/plugins'
import { ORPCError, os } from '@orpc/server'
interface ServerContext extends RequestHeadersHandlerPluginContext {}
const base = os.$context()
const requireSession = base.middleware(async ({ context, next }) => {
const session = await auth.api.getSession({
headers: context.reqHeaders ?? new Headers(),
})
if (!session) {
throw new ORPCError('UNAUTHORIZED')
}
return next({ context: { session } })
})
const protectedProcedure = base.use(requireSession)
const router = {
ping: base.handler(() => ({ message: 'pong' })),
me: protectedProcedure.handler(({ context }) => ({
id: context.session.user.id,
name: context.session.user.name,
})),
}
```
`context.session` is Better Auth's full result with `user` and `session`. Its type is inferred from your auth instance, so additional fields stay available.
Only a missing session becomes `UNAUTHORIZED`. Other errors from `getSession` propagate to oRPC's [error handling](/docs/error-handling).
`reqHeaders` is `undefined` without the plugin, such as in [server-side calls](/docs/client/server-side). The empty `Headers` fallback carries no session cookie, so `getSession` returns `null` and protected calls return `UNAUTHORIZED`. Pass `reqHeaders` in the initial context to authenticate such calls.
## Lazily Load and Share the Session
If your server resolves the session itself, pass a lazy getter into the initial context instead of the session. The lookup runs at most once per request and only when a procedure asks for it. This includes [batch](/docs/plugins/batch) requests, where every sub-request shares the getter. The same getter can also serve the rest of your request handling.
```ts
import { ORPCError, os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
type Session = Awaited>
function once(fn: () => Promise): () => Promise {
let promise: Promise | undefined
return () => {
promise ??= fn()
return promise
}
}
const base = os.$context<{ getSession: () => Promise }>()
const requireSession = base.middleware(async ({ context, next }) => {
const session = await context.getSession()
if (!session) {
throw new ORPCError('UNAUTHORIZED')
}
return next({ context: { session } })
})
const protectedProcedure = base.use(requireSession)
const router = {
greeting: base.handler(async ({ context }) => {
const session = await context.getSession()
return { message: `Hello, ${session?.user.name ?? 'guest'}` }
}),
me: protectedProcedure.handler(({ context }) => ({
id: context.session.user.id,
name: context.session.user.name,
})),
}
const handler = new RPCHandler(router)
export async function fetch(request: Request): Promise {
const getSession = once(() => auth.api.getSession({ headers: request.headers }))
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: { getSession },
})
return matched ? response : new Response('Not Found', { status: 404 })
}
```
---
# Cloudflare Workers Traces Integration
Source: https://orpc.dev/docs/integrations/cloudflare-traces
:::warning
This guide assumes familiarity with [Cloudflare Workers Traces](https://developers.cloudflare.com/workers/observability/traces/). Review the official documentation if needed.
:::
## Installation
```package-install
npm install @orpc/cloudflare@beta
```
## Setup
Enable traces in your Wrangler configuration, then register `CloudflareTracer` once at module scope. oRPC then records the same spans as the [OpenTelemetry integration](/docs/integrations/opentelemetry), nested under the spans Workers create automatically.
```jsonc title="wrangler.jsonc"
{
"observability": {
"traces": {
"enabled": true
}
}
}
```
```ts
import { experimental_CloudflareTracer as CloudflareTracer } from '@orpc/cloudflare'
new CloudflareTracer().enable()
```
:::info
`CloudflareTracer` covers both handlers and links, so a Worker that calls another oRPC server also records client spans. oRPC uses a single tracer, so do not enable `ORPCInstrumentation` alongside it.
:::
:::tip
During local development, [Local Explorer](https://developers.cloudflare.com/workers/local-development/local-explorer/) shows these spans without deploying: press `e` in `wrangler dev`, or open `/cdn-cgi/local/explorer` on the Vite dev server.
:::
## Middleware Span
oRPC creates a span for each [middleware](/docs/middleware) execution. Use `tracing.getActiveSpan()` to add attributes to it:
```ts
import { tracing } from 'cloudflare:workers'
export const someMiddleware = os.middleware(async (ctx, next) => {
tracing.getActiveSpan()?.setAttribute('someAttribute', 'someValue')
return next()
})
Object.defineProperty(someMiddleware, 'name', {
value: 'someName',
})
```
:::tip
Define the `name` property on your middleware to improve span naming and make traces easier to read.
:::
## Limitations
Workers Traces still lack some APIs oRPC relies on, so:
- Request spans are not renamed to the procedure path. The path is still recorded in the `rpc.method` attribute.
- Streamed inputs and outputs record no `yielded` and `enqueued` events.
- Trace context propagation is not configurable by oRPC.
---
# Effect Integration
Source: https://orpc.dev/docs/integrations/effect
:::warning
This guide assumes familiarity with [Effect](https://effect.website/). Review the official documentation if needed.
:::
## Installation
```package-install
npm install @orpc/experimental-effect@beta effect@beta
```
## Effectful Handlers
`handlerGen` allows you to write effectful handlers using generator functions. Inside the generator, you can yield Effect operations, and `handlerGen` will handle the execution and error handling for you.
```ts twoslash
import { os } from '@orpc/server'
// ---cut---
import { handlerGen } from '@orpc/experimental-effect'
import { Effect } from 'effect'
const procedure = os.handler(handlerGen(function* ({ input, context }) {
// You can use Effect's features here, such as concurrency, error handling, etc.
const result = yield* Effect.promise(() => Promise.resolve(5))
return result
}))
```
### `.effect` extension
Import `@orpc/experimental-effect/extensions/effect` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds an `.effect` method to the builder so you can write effectful handlers directly.
```ts usage
const procedure = base.effect(function* ({ input, context }) {
// You can use Effect's features here, such as concurrency, error handling, etc.
const result = yield* Effect.promise(() => Promise.resolve(5))
return result
})
```
```ts setup
import '@orpc/experimental-effect/extensions/effect'
import { os } from '@orpc/server'
export const base = os
```
### Effect Services
You can provide Effect services through the oRPC context in a typesafe way with `WithEffectContext` and `effect/context`:
```ts twoslash
import { call, os } from '@orpc/server'
// ---cut---
import { handlerGen, WithEffectContext } from '@orpc/experimental-effect'
import { Context, Effect } from 'effect'
class Random extends Context.Service<
Random,
{
readonly next: Effect.Effect
}
>()('MyRandomService') {}
interface ServerContext extends WithEffectContext {}
const procedure = os
.$context()
.handler(handlerGen(function* ({ input, context }) {
const random = yield* Random
const result = yield* random.next
return result
}))
const random = await call(procedure, undefined, {
context: {
'effect/context': Context.empty().pipe(
Context.add(Random, {
next: Effect.succeed(Math.random()),
}),
)
}
})
```
:::info
You can also extend the Effect context with [middleware](/docs/middleware):
```ts
const procedure = os
.$context()
.use(({ context, next }) => {
return next({
context: {
'effect/context': context['effect/context'].pipe(
Context.add(AdditionService, {}),
)
}
})
})
.handler(handlerGen(function* ({ input, context }) {
const additionService = yield* AdditionService
}))
```
:::
### Error Handling
This integration preserves the original error whenever possible. If you call `Effect.fail(error)`, the error is forwarded to [middleware](/docs/middleware) and interceptors, just like a regular thrown error.
To customize this behavior, wrap the effect before execution using `effect/wrap` in the context:
```ts
import { Context, Effect } from 'effect'
interface ServerContext extends WithEffectContext {}
export async function fetch(request: Request) {
const { matched, response } = await handler.handle(request, {
context: {
'effect/context': Context.empty(),
'effect/wrap': (effect, opts) => effect.pipe(
Effect.catchCause((cause) => {
})
),
}
})
if (matched) {
return response
}
return new Response('Not Found', { status: 404 })
}
```
:::info
For app level error handling, we recommend [middleware](/docs/middleware) or interceptors.
:::
### Typesafe Errors
An `ORPCError` that fails the effect, such as `yield* Effect.fail(new ORPCError(...))`, is thrown from the handler exactly like a thrown `ORPCError` in a regular handler. Define your errors with `.errors` and fail with `errors.X(...)` to make them [typesafe](/docs/error-handling#typesafe-errors) on the client:
```ts
const procedure = os
.errors({
NOT_FOUND: {
message: 'The resource you are looking for does not exist',
},
})
.handler(handlerGen(function* ({ errors }) {
if (resourceNotFound) {
yield* Effect.fail(errors.NOT_FOUND())
}
return 'Success'
}))
const [error, result] = await safe(call(procedure))
if (isDefinedError(error)) {
// typesafe error handling
}
```
### Catching ORPCErrors
Use `catchORPCError` to recover from every `ORPCError` failure in the error channel of an effect, or `catchORPCErrorCode` and `catchORPCErrorCodes` to recover from specific codes only. Recovered errors are excluded from the resulting effect, and other failures re-fail with their original cause:
```ts
import { catchORPCError, catchORPCErrorCode, catchORPCErrorCodes } from '@orpc/experimental-effect'
import { Effect } from 'effect'
const recovered = program.pipe(
catchORPCError(error => Effect.succeed(`caught ${error.code}`)),
)
const fallback = program.pipe(
catchORPCErrorCode('NOT_FOUND', error => Effect.succeed(error.data.id)),
)
const handled = program.pipe(
catchORPCErrorCodes({
NOT_FOUND: error => Effect.succeed(error.data.id),
CONFLICT: error => Effect.succeed(error.message),
}),
)
```
:::info
All utilities support data-first `catchORPCError(program, handler)` and data-last `program.pipe(catchORPCError(handler))` styles.
:::
## Effectful Middleware
`middlewareGen` allows you to write effectful middleware using generator functions. Inside the generator, you can yield Effect operations and `yield* next()` to continue the chain. Downstream failures land in the error channel of `next`, so you can recover from them with Effect.
```ts twoslash
import { os } from '@orpc/server'
// ---cut---
import { middlewareGen } from '@orpc/experimental-effect'
import { Effect } from 'effect'
const procedure = os
.$context<{ auth: boolean }>()
.use(middlewareGen(function* ({ context, next }) {
const startedAt = yield* Effect.sync(() => Date.now())
return yield* next({ context: { startedAt } })
}))
.handler(({ context }) => context.startedAt)
```
## Client Calls
`createEffectClient` wraps any oRPC client, whether [server-side](/docs/client/server-side) or [client-side](/docs/client/client-side), so every procedure returns a lazy Effect instead of a promise, ready to `yield*` inside Effect generators. The output becomes the success value, and errors land in the error channel with their original types preserved, ready for utilities like `catchORPCErrorCodes`:
```ts
import { catchORPCErrorCodes, createEffectClient } from '@orpc/experimental-effect'
import { Effect } from 'effect'
const effectClient = createEffectClient(client)
const program = Effect.gen(function* () {
const planet = yield* effectClient.planet.find({ id: 1 })
return planet.name
}).pipe(
catchORPCErrorCodes({
NOT_FOUND: error => Effect.succeed('unknown'),
}),
)
```
You can also combine `Effect.catchIf` with [isDefinedError](/docs/client/error-handling#using-safe-and-isdefinederror) to recover from every defined error in a typesafe way:
```ts
import { isDefinedError } from '@orpc/client'
import { Effect } from 'effect'
const recovered = effectClient.planet.find({ id: 1 }).pipe(
Effect.catchIf(isDefinedError, (error) => {
// error is fully typed here
return Effect.succeed(null)
}),
)
```
:::info
The effects are lazy: the client is invoked each time the effect runs, so they work naturally with `Effect.retry`. Interrupting the effect aborts the underlying call.
:::
## Effect Schema
oRPC natively supports [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec), and [Effect Schema](https://effect.website/docs/schema/introduction/) implements that spec through [Schema.toStandardSchemaV1](https://effect.website/docs/schema/standard-schema/):
```ts
import { Schema } from 'effect'
const procedure = os
.input(Schema.toStandardSchemaV1(Schema.Struct({ name: Schema.String })))
.handler(handlerGen(function* ({ input, context }) {
return `Hello ${input.name}!`
}))
```
### `.input` and `.output` Extensions
Import `@orpc/experimental-effect/extensions/input-output` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This lets you define `.input` and `.output` directly with Effect Schema:
```ts usage
const procedure = base
.input(Schema.Struct({ name: Schema.String }))
.output(Schema.Struct({ greeting: Schema.String }))
.handler(handlerGen(function* ({ input, context }) {
return { greeting: `Hello ${input.name}!` }
}))
```
```ts setup
import '@orpc/experimental-effect/extensions/input-output'
import { os } from '@orpc/server'
export const base = os
```
:::info
You can also use these extensions with the [contract builder](/docs/contract/procedure).
:::
### JSON Schema Converter
This integration also provides `EffectSchemaToJsonSchemaConverter`, built on top of [Effect Schema to JSON Schema](https://effect.website/docs/schema/json-schema/). You can use it with tools such as the [OpenAPI Generator](/docs/openapi/specification#openapi-generator):
```ts
import { EffectSchemaToJsonSchemaConverter } from '@orpc/experimental-effect'
const generator = new OpenAPIGenerator({
converters: [new EffectSchemaToJsonSchemaConverter()],
})
```
## OpenTelemetry Integration
First, set up the [oRPC OpenTelemetry integration](/docs/integrations/opentelemetry). Then instrument your Effect to work seamlessly with OpenTelemetry by providing `TracingLive` through `effect/wrap` in the context. This makes Effect tracing equivalent to OpenTelemetry tracing:
```ts
import { Resource, Tracer } from '@effect/opentelemetry'
import { Context, Effect, Layer } from 'effect'
interface ServerContext extends WithEffectContext {}
const TracingLive = Tracer.layerGlobal.pipe(
Layer.provide(Resource.layerFromEnv()),
)
export async function fetch(request: Request) {
const { matched, response } = await handler.handle(request, {
context: {
'effect/context': Context.empty(),
'effect/wrap': (effect, opts) => effect.pipe(Effect.provide(TracingLive)),
}
})
if (matched) {
return response
}
return new Response('Not Found', { status: 404 })
}
```
---
# Evlog Integration
Source: https://orpc.dev/docs/integrations/evlog
:::warning
This guide assumes familiarity with [Evlog](https://evlog.dev/). Review the official documentation if needed.
:::
## Installation
```package-install
npm install @orpc/evlog@beta evlog@beta
```
## Setup
Use `EvlogHandlerPlugin` to instrument your handler with structured logs, request tracking, and error monitoring.
```ts twoslash
import { RPCHandler } from '@orpc/server/fetch'
import { router } from './shared/planet'
// ---cut---
import { EvlogHandlerPlugin } from '@orpc/evlog'
const handler = new RPCHandler(router, {
plugins: [
new EvlogHandlerPlugin({
drain: undefined, // <- custom Evlog drain (optional)
plugins: [], // <- additional Evlog plugins (optional)
logAbort: true, // <- log when requests are aborted (disabled by default)
}),
],
})
```
:::info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one.
:::
## Error Logging Levels
Errors thrown from your procedures set the wide event's level to match their intent: `info` for abort errors, `warn` for `ORPCError` instances since they represent deliberate rejections, and `error` for everything else, including `ORPCError` with the `INTERNAL_SERVER_ERROR` code. Use the `procedureErrorLevel` option to customize this behavior:
```ts
const plugin = new EvlogHandlerPlugin({
procedureErrorLevel: (error, level) => {
if (level === 'error' && error instanceof ExpectedError) {
return 'warn'
}
return level
},
})
```
## Using the Logger in Your Code
This plugin supports using [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) to access the logger throughout a request and enrich the final [wide event](https://www.evlog.dev/learn/wide-events#uselogger-retrieving-the-request-logger). It is the most convenient way to use Evlog's full feature set. If your runtime does not support AsyncLocalStorage, you can still [access the logger from the context](#without-asynclocalstorage).
```ts title="business logic"
import { createLoggerStorage } from '@orpc/evlog/node'
/**
* Pass `storage` to the plugin configuration.
* Call `useLogger` inside a procedure to access the request logger.
*/
export const { storage, useLogger } = createLoggerStorage()
const procedure = os
.handler(async () => {
const logger = useLogger() // [!code highlight]
logger?.set({ user: { id: 123, name: 'John Doe' } }) // [!code highlight]
await logger.fork('child-procedure', () => {
const logger = useLogger() // [!code highlight]
})
return { success: true }
})
```
```ts title="handler setup"
const handler = new RPCHandler(router, {
plugins: [
new EvlogHandlerPlugin({
storage, // <- pass the storage to the plugin
}),
],
})
```
### Without AsyncLocalStorage
If you do not want to use AsyncLocalStorage, or your runtime does not support it, you can still read the logger from the context.
```ts
import { getLogger, LoggerContext } from '@orpc/evlog'
interface ServerContext extends LoggerContext {} // [!code highlight]
const procedure = os
.$context()
.handler(({ context }) => {
const logger = getLogger(context) // [!code highlight]
logger?.set({ user: { id: 123, name: 'John Doe' } }) // [!code highlight]
return { success: true }
})
```
---
# Hibernation Integration
Source: https://orpc.dev/docs/integrations/hibernation
## Installation
```package-install
npm install @orpc/hibernation@beta
```
## Setup
```ts
import { HibernationHandlerPlugin } from '@orpc/hibernation'
const handler = new RPCHandler(router, {
plugins: [
new HibernationHandlerPlugin(),
],
})
```
:::warning
When combined with the [Batch Plugin](/docs/plugins/batch), make sure procedures that return a `HibernationAsyncIteratorClass` are excluded from batching (e.g. via the batch link plugin's `filter` option), because hibernation cannot work through batched responses.
:::
## Usage
The plugin provides `HibernationAsyncIteratorClass` and `encodeHibernationRPCEvent` to help you return an [Async Iterator Object](/docs/async-iterator-object) that utilizes the Hibernation APIs.
1. Return a `HibernationAsyncIteratorClass` from your handler
```ts
import { HibernationAsyncIteratorClass } from '@orpc/hibernation'
const base = os.$context<{ ws: WebSocket }>()
export const onMessage = base.handler(async ({ context }) => {
return new HibernationAsyncIteratorClass<{ message: string }>((id) => {
// Save the ID. You'll need it to send events later.
context.ws.serializeAttachment({ id })
})
})
```
2. Send events to clients with `encodeHibernationRPCEvent`
```ts
import { encodeHibernationRPCEvent } from '@orpc/hibernation'
import * as z from 'zod'
const base = os.$context<{ getWebSockets: () => WebSocket[] }>()
export const sendMessage = base
.input(z.object({ message: z.string() }))
.handler(async ({ input, context }) => {
const websockets = context.getWebSockets()
for (const ws of websockets) {
const { id } = ws.deserializeAttachment()
// yield an event to all clients
ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, {
// override the default RPC serializer if needed
serializer: new RPCSerializer(),
}))
// return an event and stop the iterator
ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, { event: 'close' }))
// throw an error and stop the iterator
ws.send(await encodeHibernationRPCEvent(id, new ORPCError('INTERNAL_SERVER_ERROR'), { event: 'error' }))
}
})
```
This example shows how to build a chat room with [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) and [WebSocket Hibernation](https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/). Everyone connected to the same Durable Object can exchange messages. You can try a working version in the Cloudflare Playground, see [Playgrounds](/docs/playgrounds).
```ts title="Durable Object"
import { RPCHandler } from '@orpc/server/websocket'
import {
encodeHibernationRPCEvent,
HibernationAsyncIteratorClass,
HibernationHandlerPlugin,
} from '@orpc/hibernation'
import { onError, os } from '@orpc/server'
import { DurableObject } from 'cloudflare:workers'
import * as z from 'zod'
const base = os.$context<{
handler: RPCHandler
ws: WebSocket
getWebsockets: () => WebSocket[]
}>()
export const router = {
send: base.input(z.object({ message: z.string() })).handler(async ({ input, context }) => {
const websockets = context.getWebsockets()
for (const ws of websockets) {
const data = ws.deserializeAttachment()
if (typeof data !== 'object' || data === null) {
continue
}
const { id } = data
ws.send(await encodeHibernationRPCEvent(id, input.message))
}
}),
onMessage: base.handler(async ({ context }) => {
return new HibernationAsyncIteratorClass((id) => {
context.ws.serializeAttachment({ id })
})
}),
}
const handler = new RPCHandler(router, {
interceptors: [
onError((error) => {
console.error(error)
}),
],
plugins: [
new HibernationHandlerPlugin(),
],
})
export class ChatRoom extends DurableObject {
async fetch(): Promise {
const { '0': client, '1': server } = new WebSocketPair()
this.ctx.acceptWebSocket(server)
return new Response(null, {
status: 101,
webSocket: client,
})
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise {
await handler.message(ws, message, {
context: {
handler,
ws,
getWebsockets: () => this.ctx.getWebSockets(),
},
})
}
async webSocketClose(ws: WebSocket): Promise {
await handler.close(ws)
}
}
```
```ts Client
import { RPCLink } from '@orpc/client/websocket'
import { createORPCClient } from '@orpc/client'
import type { router } from '../../worker/dos/chat-room'
import type { RouterClient } from '@orpc/server'
const websocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/chat-room`)
websocket.addEventListener('error', (event) => {
console.error(event)
})
const link = new RPCLink({
connect: () => websocket,
})
export const chatRoomClient: RouterClient = createORPCClient(link)
```
```tsx Component
import { useEffect, useState } from 'react'
import { chatRoomClient } from '../lib/chat-room'
export function ChatRoom() {
const [messages, setMessages] = useState([])
useEffect(() => {
const controller = new AbortController()
void (async () => {
for await (const message of await chatRoomClient.onMessage(undefined, { signal: controller.signal })) {
setMessages(messages => [...messages, message])
}
})()
return () => {
controller.abort()
}
}, [])
const sendMessage = async (e: React.FormEvent) => {
e.preventDefault()
const form = new FormData(e.target as HTMLFormElement)
const message = form.get('message') as string
await chatRoomClient.send({ message })
}
return (
Chat Room
Open multiple tabs to chat together
{messages.map((message, index) => (
{message}
))}
)
}
```
---
# MSW Integration
Source: https://orpc.dev/docs/integrations/msw
:::warning
This guide assumes you are already familiar with [MSW](https://mswjs.io/). If you need a refresher, review the official MSW documentation before continuing.
:::
## Installation
```package-install
npm install @orpc/experimental-msw@beta
```
## Setup
Create MSW utils from a [router contract](/docs/contract/router) or an implemented [router](/docs/router) (convert [lazy routers](/docs/router#lazy-router) with [`unlazyRouter`](/docs/contract/router#router-to-contract) first). The `handler` option creates the fetch handler that serves each mock. Configure it like your production handler, so serialization, validation, and error envelopes behave exactly like your real server.
```ts
import { createHTTPUtils } from '@orpc/experimental-msw'
import { RPCHandler } from '@orpc/server/fetch'
export const mock = createHTTPUtils(contract, {
prefix: '/rpc',
handler: router => new RPCHandler(router),
})
```
Set `prefix` to the prefix your link sends requests to. Any origin matches by default; narrow it with the `origin` option, which supports MSW wildcards.
:::tip
Any protocol works: pair the [RPCLink](/docs/rpc/link) with a [RPCHandler](/docs/rpc/handler), or the [OpenAPILink](/docs/openapi/link) with an [OpenAPIHandler](/docs/openapi/handler).
:::
## Mocking Procedures
The `.handler` method creates an MSW request handler that resolves a procedure. The input your mock receives and the output it returns are validated and serialized by the created fetch handler, exactly like on a real server.
```ts
import { setupServer } from 'msw/node'
const server = setupServer(
mock.planet.list.handler(({ input }) => [
{ id: 1, name: 'Earth' },
]),
)
server.listen()
```
To access request details, such as the raw `request`, expose them through the [`context` option](#advanced-configuration).
:::info
[AsyncIteratorObject](/docs/async-iterator-object) outputs work too: return an async generator and the client receives a streamed response.
:::
## Mocking Errors
The `.error` method creates an MSW request handler that rejects a procedure with one of its [defined errors](/docs/contract/procedure#typesafe-errors), serialized exactly like a server-thrown error. For dynamic or arbitrary errors, use `.handler` and throw the `errors` constructors or any [`ORPCError`](/docs/error-handling#orpcerror-class):
```ts
import { ORPCError } from '@orpc/client'
const handlers = [
mock.planet.find.error('NOT_FOUND', { data: { id: 123 } }),
mock.planet.update.handler(({ input, errors }) => {
throw errors.CONFLICT({ data: { id: input.id } })
}),
mock.planet.delete.handler(() => {
throw new ORPCError('SERVICE_UNAVAILABLE')
}),
]
```
## Mocking Loading States
The `.loading` method creates an MSW request handler that never resolves, useful for testing loading states, for example in [Storybook](https://storybook.js.org/docs/writing-stories/mocking-data-and-modules/mocking-network-requests) stories:
```ts
export const Loading: Story = {
parameters: {
msw: {
handlers: [mock.planet.list.loading()],
},
},
}
```
## Passthrough
The `.passthrough` method creates an MSW request handler that performs matching requests against the real server as-is, useful to exempt specific procedures from mocking, for example while [onUnhandledRequest](https://mswjs.io/docs/api/setup-server/listen#onunhandledrequest) treats everything else as an error:
```ts
const handlers = [
mock.planet.list.handler(() => []),
mock.planet.find.passthrough(), // hits the real server
]
```
## Advanced Configuration
All handler behavior is configured through the `handler` option, so mocks can mirror your production setup exactly, such as plugins, a custom serializer, or `allowMethods` if your client sends [GET requests](/docs/rpc/handler#supported-http-methods) over the RPC protocol:
```ts
import { RPCHandler } from '@orpc/server/fetch'
import { ResponseHeadersHandlerPlugin } from '@orpc/server/plugins'
const mock = createHTTPUtils(contract, {
prefix: '/rpc',
handler: router => new RPCHandler(router, {
plugins: [new ResponseHeadersHandlerPlugin()],
}),
})
```
The `context` option controls the [context](/docs/context) passed to the created handler on each request, and mock handlers receive it as `context`, enabling context-driven behaviors such as the [Response Headers Plugin](/docs/plugins/response-headers).
```ts
import { ResponseHeadersHandlerPlugin, type ResponseHeadersHandlerPluginContext } from '@orpc/server/plugins'
interface MockServerContext extends ResponseHeadersHandlerPluginContext {
reqHeaders: Headers
}
const mock = createHTTPUtils(contract, {
context: (info): MockServerContext => ({ reqHeaders: info.request.headers }),
handler: router => new RPCHandler(router, {
plugins: [new ResponseHeadersHandlerPlugin()],
}),
})
const handlers = [
mock.planet.list.handler(({ context }) => {
const locale = context.reqHeaders.get('accept-language') ?? 'en'
context.resHeaders?.set('content-language', locale)
return []
}),
]
```
You can also disable input or output validation of the mocked data:
```ts
const mock = createHTTPUtils(contract, {
handler: router => new RPCHandler(router),
disableInputValidation: true,
disableOutputValidation: true,
})
```
Each mock serves a router containing only the procedure being mocked. Requests the created handler does not match simply fall through to other MSW handlers.
## Limitations
Requests sent through the [Batch Requests Plugin](/docs/plugins/batch) cannot be mocked. Each mock serves a router containing only its own procedure, so even a `handler` configured with the batch plugin cannot resolve the other procedures bundled into the same HTTP request. Disable batching when mocking with MSW.
---
# Implement oRPC contract with NestJS
Source: https://orpc.dev/docs/integrations/nest
## Installation
```package-install
npm install @orpc/nest@beta
```
## Requirements
oRPC is an ESM-only library, but NestJS versions below v12 do not natively support ESM. You might need to configure your project for ESM and use a Node.js version that supports `require()` for ESM modules (Node.js 22+ is recommended). The following configuration is recommended:
```json tsconfig.json
{
"compilerOptions": {
"module": "NodeNext", // <- recommended
"strict": true // <- recommended
// ... other options
}
}
```
## Define Your Contract
Before implementation, define your [contract](/docs/contract/procedure) as usual, including [routing](/docs/openapi/routing). There is no special requirement, except that each contract must define an `openapi.path` meta.
```ts
import { oc } from '@orpc/contract'
import { openapi, populateRouterContractOpenAPIPaths } from '@orpc/openapi'
const example = oc.meta(openapi({
path: '/example' // [!code highlight]
}))
// or using the `populateRouterContractOpenAPIPaths` helper to
// automatically populate OpenAPI paths for all contracts
const contract = populateRouterContractOpenAPIPaths({
example
})
```
## Implement Your Contract
To implement your contract in NestJS, use the `@Implement` decorator and the `implement` function. The `@Implement` very similar to NestJS built-in HTTP method decorators (e.g., `@Get`, `@Post`) and can be used to implement either a single procedure contract or an [router contract](/docs/contract/router) or combine with other NestJS decorators.
```ts
import { Implement } from '@orpc/nest'
import { implement, ORPCError } from '@orpc/server'
@Controller()
export class PlanetController {
/**
* Implement a procedure contract
*/
@Implement(contract.planet.list)
list() {
return implement(contract.planet.list).handler(({ input }) => {
// Implement logic here
})
}
/**
* Implement a router contract
*/
@Implement(contract.planet)
planet() {
return {
list: implement(contract.planet.list).handler(({ input }) => {
// Implement logic here
}),
find: implement(contract.planet.find).handler(({ input }) => {
// Implement logic here
}),
create: implement(contract.planet.create).handler(({ input }) => {
// Implement logic here
}),
}
}
// other handlers...
}
```
:::info
When you use the `@Implement` decorator with a router contract, under the hood it creates a corresponding NestJS method for each procedure contract. All decorators applied to the original method are reflected on these methods.
:::
## Error Handling
By default, errors thrown in implemented procedures are caught and handled by oRPC, which then rethrows a generic `HttpException` to NestJS. If you want NestJS to catch the original error instead of `HttpException`, use the [Rethrow Plugin](/docs/plugins/rethrow) to bypass oRPC error handling and let NestJS handle the error directly.
:::tip
Learn how to customize input and output validation errors in [Validation Errors](/docs/recipes/validation-customization#custom-validation-errors) and the [ORPCModule](#configuration) section.
:::
## Body Parser
oRPC uses bodies parsed by NestJS when available, and falls back to its own parser otherwise. In some cases, you may want to disable the NestJS body parser so oRPC can handle parsing directly:
- NestJS `urlencoded` parsing does not support [Bracket Notation](/docs/openapi/bracket-notation).
- File uploads with common content types like `application/json` may not be parsed as `File` instances.
```ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bodyParser: false, // [!code highlight]
})
await app.listen(process.env.PORT ?? 3000)
}
```
## Configuration
Configure `@orpc/nest` by importing `ORPCModule` into your NestJS module. It supports the same
options as the [OpenAPI Handler](/docs/openapi/handler), except for options that are unrelated to NestJS and options that are specific to NestJS.
```ts Static
import { onError } from '@orpc/server'
import { ORPCModule } from '@orpc/nest'
@Module({
imports: [
ORPCModule.forRoot({
interceptors: [
onError((error) => {
console.error(error)
}),
],
}),
],
})
export class AppModule {}
```
```ts title="Dynamic with Dependency Injection"
import { onError } from '@orpc/server'
import { ORPCModule } from '@orpc/nest'
@Module({
imports: [
ORPCModule.forRootAsync({
inject: [YourLoggerService],
useFactory: (logger: YourLoggerService) => ({
interceptors: [
onError((error) => {
logger.error(error)
}),
],
}),
}),
],
})
export class AppModule {}
```
### Initial Context
To define [initial context](/docs/context#initial-context) for use in oRPC scopes, extend the `DefaultInitialContext` interface and provide `context` through `ORPCModule`.
```ts
import { ExecutionContext } from '@nestjs/common'
declare module '@orpc/server' {
/**
* Extend the context interface to enable typesafe access across oRPC scopes
*/
interface DefaultInitialContext {
request: Request
}
}
@Module({
imports: [
ORPCModule.forRoot({
/**
* Can be a static value or an async function that
* receives the ExecutionContext on each request
*/
context: (ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest() as Request
return { request }
},
}),
],
})
export class AppModule {}
```
### Plugins
Most handler plugins also work in NestJS, for example [Request Headers](/docs/plugins/request-headers), [Response Headers](/docs/plugins/response-headers), [Rethrow](/docs/plugins/rethrow), and [Smart Coercion](/docs/plugins/smart-coercion).
```ts
@Module({
imports: [
ORPCModule.forRoot({
plugins: [
new RethrowHandlerPlugin({
// Bypass oRPC error handling and let NestJS handle the error instead
filter: error => !(error instanceof ORPCError)
}),
],
}),
],
})
export class AppModule {}
```
:::warning
Procedures run only when a matching NestJS controller method is called. If no route matches (404), neither the procedure nor its plugins run. As a result, plugins like [OpenAPI Reference](/docs/plugins/openapi-reference) may not work as expected, since NestJS can respond with 404 before the procedure runs.
:::
### Event Stream Options
Configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `toNestResponse.eventStream` options.
```ts
@Module({
imports: [
ORPCModule.forRoot({
toNestResponse: {
eventStream: {
initialComment: {
/**
* If true, an initial comment is sent immediately upon stream start to flush headers.
* This allows the receiving side to establish the connection without waiting for the first event.
*
* @default true
*/
enabled: true,
/**
* The content of the initial comment sent upon stream start. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
keepAlive: {
/**
* If true, a ping comment is sent periodically to keep the connection alive.
*
* @default true
*/
enabled: true,
/**
* Interval (in milliseconds) between ping comments sent after the last event.
*
* @default 15000
*/
interval: 15000,
/**
* The content of the ping comment. Must not include newline characters.
*
* @default ''
*/
comment: '',
},
/**
* If true, a `close` event is sent even when the iterator completes with `undefined`.
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
*
* @default true
*/
emptyCloseEventEnabled: true,
},
},
}),
],
})
export class AppModule {}
```
### `toNestStandardLazyRequest` option
By default, `@orpc/nest` supports the Express and Fastify adapters. If you use another adapter, you may need to customize how a NestJS request is converted into a standard request (including additional params). For details, see [Standard Server](https://github.com/middleapi/standard-server#request-and-response-types).
```ts
import { NestStandardLazyRequest } from '@orpc/nest'
import { toStandardLazyRequest } from '@standard-server/fetch'
@Module({
imports: [
ORPCModule.forRoot({
toNestStandardLazyRequest: (req, res) => {
// example Hono platform support
const standardRequest: NestStandardLazyRequest = toStandardLazyRequest(req.raw)
standardRequest.params = req.params
return standardRequest
},
}),
],
})
export class AppModule {}
```
## Typesafe Client
After implementing your contract in NestJS, you can use the same contract to create a typesafe client. See [OpenAPI Link](/docs/openapi/link) for more details.
---
# Next.js Integration
Source: https://orpc.dev/docs/integrations/next
## Installation
```package-install
npm install @orpc/next@beta
```
## Server Functions
Use `createServerFunction` to turn a [procedure](/docs/procedure) into a [server function](https://nextjs.org/docs/app/api-reference/directives/use-server). It accepts the same options as [server-side clients](/docs/client/server-side#router-clients), and the returned function accepts the same input as the original procedure.
```ts twoslash
'use server'
import { os } from '@orpc/server'
import { createServerFunction } from '@orpc/next'
const procedure = os.handler(async () => 'Hello from oRPC + Next.js!')
export const serverFunction = createServerFunction(procedure, {
context: async () => { // <- provide initial context if needed
return { user: { id: '123', name: 'Alice' } }
},
interceptors: [] // <- add interceptors if needed
})
```
You can call the returned `serverFunction` from a client component.
```tsx
'use client'
import { serverFunction } from './path/to/server/function'
export default function Page() {
const handleClick = async () => {
const [error, message] = await serverFunction()
if (!error) {
console.log({ message })
}
}
return (
)
}
```
Special Next.js errors such as [redirect](https://nextjs.org/docs/app/api-reference/functions/redirect) and [notFound](https://nextjs.org/docs/app/api-reference/functions/not-found) are rethrown so Next.js handles them normally. All other errors are serialized to `ORPCErrorJSON` and returned as the first element of the tuple.
### Typesafe Errors
[Typesafe errors](/docs/error-handling#typesafe-errors) are supported as well. Because errors are serialized before they reach the client, use the `defined` field to distinguish errors.
```tsx client
'use client'
import { serverFunction } from './path/to/server/function'
export default function Page() {
const handleClick = async () => {
const [error, message] = await serverFunction()
if (error) {
if (error.defined) {
// handle typesafe error
}
else {
// handle unknown error
}
}
else {
// handle success case
}
}
return (
)
}
```
```ts server
'use server'
const procedure = os
.errors({
NOT_FOUND: {
message: 'The resource was not found',
},
})
.handler(async ({ errors }) => {
throw errors.NOT_FOUND()
})
export const serverFunction = createServerFunction(procedure)
```
### `createServerFunctionable`
If you reuse the same options across multiple server functions, `createServerFunctionable` creates a preconfigured helper. The helper takes a procedure and returns a value that works as both a server function and the original [procedure](/docs/procedure) on the server.
```ts
import { createServerFunctionable } from '@orpc/next'
const functionable = createServerFunctionable({
context: async () => { // <- provide initial context if needed
return { user: { id: '123', name: 'Alice' } }
},
})
// Works as both a server function and a procedure.
export const functionableProcedure = functionable(
os.handler(async () => 'Hello from oRPC + Next.js!')
)
```
### `.actionable` Extension
Import `@orpc/next/extensions/actionable` from a module that always runs during initialization, such as the file where you define your base builder. This adds an `.actionable` method to decorated procedures. Like `createServerFunctionable`, it returns a value that works as both a server function and a [procedure](/docs/procedure).
```ts usage
export const functionableProcedure = base
.handler(async () => 'Hello from oRPC + Next.js!')
.actionable({
context: async () => { // <- provide initial context if needed
return { user: { id: '123', name: 'Alice' } }
},
})
```
```ts setup
import '@orpc/next/extensions/actionable'
import { os } from '@orpc/server'
export const base = os
```
### Hooks
This integration also includes React hooks for server functions. `useServerFunction` executes a server function and tracks its status. `useOptimisticServerFunction` does the same, with optimistic updates. Unlike direct server function calls, hook errors are deserialized into native `ORPCError` instances instead of plain JSON (`ORPCErrorJSON`) for a more natural developer experience.
```tsx useServerFunction
'use client'
import { isDefinedError } from '@orpc/client'
import {
getIssueMessage,
onErrorDeferred,
parseFormData,
} from '@orpc/next'
import { useServerFunction } from '@orpc/next/hooks'
export function MyComponent() {
const { execute, data, error, status } = useServerFunction(serverFunction, {
interceptors: [
onErrorDeferred((error) => {
if (isDefinedError(error)) {
console.error(error.data)
// ^ Typed error data
}
}),
],
})
return (
)
}
```
```tsx useOptimisticServerFunction
'use client'
import { useOptimisticServerAction } from '@orpc/next/hooks'
import {
getIssueMessage,
onSuccessDeferred,
parseFormData,
} from '@orpc/next'
export function MyComponent() {
const [todos, setTodos] = useState([])
const { execute, optimisticState } = useOptimisticServerAction(someAction, {
optimisticPassthrough: todos,
optimisticReducer: (currentState, newTodo) => [...currentState, newTodo],
interceptors: [
onSuccessDeferred(({ data }) => {
setTodos(prevTodos => [...prevTodos, data])
}),
],
})
return (
{optimisticState.map(todo => (
{todo.todo}
))}
)
}
```
:::info
Besides hooks, this integration also re-exports [form-data helpers](/docs/helpers/form-data) for working with `FormData`, as well as deferred interceptors for updating UI states: `onStartDeferred`, `onSuccessDeferred`, `onErrorDeferred`, and `onFinishDeferred`.
:::
:::info
You can use [`safe` and `isDefinedError`](/docs/client/error-handling#using-safe-and-isdefinederror) together for typesafe error handling in interceptors.
:::
## Server Form Functions
Use `createServerFormFunction` to turn a procedure into a form action for `