--- url: /docs/plugins/batch-request-response.md description: A plugin for oRPC to batch requests and responses. --- # Batch Request/Response Plugin The **Batch Request/Response Plugin** allows you to combine multiple requests and responses into a single batch, reducing the overhead of sending each one separately. :::info The **Batch Plugin** streams responses asynchronously so that no individual request blocks another, ensuring all responses are handled independently for faster, more efficient batching. ::: ## Setup This plugin requires configuration on both the server and client sides. ### Server ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { BatchHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [new BatchHandlerPlugin()], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler) or custom implementations. Note that this plugin uses its own protocol for batching requests and responses, which is different from the handler’s native protocol. ::: ### Client To use the `BatchLinkPlugin`, define at least one group. Requests within the same group will be considered for batching together, and each group requires a `context` as described in [client context](/docs/client/rpc-link#using-client-context). ```ts twoslash import { RPCLink } from '@orpc/client/fetch' // ---cut--- import { BatchLinkPlugin } from '@orpc/client/plugins' const link = new RPCLink({ url: 'https://api.example.com/rpc', plugins: [ new BatchLinkPlugin({ groups: [ { condition: options => true, context: {} // This context will represent the batch request and persist throughout the request lifecycle } ] }), ], }) ``` ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/client/rpc-link), [OpenAPILink](/docs/openapi/client/openapi-link), or custom implementations. ::: ## Limitations The plugin does not support [AsyncIteratorObject](/docs/rpc-handler#supported-data-types) or [File/Blob](/docs/rpc-handler#supported-data-types) in responses (requests will auto fall back to the default behavior). To exclude unsupported procedures, use the `exclude` option: ```ts twoslash import { RPCLink } from '@orpc/client/fetch' import { BatchLinkPlugin } from '@orpc/client/plugins' // ---cut--- const link = new RPCLink({ url: 'https://api.example.com/rpc', plugins: [ new BatchLinkPlugin({ groups: [ { condition: options => true, context: {} } ], exclude: ({ path }) => { return ['planets/getImage', 'planets/subscribe'].includes(path.join('/')) } }), ], }) ``` ## Request Headers By default, oRPC uses the headers appear in all requests in the batch. To customize headers, use the `headers` option: ```ts twoslash import { RPCLink } from '@orpc/client/fetch' import { BatchLinkPlugin } from '@orpc/client/plugins' // ---cut--- const link = new RPCLink({ url: 'https://api.example.com/rpc', plugins: [ new BatchLinkPlugin({ groups: [ { condition: options => true, context: {} } ], headers: () => ({ authorization: 'Bearer 1234567890', }) }), ], }) ``` ## Response Headers By default, the response headers are empty. To customize headers, use the `headers` option: ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { BatchHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [new BatchHandlerPlugin({ headers: responses => ({ 'some-header': 'some-value', }) })], }) ``` ## Groups Requests within the same group will be considered for batching together, and each group requires a `context` as described in [client context](/docs/client/rpc-link#using-client-context). In the example below, I used a group and `context` to batch requests based on the `cache` control: ```ts twoslash import { RPCLink } from '@orpc/client/fetch' import { BatchLinkPlugin } from '@orpc/client/plugins' interface ClientContext { cache?: RequestCache } const link = new RPCLink({ url: 'http://localhost:3000/rpc', method: ({ context }) => { if (context?.cache) { return 'GET' } return 'POST' }, plugins: [ new BatchLinkPlugin({ groups: [ { condition: ({ context }) => context?.cache === 'force-cache', context: { // This context will be passed to the fetch method cache: 'force-cache', }, }, { // Fallback for all other requests - need put it at the end of list condition: () => true, context: {}, }, ], }), ], fetch: (request, init, { context }) => globalThis.fetch(request, { ...init, cache: context?.cache, }), }) ``` Now, calls with `cache=force-cache` will be sent with `cache=force-cache`, whether they're batched or executed individually. --- --- url: /docs/plugins/body-limit.md description: A plugin for oRPC to limit the request body size. --- # Body Limit Plugin The **Body Limit Plugin** restricts the size of the request body. ## Import Depending on your adapter, import the corresponding plugin: ```ts import { BodyLimitPlugin } from '@orpc/server/fetch' import { BodyLimitPlugin } from '@orpc/server/node' ``` ## Setup Configure the plugin with your desired maximum body size: ```ts const handler = new RPCHandler(router, { plugins: [ new BodyLimitPlugin({ maxBodySize: 1024 * 1024, // 1MB }), ], }) ``` --- --- url: /docs/openapi/bracket-notation.md description: >- Represent structured data in limited formats such as URL queries and form data. --- # Bracket Notation Bracket Notation encodes structured data in formats with limited syntax, like URL queries and form data. It is used by [OpenAPIHandler](/docs/openapi/openapi-handler) and [OpenAPILink](/docs/openapi/client/openapi-link). ## Usage * Append `[]` **at the end** to denote an array. * Append `[number]` to specify an array index (missing indexes create sparse arrays). * Append `[key]` to denote an object property. ## Limitations * **Empty Arrays:** Cannot be represented; arrays must have at least one element. * **Empty Objects:** Cannot be represented. Objects with empty or numeric keys may be interpreted as arrays, so ensure objects include at least one non-empty, non-numeric key. ## Examples ### URL Query ```bash curl http://example.com/api/example?name[first]=John&name[last]=Doe ``` This query is parsed as: ```json { "name": { "first": "John", "last": "Doe" } } ``` ### Form Data ```bash curl -X POST http://example.com/api/example \ -F 'name[first]=John' \ -F 'name[last]=Doe' ``` This form data is parsed as: ```json { "name": { "first": "John", "last": "Doe" } } ``` ### Complex Example ```bash curl -X POST http://example.com/api/example \ -F 'data[names][0][first]=John1' \ -F 'data[names][0][last]=Doe1' \ -F 'data[names][1][first]=John2' \ -F 'data[names][1][last]=Doe2' \ -F 'data[ages][0]=18' \ -F 'data[ages][2]=25' \ -F 'data[files][]=@/path/to/file1' \ -F 'data[files][]=@/path/to/file2' ``` This form data is parsed as: ```json { "data": { "names": [ { "first": "John1", "last": "Doe1" }, { "first": "John2", "last": "Doe2" } ], "ages": ["18", "", "25"], "files": ["", ""] } } ``` --- --- url: /docs/integrations/bun.md description: Integrate oRPC with Bun's built-in HTTP server --- # Bun Integration [Bun](https://bun.sh/) comes with a built-in, high-performance HTTP server that follows the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ```ts import { RPCHandler } from '@orpc/server/fetch' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin() ] }) Bun.serve({ async fetch(request: Request) { 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 }) } }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/plugins/client-retry.md description: A plugin for oRPC that enables retrying client calls when errors occur. --- # Client Retry Plugin The `Client Retry Plugin` enables retrying client calls when errors occur. ## Setup Before you begin, please review the [Client Context](/docs/client/rpc-link#using-client-context) documentation. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { createORPCClient } from '@orpc/client' // ---cut--- import { RPCLink } from '@orpc/client/fetch' import { ClientRetryPlugin, ClientRetryPluginContext } from '@orpc/client/plugins' interface ORPCClientContext extends ClientRetryPluginContext {} const link = new RPCLink({ url: 'http://localhost:3000/rpc', plugins: [ new ClientRetryPlugin({ default: { // Optional override for default options retry: ({ path }) => { if (path.join('.') === 'planet.list') { return 2 } return 0 } }, }), ], }) const client: RouterClient = createORPCClient(link) ``` ## Usage ```ts twoslash import { router } from './shared/planet' import { ClientRetryPluginContext } from '@orpc/client/plugins' import { RouterClient } from '@orpc/server' declare const client: RouterClient // ---cut--- const planets = await client.planet.list({ limit: 10 }, { context: { retry: 3, // Maximum retry attempts retryDelay: 2000, // Delay between retries in ms shouldRetry: options => true, // Determines whether to retry based on the error onRetry: (options) => { // Hook executed on each retry return (isSuccess) => { // Execute after the retry is complete } }, } }) ``` ::: info By default, retries are disabled unless a `retry` count is explicitly set. * **retry:** Maximum retry attempts before throwing an error (default: `0`). * **retryDelay:** Delay between retries (default: `(o) => o.lastEventRetry ?? 2000`). * **shouldRetry:** Function that determines whether to retry (default: `true`). ::: ## Event Iterator (SSE) To replicate the behavior of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) for [Event Iterator](/docs/event-iterator), use the following configuration: ```ts const streaming = await client.streaming('the input', { context: { retry: Number.POSITIVE_INFINITY, } }) for await (const message of streaming) { console.log(message) } ``` --- --- url: /docs/client/client-side.md description: Call your oRPC procedures remotely as if they were local functions. --- # Client-Side Clients Call your [procedures](/docs/procedure) remotely as if they were local functions. ## Installation ::: code-group ```sh [npm] npm install @orpc/client@latest ``` ```sh [yarn] yarn add @orpc/client@latest ``` ```sh [pnpm] pnpm add @orpc/client@latest ``` ```sh [bun] bun add @orpc/client@latest ``` ```sh [deno] deno install npm:@orpc/client@latest ``` ::: ## Creating a Client This guide uses [RPCLink](/docs/client/rpc-link), so make sure your server is set up with [RPCHandler](/docs/rpc-handler) or any API that follows the [RPC Protocol](/docs/advanced/rpc-protocol). ```ts import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' import { RouterClient } from '@orpc/server' import { ContractRouterClient } from '@orpc/contract' const link = new RPCLink({ url: 'http://localhost:3000/rpc', headers: () => ({ authorization: 'Bearer token', }), // fetch: <-- provide fetch polyfill fetch if needed }) // Create a client for your router const client: RouterClient = createORPCClient(link) // Or, create a client using a contract const client: ContractRouterClient = createORPCClient(link) ``` :::tip You can export `RouterClient` and `ContractRouterClient` from server instead. ::: ## Calling Procedures Once your client is set up, you can call your [procedures](/docs/procedure) as if they were local functions. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' const client = {} as RouterClient // ---cut--- const planet = await client.planet.find({ id: 1 }) client.planet.create // ^| ``` ## Merge Clients In oRPC, a client is a simple object-like structure. To merge multiple clients, you simply assign each client to a property in 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, } ``` --- --- url: /docs/integrations/cloudflare-workers.md description: Integrate oRPC with Cloudflare Workers --- # Cloudflare Workers Integration [Cloudflare Workers](https://workers.cloudflare.com/) provide a serverless execution environment for building fast, globally distributed applications that follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ```ts import { RPCHandler } from '@orpc/server/fetch' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin() ] }) export default { async fetch(request: Request, env: any, ctx: ExecutionContext): 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 }) } } ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/comparison.md description: How is oRPC different from other RPC or REST solutions? --- # Comparison This comparison table helps you understand how oRPC differs from other popular TypeScript RPC and REST solutions. * ✅ First-class, built-in support * 🟡 Lacks features, or requires third-party integrations * 🛑 Not supported or not documented | Feature | oRPC docs | oRPC | tRPC | ts-rest | | -------------------------------------------- | -------------------------------------------------------------------------------------------- | ---- | ---- | ------- | | End-to-end Typesafe Input/Output | | ✅ | ✅ | ✅ | | End-to-end Typesafe Errors | [1](/docs/client/error-handling), [2](/docs/error-handling#type%E2%80%90safe-error-handling) | ✅ | 🟡 | ✅ | | End-to-end Typesafe File/Blob | [1](/docs/file-upload-download) | ✅ | 🟡 | 🛑 | | End-to-end Typesafe Streaming | [1](/docs/event-iterator) | ✅ | ✅ | 🛑 | | Tanstack Query Integration (React) | [1](/docs/tanstack-query/react) | ✅ | ✅ | 🟡 | | Tanstack Query Integration (Vue) | [1](/docs/tanstack-query/vue) | ✅ | 🛑 | 🟡 | | Tanstack Query Integration (Solid) | [1](/docs/tanstack-query/solid) | ✅ | 🛑 | 🟡 | | Tanstack Query Integration (Svelte) | [1](/docs/tanstack-query/svelte) | ✅ | 🛑 | 🛑 | | Vue Pinia Colada Integration | [1](/docs/pinia-colada) | ✅ | 🛑 | 🛑 | | With Contract-First Approach | [1](/docs/contract-first/define-contract) | ✅ | 🛑 | ✅ | | Without Contract-First Approach | | ✅ | ✅ | 🛑 | | OpenAPI Support | [1](/docs/openapi/openapi-handler) | ✅ | 🟡 | 🟡 | | OpenAPI Support for multiple schema | [1](/docs/openapi/openapi-handler) | ✅ | 🛑 | 🛑 | | OpenAPI Bracket Notation Support | [1](/docs/openapi/bracket-notation) | ✅ | 🛑 | 🛑 | | Server Actions Support | [1](/docs/server-action) | ✅ | ✅ | 🛑 | | Lazy Router | [1](/docs/router#lazy-router) | ✅ | ✅ | 🛑 | | Native Types (Date, URL, Set, Maps, ...) | [1](/docs/rpc-handler#supported-data-types) | ✅ | 🟡 | 🛑 | | Streaming response (SSE) | [1](/docs/event-iterator) | ✅ | ✅ | 🛑 | | Standard Schema (Zod, Valibot, ArkType, ...) | | ✅ | ✅ | 🛑 | | Built-in Plugins (CORS, CSRF, Retry, ...) | | ✅ | 🛑 | 🛑 | | Batch Request/Response | [1](/docs/plugins/batch-request-response) | ✅ | ✅ | 🛑 | | WebSockets | (working) | 🛑 | ✅ | 🛑 | | Nest.js integration | | 🛑 | 🟡 | ✅ | --- --- url: /docs/context.md description: Understanding context in oRPC --- # Context in oRPC oRPC’s context mechanism provides a type-safe dependency injection pattern. It lets you supply required dependencies either explicitly or dynamically through middleware. There are two types: * **Initial Context:** Provided explicitly when invoking a procedure. * **Execution Context:** Generated during procedure execution, typically by middleware. ## Initial Context Initial context is used to define required dependencies (usually environment-specific) that must be passed when calling a procedure. ```ts twoslash import { os } from '@orpc/server' // ---cut--- const base = os.$context<{ headers: Headers, env: { DB_URL: string } }>() const getting = base .handler(async ({ context }) => { console.log(context.env) }) export const router = { getting } ``` When calling that requires initial context, pass it explicitly: ```ts twoslash import { os } from '@orpc/server' const base = os.$context<{ headers: Headers, env: { DB_URL: string } }>() const getting = base .handler(async ({ context }) => { }) export const router = { getting } // ---cut--- import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(router) export default function fetch(request: Request) { handler.handle(request, { context: { // <-- you must pass initial context here headers: request.headers, env: { DB_URL: '***' } } }) } ``` ## Execution context Execution context is computed during the process lifecycle—usually via [middleware](/docs/middleware). It can be used independently or combined with initial context. ```ts twoslash import { os } from '@orpc/server' // ---cut--- import { cookies, headers } from 'next/headers' const base = os.use(async ({ next }) => next({ context: { headers: await headers(), cookies: await cookies(), }, })) const getting = base.handler(async ({ context }) => { context.cookies.set('key', 'value') }) export const router = { getting } ``` When using execution context, you don’t need to pass any context manually: ```ts twoslash import { os } from '@orpc/server' import { cookies, headers } from 'next/headers' const base = os.use(async ({ next }) => next({ context: { headers: await headers(), cookies: await cookies(), }, })) const getting = base.handler(async ({ context }) => { context.cookies.set('key', 'value') }) export const router = { getting } // ---cut--- import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(router) export default function fetch(request: Request) { handler.handle(request) // <-- no need to pass anything more } ``` ## Combining Initial and Execution Context Often you need both static and dynamic dependencies. Use initial context for environment-specific values (e.g., database URLs) and middleware (execution context) for runtime data (e.g., user authentication). ```ts twoslash import { ORPCError, os } from '@orpc/server' // ---cut--- const base = os.$context<{ headers: Headers, env: { DB_URL: string } }>() const requireAuth = base.middleware(async ({ context, next }) => { const user = parseJWT(context.headers.get('authorization')?.split(' ')[1]) if (user) { return next({ context: { user } }) } throw new ORPCError('UNAUTHORIZED') }) const dbProvider = base.middleware(async ({ context, next }) => { const client = new Client(context.env.DB_URL) try { await client.connect() return next({ context: { db: client } }) } finally { await client.disconnect() } }) const getting = base .use(dbProvider) .use(requireAuth) .handler(async ({ context }) => { console.log(context.db) console.log(context.user) }) // ---cut-after--- declare function parseJWT(token: string | undefined): { userId: number } | null declare class Client { constructor(url: string) connect(): Promise disconnect(): Promise } ``` --- --- url: /docs/plugins/cors.md description: CORS Plugin for oRPC --- # CORS Plugin `CORSPlugin` is a plugin for oRPC that allows you to configure CORS for your API. ## Basic ```ts import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin({ origin: (origin, options) => origin, allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'], // ... }), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/best-practices/dedupe-middleware.md description: Enhance oRPC middleware performance by avoiding redundant executions. --- # Dedupe Middleware This guide explains how to optimize your [middleware](/docs/middleware) for fast and efficient repeated execution. ## Problem When a procedure [calls](/docs/client/server-side#using-the-call-utility) another procedure, overlapping middleware might be applied in both. Similarly, when using `.use(auth).router(router)`, some procedures inside `router` might already include the `auth` middleware. :::warning Redundant middleware execution can hurt performance, especially if the middleware is resource-intensive. ::: ## Solution Use the `context` to track middleware execution and prevent duplication. For example: ```ts twoslash import { os } from '@orpc/server' declare function connectDb(): Promise<'a_fake_db'> // ---cut--- const dbProvider = os .$context<{ db?: Awaited> }>() .middleware(async ({ context, next }) => { /** * If db already exists, skip the connection. */ const db = context.db ?? await connectDb() // [!code highlight] return next({ context: { db } }) }) ``` Now `dbProvider` middleware can be safely applied multiple times without duplicating the database connection: ```ts twoslash import { call, os } from '@orpc/server' declare function connectDb(): Promise<'a_fake_db'> const dbProvider = os .$context<{ db?: Awaited> }>() .middleware(async ({ context, next }) => { const db = context.db ?? await connectDb() return next({ context: { db } }) }) // ---cut--- const foo = os.use(dbProvider).handler(({ context }) => 'Hello World') const bar = os.use(dbProvider).handler(({ context }) => { /** * Now when you call foo, the dbProvider middleware no need to connect to the database again. */ const result = call(foo, 'input', { context }) // [!code highlight] return 'Hello World' }) /** * Now even when `dbProvider` is applied multiple times, it still only connects to the database once. */ const router = os .use(dbProvider) // [!code highlight] .use(({ next }) => { // Additional middleware logic return next() }) .router({ foo, bar, }) ``` ## Built-in Dedupe Middleware oRPC can automatically dedupe some middleware under specific conditions. ::: info Deduplication occurs only if the router middlewares is a **subset** of the **leading** procedure middlewares and appears in the **same order**. ::: ```ts const router = os.use(logging).use(dbProvider).router({ ping: os.use(logging).use(dbProvider).use(auth).handler(({ context }) => 'ping'), pong: os.use(logging).use(dbProvider).handler(({ context }) => 'pong'), // ⛔ Deduplication does not occur: diff_subset: os.use(logging).handler(({ context }) => 'ping'), diff_order: os.use(dbProvider).use(logging).handler(({ context }) => 'pong'), diff_leading: os.use(monitor).use(logging).use(dbProvider).handler(({ context }) => 'bar'), }) // --- equivalent to --- const router = { ping: os.use(logging).use(dbProvider).use(auth).handler(({ context }) => 'ping'), pong: os.use(logging).use(dbProvider).handler(({ context }) => 'pong'), // ⛔ Deduplication does not occur: diff_subset: os.use(logging).use(dbProvider).use(logging).handler(({ context }) => 'ping'), diff_order: os.use(logging).use(dbProvider).use(dbProvider).use(logging).handler(({ context }) => 'pong'), diff_leading: os.use(logging).use(dbProvider).use(monitor).use(logging).use(dbProvider).handler(({ context }) => 'bar'), } ``` ### Configuration Disable middleware deduplication by setting `dedupeLeadingMiddlewares` to `false` in `.$config`: ```ts const base = os.$config({ dedupeLeadingMiddlewares: false }) ``` :::warning The deduplication behavior is safe unless you want to apply middleware multiple times. ::: --- --- url: /docs/contract-first/define-contract.md description: Learn how to define a contract for contract-first development in oRPC --- # Define Contract **Contract-first development** is a design pattern where you define the API contract before writing any implementation code. This methodology promotes a well-structured codebase that adheres to best practices and facilitates easier maintenance and evolution over time. In oRPC, a **contract** specifies the rules and expectations for a procedure. It details the input, output, errors,... types and can include constraints or validations to ensure that both client and server share a clear, consistent interface. ## Installation ::: code-group ```sh [npm] npm install @orpc/contract@latest ``` ```sh [yarn] yarn add @orpc/contract@latest ``` ```sh [pnpm] pnpm add @orpc/contract@latest ``` ```sh [bun] bun add @orpc/contract@latest ``` ```sh [deno] deno install npm:@orpc/contract@latest ``` ::: ## Procedure Contract A procedure contract in oRPC is similar to a standard [procedure](/docs/procedure) definition, but with extraneous APIs removed to better support contract-first development. ```ts twoslash import { z } from 'zod' // ---cut--- import { oc } from '@orpc/contract' export const exampleContract = oc .input( z.object({ name: z.string(), age: z.number().int().min(0), }), ) .output( z.object({ id: z.number().int().min(0), name: z.string(), age: z.number().int().min(0), }), ) ``` ## Contract Router Similar to the standard [router](/docs/router) in oRPC, the contract router organizes your defined contracts into a structured hierarchy. The contract router is streamlined by removing APIs that are not essential for contract-first development. ```ts export const routerContract = { example: exampleContract, nested: { example: exampleContract, }, } ``` ## Full Example Below is a complete example demonstrating how to define a contract for a simple "Planet" service. This example extracted from our [Getting Started](/docs/getting-started) guide. ```ts twoslash import { z } from 'zod' import { oc } from '@orpc/contract' // ---cut--- export const PlanetSchema = z.object({ id: z.number().int().min(1), name: z.string(), description: z.string().optional(), }) export const listPlanetContract = oc .input( z.object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.number().int().min(0).default(0), }), ) .output(z.array(PlanetSchema)) export const findPlanetContract = oc .input(PlanetSchema.pick({ id: true })) .output(PlanetSchema) export const createPlanetContract = oc .input(PlanetSchema.omit({ id: true })) .output(PlanetSchema) export const contract = { planet: { list: listPlanetContract, find: findPlanetContract, create: createPlanetContract, }, } ``` ## Utilities ### Infer Contract Router Input ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferContractRouterInputs } from '@orpc/contract' export type Inputs = InferContractRouterInputs type FindPlanetInput = Inputs['planet']['find'] ``` This snippet automatically extracts the expected input types for each procedure in the router. ### Infer Contract Router Output ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferContractRouterOutputs } from '@orpc/contract' export type Outputs = InferContractRouterOutputs type FindPlanetOutput = Outputs['planet']['find'] ``` Similarly, this utility infers the output types, ensuring that your application correctly handles the results from each procedure. --- --- url: /docs/integrations/deno.md description: Integrate oRPC with Deno's built-in HTTP server --- # Deno Integration [Deno](https://deno.land/) provide a serverless execution environment for building fast, globally distributed applications that follow the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ```ts import { RPCHandler } from '@orpc/server/fetch' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin() ] }) Deno.serve(async (request) => { 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 }) }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/client/dynamic-link.md description: Dynamically switch between multiple oRPC's links. --- # DynamicLink `DynamicLink` lets you dynamically choose between different oRPC's links based on your client context. This capability enables flexible routing of RPC requests. ## Example This example shows how the client dynamically selects between two [RPCLink](/docs/client/rpc-link) instances based on the client context: one dedicated to cached requests and another for non-cached requests. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { RPCLink } from '@orpc/client/fetch' // ---cut--- import { createORPCClient, DynamicLink } from '@orpc/client' interface ClientContext { cache?: boolean } const cacheLink = new RPCLink({ url: 'https://cache.example.com/rpc', }) const noCacheLink = new RPCLink({ url: 'https://example.com/rpc', }) const link = new DynamicLink((options, path, input) => { if (options.context?.cache) { return cacheLink } return noCacheLink }) const client: RouterClient = createORPCClient(link) ``` :::info Any oRPC's link is supported, not strictly limited to `RPCLink`. ::: --- --- url: /docs/ecosystem.md description: oRPC ecosystem & community resources --- # Ecosystem :::info If your project is missing here, please [open a PR](https://github.com/unnoq/orpc/edit/main/apps/content/docs/ecosystem.md) to add it. ::: ## Starter Kits | Name | Stars | Description | | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | [Zap.ts](https://zap-ts.alexandretrotel.org/) | [![Stars](https://img.shields.io/github/stars/alexandretrotel/zap.ts?style=flat)](https://github.com/alexandretrotel/zap.ts) | Next.js boilerplate designed to help you build applications faster using a modern set of tools. | | [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack) | [![Stars](https://img.shields.io/github/stars/AmanVarshney01/create-better-t-stack?style=flat)](https://github.com/AmanVarshney01/create-better-t-stack) | A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations | ## Tools | Name | Stars | Description | | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | [orpc-file-based-router](https://github.com/zeeeeby/orpc-file-based-router) | [![Stars](https://img.shields.io/github/stars/zeeeeby/orpc-file-based-router?style=flat)](https://github.com/zeeeeby/orpc-file-based-router) | Automatically creates an oRPC router configuration based on your file structure, similar to Next.js, express-file-routing | | [Vertrag](https://github.com/Quatton/vertrag) | [![Stars](https://img.shields.io/github/stars/Quatton/vertrag?style=flat)](https://github.com/Quatton/vertrag) | A spec-first API development tool (oRPC contract + any backend language) | ## Libraries | Name | Stars | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | [Permix](https://permix.letstri.dev/) | [![Stars](https://img.shields.io/github/stars/letstri/permix?style=flat)](https://github.com/letstri/permix) | lightweight, framework-agnostic, type-safe permissions management library | --- --- url: /docs/integrations/elysia.md description: Integrate oRPC with Elysia --- # Elysia Integration [Elysia](https://elysiajs.com/) is a high-performance web framework for [Bun](https://bun.sh/) that adheres to the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ```ts import { Elysia } from 'elysia' import { OpenAPIHandler } from '@orpc/openapi/fetch' const handler = new OpenAPIHandler(router) const app = new Elysia() .all('/rpc*', async ({ request }: { request: Request }) => { const { response } = await handler.handle(request, { prefix: '/rpc', }) return response ?? new Response('Not Found', { status: 404 }) }) .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/openapi-handler), or another custom handler. ::: --- --- url: /docs/error-handling.md description: Manage errors in oRPC using both traditional and type‑safe strategies. --- # Error Handling in oRPC oRPC offers a robust error handling system. You can either throw standard JavaScript errors or, preferably, use the specialized `ORPCError` class to utilize oRPC features. There are two primary approaches: * **Normal Approach:** Throw errors directly (using `ORPCError` is recommended for clarity). * **Type‑Safe Approach:** Predefine error types so that clients can infer and handle errors in a type‑safe manner. :::warning The `ORPCError.data` property is sent to the client. Avoid including sensitive information. ::: ## Normal Approach In the traditional approach you may throw any JavaScript error. However, using the `ORPCError` class improves consistency and ensures that error codes and optional data are handled appropriately. **Key Points:** * The first argument is the error code. * You may optionally include a message, additional error data, or any standard error options. ```ts const rateLimit = os.middleware(async ({ next }) => { throw new ORPCError('RATE_LIMITED', { message: 'You are being rate limited', data: { retryAfter: 60 } }) return next() }) const example = os .use(rateLimit) .handler(async ({ input }) => { throw new ORPCError('NOT_FOUND') throw new Error('Something went wrong') // <-- will be converted to INTERNAL_SERVER_ERROR }) ``` ::: danger Do not pass sensitive data in the `ORPCError.data` field. ::: ## Type‑Safe Error Handling For a fully type‑safe error management experience, define your error types using the `.errors` method. This lets the client infer the error’s structure and handle it accordingly. You can use any [Standard Schema](https://github.com/standard-schema/standard-schema?tab=readme-ov-file#what-schema-libraries-implement-the-spec) library to validate error data. ```ts twoslash import { os } from '@orpc/server' import { z } from 'zod' // ---cut--- const base = os.errors({ // <-- common errors RATE_LIMITED: { data: z.object({ retryAfter: z.number(), }), }, UNAUTHORIZED: {}, }) const rateLimit = base.middleware(async ({ next, errors }) => { throw errors.RATE_LIMITED({ message: 'You are being rate limited', data: { retryAfter: 60 } }) return next() }) const example = base .use(rateLimit) .errors({ NOT_FOUND: { message: 'The resource was not found', // <-- default message }, }) .handler(async ({ input, errors }) => { throw errors.NOT_FOUND() }) ``` :::danger Again, avoid including any sensitive data in the error data since it will be exposed to the client. ::: Learn more about [Client Error Handling](/docs/client/error-handling). ## Combining Both Approaches You can combine both strategies seamlessly. When you throw an `ORPCError` instance, if the `code` and `data` match with the errors defined in the `.errors` method, oRPC will treat it exactly as if you had thrown `errors.[code]` using the type‑safe approach. ```ts const base = os.errors({ // <-- common errors RATE_LIMITED: { data: z.object({ retryAfter: z.number().int().min(1).default(1), }), }, UNAUTHORIZED: {}, }) const rateLimit = base.middleware(async ({ next, errors }) => { throw errors.RATE_LIMITED({ message: 'You are being rate limited', data: { retryAfter: 60 } }) // OR --- both are equivalent throw new ORPCError('RATE_LIMITED', { message: 'You are being rate limited', data: { retryAfter: 60 } }) return next() }) const example = base .use(rateLimit) .handler(async ({ input }) => { throw new ORPCError('BAD_REQUEST') // <-- unknown error }) ``` :::danger Remember: Since `ORPCError.data` is transmitted to the client, do not include any sensitive information. ::: --- --- url: /docs/client/error-handling.md description: Learn how to handle errors in a type-safe way in oRPC clients. --- # Error Handling in oRPC Clients This guide explains how to handle type-safe errors in oRPC clients using [type-safe error handling](/docs/error-handling#type‐safe-error-handling). Both [server-side](/docs/client/server-side) and [client-side](/docs/client/client-side) clients are supported. ## Using `safe` and `isDefinedError` ```ts twoslash import { os } from '@orpc/server' import { z } from 'zod' // ---cut--- import { isDefinedError, safe } from '@orpc/client' const doSomething = 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 } }) return { id: input.id } }) .callable() const [error, data, isDefined] = await safe(doSomething({ id: '123' })) // or const { error, data, isDefined } = await safe(doSomething({ id: '123' })) if (isDefinedError(error)) { // or isDefined // handle known error console.log(error.data.retryAfter) } else if (error) { // handle unknown error } else { // handle success console.log(data) } ``` :::info * `safe` works like `try/catch`, but can infer error types. * `safe` supports both tuple `[error, data, isDefined]` and object `{ error, data, isDefined }` styles. * `isDefinedError` checks if an error originates from `.errors`. * `isDefined` can replace `isDefinedError` ::: --- --- url: /docs/event-iterator.md description: >- Learn how to streaming responses, real-time updates, and server-sent events using oRPC. --- # Event Iterator (SSE) oRPC provides built‑in support for streaming responses, real‑time updates, and server-sent events (SSE) without any extra configuration. This functionality is ideal for applications that require live updates—such as AI chat responses, live sports scores, or stock market data. ## Overview The event iterator is defined by an asynchronous generator function. In the example below, the handler continuously yields a new event every second: ```ts const example = os .handler(async function* ({ input, lastEventId }) { while (true) { yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } }) ``` Learn how to consume the event iterator on the client [here](/docs/client/event-iterator) ## Validate Event Iterator oRPC includes a built‑in `eventIterator` helper that works with any [Standard Schema](https://github.com/standard-schema/standard-schema?tab=readme-ov-file#what-schema-libraries-implement-the-spec) library to validate events. ```ts import { eventIterator } from '@orpc/server' const example = os .output(eventIterator(z.object({ message: z.string() }))) .handler(async function* ({ input, lastEventId }) { while (true) { 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 meta](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 a retry interval) to each event. ::: info When used with [Client Retry Plugin](/docs/plugins/client-retry) or [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), the client will reconnect with the last event ID. This value is made available to your handler as `lastEventId`, allowing you to resume the stream seamlessly. ::: ```ts import { withEventMeta } from '@orpc/server' const example = os .handler(async function* ({ input, lastEventId }) { if (lastEventId) { // Resume streaming from lastEventId } else { while (true) { yield withEventMeta({ message: 'Hello, world!' }, { id: 'some-id', retry: 10_000 }) await new Promise(resolve => setTimeout(resolve, 1000)) } } }) ``` ## Stop Event Iterator To signal the end of the stream, simply use a `return` statement. When the handler returns, oRPC marks the stream as successfully completed. :::warning This behavior is exclusive to oRPC. Standard [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) clients, such as those using [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) will automatically reconnect when the connection closes. ::: ```ts const example = os .handler(async function* ({ input, lastEventId }) { while (true) { if (done) { return } yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } }) ``` ## Cleanup Side-Effects If the client closes the connection or an unexpected error occurs, you can use a `finally` block to clean up any side effects (for example, closing database connections or stopping background tasks): ```ts const example = os .handler(async function* ({ input, lastEventId }) { try { while (true) { yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } } finally { console.log('Cleanup logic here') } }) ``` --- --- url: /docs/client/event-iterator.md description: Learn how to use event iterators in oRPC clients. --- # Event Iterator in oRPC Clients An [Event Iterator](/docs/event-iterator) in oRPC behaves like an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator). Simply iterate over it and await each event. ## Basic Usage ```ts twoslash import { ContractRouterClient, eventIterator, oc } from '@orpc/contract' import { z } from 'zod' const contract = { streaming: oc.output(eventIterator(z.object({ message: z.string() }))) } declare const client: ContractRouterClient // ---cut--- const iterator = await client.streaming() for await (const event of iterator) { console.log(event.message) } ``` ## Stopping the Stream Manually Call `.return()` on the iterator to gracefully end the stream. ```ts const iterator = await client.streaming() for await (const event of iterator) { if (wantToStop) { await iterator.return() break } console.log(event.message) } ``` ## Error Handling ::: info Unlike traditional SSE, the Event Iterator does not automatically retry on error. To enable automatic retries, refer to the [Client Retry Plugin](/docs/plugins/client-retry). ::: ```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 } } ``` ::: info Errors thrown by the server can be instances of `ORPCError`. ::: --- --- url: /docs/advanced/exceeds-the-maximum-length-problem.md description: How to address the Exceeds the Maximum Length Problem in oRPC. --- # Exceeds the Maximum Length Problem ```ts twoslash // @error: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed. export const router = { // many procedures here } ``` Are you seeing this error? If so, congratulations! your project is now complex enough to encounter it! ## Why It Happens This error is expected, not a bug. Typescript enforces this to keep your IDE suggestions fast. It appears when all three of these conditions are met: 1. Your project uses `"declaration": true` in `tsconfig.json`. 2. Your project is large or your types are very complex. 3. You export your router as a single, large object. ## How to Fix It ### 1. Disable `"declaration": true` in `tsconfig.json` This is the simplest option, though it may not be ideal for your project. ### 2. Define the `.output` Type for Your Procedures By explicitly specifying the `.output` or your `handler's return type`, you enable TypeScript to infer the output without parsing the handler's code. This approach can dramatically enhance both type-checking and IDE-suggestion speed. :::tip Use the [type](/docs/procedure#type-utility) utility if you just want to specify the output type without validating the output. ::: ### 3. Export the Router in Parts Instead of exporting one large object on the server (with `"declaration": true`), export each router segment individually and merge them on the client (where `"declaration": false`): ```ts export const userRouter = { /** ... */ } export const planetRouter = { /** ... */ } export const publicRouter = { /** ... */ } ``` Then, on the client side: ```ts interface Router { user: typeof userRouter planet: typeof planetRouter public: typeof publicRouter } export const client: RouterClient = createORPCClient(link) ``` --- --- url: /docs/integrations/express.md description: Seamlessly integrate oRPC with Express.js --- # Express.js Integration [Express.js](https://expressjs.com/) is a popular Node.js framework for building web applications. For additional context, refer to the [Node Integration](/docs/integrations/node) guide. ::: warning oRPC uses its own request parser. To avoid conflicts, register any body-parsing middleware **after** your oRPC middleware or only on routes that don't use oRPC. ::: ## Basic ```ts import express from 'express' import cors from 'cors' import { RPCHandler } from '@orpc/server/node' const app = express() app.use(cors()) const handler = new RPCHandler(router) app.use('/rpc*', async (req, res, next) => { const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {}, }) 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/openapi-handler), or another custom handler. ::: --- --- url: /docs/integrations/fetch-server.md description: Integrate oRPC with the modern Fetch API Server --- # Fetch Server Integration The Fetch API Server is a lightweight and high-performance server available in modern runtimes such as [Deno](https://deno.land/), [Bun](https://bun.sh/), and [Cloudflare Workers](https://workers.cloudflare.com/). ## Basic ```ts import { RPCHandler } from '@orpc/server/fetch' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin() ] }) 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 }) } ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/file-upload-download.md description: Learn how to upload and download files using oRPC. --- # File Operations in oRPC oRPC natively supports file uploads and downloads using standard [File](https://developer.mozilla.org/en-US/docs/Web/API/File) and [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, requiring no additional configuration. :::tip For files larger than 10MB, it is recommended to use a dedicated solution for performance and reliability. ::: ## Validation oRPC uses the standard [File](https://developer.mozilla.org/en-US/docs/Web/API/File) and [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects to handle file operations. To validate file uploads and downloads, you can use the `z.instanceof(File)` and `z.instanceof(Blob)` validators, or equivalent schemas in libraries like Valibot or Arktype. ```ts twoslash import { os } from '@orpc/server' import { z } from 'zod' // ---cut--- const example = os .input(z.object({ file: z.instanceof(File) })) .output(z.object({ file: z.instanceof(File) })) .handler(async ({ input }) => { console.log(input.file.name) return { file: input.file } }) ``` :::info If you are using Node.js 18, you can import the `File` class from the `buffer` module. ::: --- --- url: /docs/getting-started.md description: Quick guide to oRPC --- # Getting Started oRPC (OpenAPI Remote Procedure Call) combines RPC (Remote Procedure Call) with OpenAPI, allowing you to define and call remote (or local) procedures through a type-safe API while adhering to the OpenAPI specification. oRPC simplifies RPC service definition, making it easy to build scalable applications—from simple scripts to complex microservices. This guide covers the basics: defining procedures, handling errors, and integrating with popular frameworks. ## Prerequisites * Node.js 18+ (20+ recommended) | Bun | Deno | Cloudflare Workers * A package manager: npm | pnpm | yarn | bun | deno * A TypeScript project (strict mode recommended) ## Installation ::: code-group ```sh [npm] npm install @orpc/server@latest @orpc/client@latest ``` ```sh [yarn] yarn add @orpc/server@latest @orpc/client@latest ``` ```sh [pnpm] pnpm add @orpc/server@latest @orpc/client@latest ``` ```sh [bun] bun add @orpc/server@latest @orpc/client@latest ``` ```sh [deno] deno install npm:@orpc/server@latest npm:@orpc/client@latest ``` ::: ## Define App Router We'll use [Zod](https://github.com/colinhacks/zod) for schema validation (optional, any [standard schema](https://github.com/standard-schema/standard-schema) is supported). ```ts twoslash import type { IncomingHttpHeaders } from 'node:http' import { ORPCError, os } from '@orpc/server' import { z } from 'zod' const PlanetSchema = z.object({ id: z.number().int().min(1), name: z.string(), description: z.string().optional(), }) export const listPlanet = os .input( z.object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.number().int().min(0).default(0), }), ) .handler(async ({ input }) => { // your list code here return [{ id: 1, name: 'name' }] }) export const findPlanet = os .input(PlanetSchema.pick({ id: true })) .handler(async ({ input }) => { // your find code here return { id: 1, name: 'name' } }) export const createPlanet = os .$context<{ headers: IncomingHttpHeaders }>() .use(({ context, next }) => { const user = parseJWT(context.headers.authorization?.split(' ')[1]) if (user) { return next({ context: { user } }) } throw new ORPCError('UNAUTHORIZED') }) .input(PlanetSchema.omit({ id: true })) .handler(async ({ input, context }) => { // your create code here return { id: 1, name: 'name' } }) export const router = { planet: { list: listPlanet, find: findPlanet, create: createPlanet } } // ---cut-after--- declare function parseJWT(token: string | undefined): { userId: number } | null ``` ## Create Server Using [Node.js](/docs/integrations/node) as the server runtime, but oRPC also supports other runtimes like [Bun](/docs/integrations/bun), [Deno](/docs/integrations/deno), [Cloudflare Workers](/docs/integrations/cloudflare-workers), ... ```ts twoslash import { router } from './shared/planet' // ---cut--- import { createServer } from 'node:http' import { RPCHandler } from '@orpc/server/node' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [new CORSPlugin()] }) const server = createServer(async (req, res) => { const result = await handler.handle(req, res, { context: { headers: req.headers } }) if (!result.matched) { res.statusCode = 404 res.end('No procedure matched') } }) server.listen( 3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000') ) ``` Learn more about [RPCHandler](/docs/rpc-handler). ## Create Client ```ts twoslash import { router } from './shared/planet' // ---cut--- import type { RouterClient } from '@orpc/server' import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' const link = new RPCLink({ url: 'http://127.0.0.1:3000', headers: { Authorization: 'Bearer token' }, }) export const orpc: RouterClient = createORPCClient(link) ``` Supports both [client-side clients](/docs/client/client-side) and [server-side clients](/docs/client/server-side). ## Call Procedure End-to-end type-safety and auto-completion out of the box. ```ts twoslash import { orpc } from './shared/planet' // ---cut--- const planet = await orpc.planet.find({ id: 1 }) orpc.planet.create // ^| ``` ## Next Steps This guide introduced the RPC aspects of oRPC. To explore OpenAPI integration, visit the [OpenAPI Guide](/docs/openapi/getting-started). --- --- url: /docs/openapi/getting-started.md description: Quick guide to OpenAPI in oRPC --- # Getting Started OpenAPI is a widely adopted standard for describing RESTful APIs. With oRPC, you can easily publish OpenAPI-compliant APIs with minimal effort. oRPC is inherently compatible with OpenAPI, but you may need additional configurations such as path prefixes, custom routing, or including headers, parameters, and queries in inputs and outputs. This guide explains how to make your oRPC setup fully OpenAPI-compatible. It assumes basic knowledge of oRPC or familiarity with the [Getting Started](/docs/getting-started) guide. ## Prerequisites * Node.js 18+ (20+ recommended) | Bun | Deno | Cloudflare Workers * A package manager: npm | pnpm | yarn | bun | deno * A TypeScript project (strict mode recommended) ## Installation ::: code-group ```sh [npm] npm install @orpc/server@latest @orpc/client@latest @orpc/openapi@latest ``` ```sh [yarn] yarn add @orpc/server@latest @orpc/client@latest @orpc/openapi@latest ``` ```sh [pnpm] pnpm add @orpc/server@latest @orpc/client@latest @orpc/openapi@latest ``` ```sh [bun] bun add @orpc/server@latest @orpc/client@latest @orpc/openapi@latest ``` ```sh [deno] deno install npm:@orpc/server@latest npm:@orpc/client@latest @orpc/openapi@latest ``` ::: ## Defining Routes This snippet is based on the [Getting Started](/docs/getting-started) guide. Please read it first. ```ts twoslash import type { IncomingHttpHeaders } from 'node:http' import { ORPCError, os } from '@orpc/server' import { z } from 'zod' const PlanetSchema = z.object({ id: z.number().int().min(1), name: z.string(), description: z.string().optional(), }) export const listPlanet = os .route({ method: 'GET', path: '/planets' }) .input(z.object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.number().int().min(0).default(0), })) .output(z.array(PlanetSchema)) .handler(async ({ input }) => { // your list code here return [{ id: 1, name: 'name' }] }) export const findPlanet = os .route({ method: 'GET', path: '/planets/{id}' }) .input(z.object({ id: z.coerce.number().int().min(1) })) .output(PlanetSchema) .handler(async ({ input }) => { // your find code here return { id: 1, name: 'name' } }) export const createPlanet = os .$context<{ headers: IncomingHttpHeaders }>() .use(({ context, next }) => { const user = parseJWT(context.headers.authorization?.split(' ')[1]) if (user) { return next({ context: { user } }) } throw new ORPCError('UNAUTHORIZED') }) .route({ method: 'POST', path: '/planets' }) .input(PlanetSchema.omit({ id: true })) .output(PlanetSchema) .handler(async ({ input, context }) => { // your create code here return { id: 1, name: 'name' } }) export const router = { planet: { list: listPlanet, find: findPlanet, create: createPlanet } } // ---cut-after--- declare function parseJWT(token: string | undefined): { userId: number } | null ``` ### Key Enhancements: * `.route` defines HTTP methods and paths. * `.output` enables automatic OpenAPI spec generation. * `z.coerce` ensures correct parameter parsing. For handling headers, queries, etc., see [Input/Output Structure](/docs/openapi/input-output-structure). For auto-coercion, see [Zod Smart Coercion Plugin](/docs/openapi/plugins/zod-smart-coercion). For more `.route` options, see [Routing](/docs/openapi/routing). ## Creating a Server ```ts twoslash import { router } from './shared/planet' // ---cut--- import { createServer } from 'node:http' import { OpenAPIHandler } from '@orpc/openapi/node' import { CORSPlugin } from '@orpc/server/plugins' const handler = new OpenAPIHandler(router, { plugins: [new CORSPlugin()] }) const server = createServer(async (req, res) => { const result = await handler.handle(req, res, { context: { headers: req.headers } }) if (!result.matched) { res.statusCode = 404 res.end('No procedure matched') } }) server.listen( 3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000') ) ``` ### Important Changes: * Use `OpenAPIHandler` instead of `RPCHandler`. * Learn more in [OpenAPIHandler](/docs/openapi/openapi-handler). ## Accessing APIs ```bash curl -X GET http://127.0.0.1:3000/planets curl -X GET http://127.0.0.1:3000/planets/1 curl -X POST http://127.0.0.1:3000/planets \ -H 'Authorization: Bearer token' \ -H 'Content-Type: application/json' \ -d '{"name": "name"}' ``` Just a small tweak makes your oRPC API OpenAPI-compliant! ## Generating OpenAPI Spec ```ts twoslash import { OpenAPIGenerator } from '@orpc/openapi' import { ZodToJsonSchemaConverter } from '@orpc/zod' import { router } from './shared/planet' const generator = new OpenAPIGenerator({ schemaConverters: [ new ZodToJsonSchemaConverter() ] }) const spec = await generator.generate(router, { info: { title: 'Planet API', version: '1.0.0' } }) console.log(JSON.stringify(spec, null, 2)) ``` Run the script above to generate your OpenAPI spec. ::: info oRPC supports a wide range of [Standard Schema](https://github.com/standard-schema/standard-schema) for OpenAPI generation. See the full list [here](/docs/openapi/openapi-specification#generating-specifications) ::: --- --- url: /docs/integrations/hono.md description: Integrate oRPC with Hono --- # Hono Integration [Hono](https://honojs.dev/) is a high-performance web framework built on top of [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ```ts import { Hono } from 'hono' import { RPCHandler } from '@orpc/server/fetch' const app = new Hono() const handler = new RPCHandler(router) 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 ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/contract-first/implement-contract.md description: Learn how to implement a contract for contract-first development in oRPC --- # Implement Contract After defining your contract, the next step is to implement it in your server code. oRPC enforces your contract at runtime, ensuring that your API consistently adheres to its specifications. ## Installation ::: code-group ```sh [npm] npm install @orpc/server@latest ``` ```sh [yarn] yarn add @orpc/server@latest ``` ```sh [pnpm] pnpm add @orpc/server@latest ``` ```sh [bun] bun add @orpc/server@latest ``` ```sh [deno] deno install npm:@orpc/server@latest ``` ::: ## The Implementer The `implement` function converts your contract into an implementer instance. This instance compatible with the original `os` from `@orpc/server` provides a type-safe interface to define your procedures and supports features like [Middleware](/docs/middleware) and [Context](/docs/context). ```ts twoslash import { contract } from './shared/planet' // ---cut--- import { implement } from '@orpc/server' const os = implement(contract) // fully replaces the os from @orpc/server ``` ## Implementing Procedures Define a procedure by attaching a `.handler` to its corresponding contract, ensuring it adheres to the contract’s specifications. ```ts twoslash import { contract } from './shared/planet' import { implement } from '@orpc/server' const os = implement(contract) // ---cut--- export const listPlanet = os.planet.list .handler(({ input }) => { // Your logic for listing planets return [] }) ``` ## Building the Router To assemble your API, create a router at the root level using `.router`. This ensures that the entire router is type-checked and enforces the contract at runtime. ```ts const router = os.router({ // <-- Essential for full contract enforcement planet: { list: listPlanet, find: findPlanet, create: createPlanet, }, }) ``` ## Full Implementation Example Below is a complete implementation of the contract defined in the [previous section](/docs/contract-first/define-contract). ```ts twoslash import { contract } from './shared/planet' import { implement } from '@orpc/server' // ---cut--- const os = implement(contract) export const listPlanet = os.planet.list .handler(({ input }) => { return [] }) export const findPlanet = os.planet.find .handler(({ input }) => { return { id: 123, name: 'Planet X' } }) export const createPlanet = os.planet.create .handler(({ input }) => { return { id: 123, name: 'Planet X' } }) export const router = os.router({ planet: { list: listPlanet, find: findPlanet, create: createPlanet, }, }) ``` --- --- url: /docs/openapi/input-output-structure.md description: Control how input and output data is structured in oRPC --- # Input/Output Structure oRPC allows you to control the organization of request inputs and response outputs using the `inputStructure` and `outputStructure` options. This is especially useful when you need to handle parameters, query strings, headers, and body data separately. ## Input Structure The `inputStructure` option defines how the incoming request data is structured. You can choose between two modes: * **compact** (default): Merges path parameters with either the query or body data (depending on the HTTP method) into a single object. * **detailed**: Separates the request into distinct objects for `params`, `query`, `headers`, and `body`. ### Compact Mode ```ts const compactMode = os.route({ path: '/ping/{name}', method: 'POST', }) .input(z.object({ name: z.string(), description: z.string().optional(), })) ``` ### Detailed Mode ```ts const detailedMode = os.route({ path: '/ping/{name}', method: 'POST', inputStructure: 'detailed', }) .input(z.object({ params: z.object({ name: z.string() }), query: z.object({ search: z.string() }), body: z.object({ description: z.string() }).optional(), headers: z.object({ 'x-custom-header': z.string() }), })) ``` When using **detailed** mode, the input object adheres to the following structure: ```ts export type DetailedInput = { params: Record | undefined query: any body: any headers: Record } ``` Ensure your input schema matches this structure when detailed mode is enabled. ## Output Structure The `outputStructure` option determines the format of the response data. There are two modes: * **compact** (default): Returns only the body data directly. * **detailed**: Returns an object with separate `headers` and `body` fields. The headers you provide are merged into the final HTTP response headers. ### Compact Mode ```ts const compactMode = os .handler(async ({ input }) => { return { message: 'Hello, world!' } }) ``` ### Detailed Mode ```ts const detailedMode = os .route({ outputStructure: 'detailed' }) .handler(async ({ input }) => { return { headers: { 'x-custom-header': 'value' }, body: { message: 'Hello, world!' }, } }) ``` When using **detailed** mode, the output object follows this structure: ```ts export type DetailedOutput = { headers: Record body: any } ``` Make sure your handler’s return value matches this structure when using detailed mode. ## Initial Configuration Customize the initial oRPC input/output structure settings using `.$route`: ```ts const base = os.$route({ inputStructure: 'detailed' }) ``` --- --- url: /docs/lifecycle.md description: >- Master the oRPC lifecycle to confidently implement and customize your procedures. --- # Lifecycle Master the oRPC lifecycle to confidently implement and customize your procedures. ## Overview | Name | Description | Customizable | | ----------------------- | -------------------------------------------------------------------------------------- | ------------ | | **Handler** | Procedures defined with `.handler`. | ✅ | | **Middlewares** | Procedures added via `.use`. | ✅ | | **Input Validation** | Validates input data against the schema specified in `.input`. | 🟡 | | **Output Validation** | Ensures output data conforms to the schema defined in `.output`. | 🟡 | | **Client Interceptors** | Interceptors executed before Error Validation. | ✅ | | **Error Validation** | Flags errors as defined and validate error data if they match the schema in `.errors`. | 🟡 | | **Routing** | Determines which procedure to execute based on the incoming request. | ✅ | | **Interceptors** | Interceptors executed before the Error Handler. | ✅ | | **Error Handler** | Catches errors and converts them into a response. | 🟡 | | **Root Interceptors** | Modify the request or response as needed. | ✅ | > **Note:** The components *Routing*, *Interceptors*, *Error Handler*, and *Root Interceptors* are not available when using the [server-side client](/docs/client/server-side). ## Middlewares Order To ensure that all middlewares run after input validation and before output validation apply the following configuration: ```ts const base = os.$config({ initialInputValidationIndex: Number.NEGATIVE_INFINITY, initialOutputValidationIndex: Number.NEGATIVE_INFINITY, }) ``` :::info By default, oRPC executes middlewares based on their registration order relative to validation steps. Middlewares registered before `.input` run before input validation, and those registered after `.output` run before output validation. ::: --- --- url: /docs/metadata.md description: Enhance your procedures with metadata. --- # Metadata oRPC procedures support metadata, simple key-value pairs that provide extra information to customize behavior. ## Basic Example ```ts twoslash import { os } from '@orpc/server' declare const db: Map // ---cut--- interface ORPCMetadata { cache?: boolean } const base = os .$meta({}) // require define initial context [!code highlight] .use(async ({ procedure, next, path }, input, output) => { if (!procedure['~orpc'].meta.cache) { return await next() } const cacheKey = path.join('/') + JSON.stringify(input) if (db.has(cacheKey)) { return output(db.get(cacheKey)) } const result = await next() db.set(cacheKey, result.output) return result }) const example = base .meta({ cache: true }) // [!code highlight] .handler(() => { // Implement your procedure logic here }) ``` :::info The `.meta` can be called multiple times; each call [spread merges](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) the new metadata with the existing metadata or the initial metadata. ::: --- --- url: /docs/middleware.md description: Understanding middleware in oRPC --- # Middleware in oRPC Middleware is a powerful feature in oRPC that enables reusable and extensible procedures. It allows you to: * Intercept, hook into, or listen to a handler's execution. * Inject or guard the execution context. ## Overview Middleware is a function that takes a `next` function as a parameter and either returns the result of `next` or modifies the result before returning it. ```ts twoslash import { os } from '@orpc/server' // ---cut--- const authMiddleware = os .$context<{ something?: string }>() // <-- define dependent-context .middleware(async ({ context, next }) => { // Execute logic before the handler const result = await next({ context: { // Pass additional context user: { id: 1, name: 'John' } } }) // Execute logic after the handler return result }) const example = os .use(authMiddleware) .handler(async ({ context }) => { const user = context.user }) ``` ## Dependent context Before `.middleware`, you can `.$context` to specify the dependent context, which must be satisfied when the middleware is used. ## Inline Middleware Middleware can be defined inline within `.use`, which is useful for simple middleware functions. ```ts const example = os .use(async ({ context, next }) => { // Execute logic before the handler return next() }) .handler(async ({ context }) => { // Handler logic }) ``` ## Middleware Context Middleware can use to inject or guard the [context](/docs/context). ```ts twoslash import { ORPCError, os } from '@orpc/server' // ---cut--- const setting = os .use(async ({ context, next }) => { return next({ context: { auth: await auth() // <-- inject auth payload } }) }) .use(async ({ context, next }) => { if (!context.auth) { // <-- guard auth throw new ORPCError('UNAUTHORIZED') } return next({ context: { auth: context.auth // <-- override auth } }) }) .handler(async ({ context }) => { console.log(context.auth) // <-- access auth }) // ---cut-after--- declare function auth(): { userId: number } | null ``` > When you pass additional context to `next`, it will be merged with the existing context. ## Middleware Input Middleware can access input, enabling use cases like permission checks. ```ts const canUpdate = os.middleware(async ({ context, next }, input: number) => { // Perform permission check return next() }) const ping = os .input(z.number()) .use(canUpdate) .handler(async ({ input }) => { // Handler logic }) // Mapping input if necessary const pong = os .input(z.object({ id: z.number() })) .use(canUpdate, input => input.id) .handler(async ({ input }) => { // Handler logic }) ``` ::: info You can adapt a middleware to accept a different input shape by using `.mapInput`. ```ts const canUpdate = os.middleware(async ({ context, next }, input: number) => { return next() }) // Transform middleware to accept a new input shape const mappedCanUpdate = canUpdate.mapInput((input: { id: number }) => input.id) ``` ::: ## Middleware Output Middleware can also modify the output of a handler, such as implementing caching mechanisms. ```ts const cacheMid = os.middleware(async ({ context, next, path }, input, output) => { const cacheKey = path.join('/') + JSON.stringify(input) if (db.has(cacheKey)) { return output(db.get(cacheKey)) } const result = await next({}) db.set(cacheKey, result.output) return result }) ``` ## Concatenation Multiple middleware functions can be combined using `.concat`. ```ts const concatMiddleware = aMiddleware .concat(os.middleware(async ({ next }) => next())) .concat(anotherMiddleware) ``` ::: info If you want to concatenate two middlewares with different input types, you can use `.mapInput` to align their input types before concatenation. ::: ## Built-in Middlewares oRPC provides some built-in middlewares that can be used to simplify common use cases. ```ts import { onError, onFinish, onStart, onSuccess } from '@orpc/server' const ping = os .use(onStart(() => { // Execute logic before the handler })) .use(onSuccess(() => { // Execute when the handler succeeds })) .use(onError(() => { // Execute when the handler fails })) .use(onFinish(() => { // Execute logic after the handler })) .handler(async ({ context }) => { // Handler logic }) ``` --- --- url: /docs/advanced/mocking.md description: Easily mock your oRPC handlers for testing. --- # Mocking Mock your oRPC handlers with ease. ::: warning This page is incomplete and may be missing important information. ::: ## Using the Implementer The [Implementer](/docs/contract-first/implement-contract#the-implementer) is designed for contract-first development. However, it can also be used to create alternative versions of your [router](/docs/router) or [procedure](/docs/procedure) for testing purposes. ```ts twoslash import { router } from './shared/planet' // ---cut--- import { implement, unlazyRouter } from '@orpc/server' const fakeListPlanet = implement(router.planet.list).handler(() => []) ``` You can now use `fakeListPlanet` to replace `listPlanet`. Additionally, the `implement` function can be used to create a fake server for front-end testing. ::: warning The `implement` function does not support the [lazy router](/docs/router#lazy-router) yet. Please use the `unlazyRouter` utility to convert your lazy router before implementing. ::: --- --- url: /docs/integrations/nextjs.md description: Seamlessly integrate oRPC with Next.js --- # Next.js Integration [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). For additional context, refer to the [Node Integration](/docs/integrations/node) and [Fetch Server Integration](/docs/integrations/fetch-server) guides. ::: info oRPC also supports [Server Action](/docs/server-action) out-of-the-box. ::: ## App Router ::: code-group ```ts [app/rpc/[[...rest]]/route.ts] import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(router) 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 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/openapi-handler), or another custom handler. ::: ## Pages Router ::: code-group ```ts [pages/rpc/[[...rest]].ts] import { RPCHandler } from '@orpc/server/node' const handler = new RPCHandler(router) export const config = { api: { bodyParser: false, }, } export default 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') } ``` ::: ::: warning Next.js default [body parser](https://nextjs.org/docs/pages/building-your-application/routing/api-routes#custom-config) blocks oRPC raw‑request handling. Ensure `bodyParser` is disabled in your API route: ```ts export const config = { api: { bodyParser: false, }, } ``` ::: ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/best-practices/no-throw-literal.md description: Always throw `Error` instances instead of literal values. --- # No Throw Literal In JavaScript, you can throw any value, but it's best to throw only `Error` instances. ```ts // eslint-disable-next-line no-throw-literal throw 'error' // ✗ avoid throw new Error('error') // ✓ recommended ``` :::info oRPC treats thrown `Error` instances as best practice by default, as recommended by the [JavaScript Standard Style](https://standardjs.com/rules.html#throw-new-error-old-style). ::: ## Configuration Customize oRPC's behavior by setting `throwableError` in the `Registry`: ```ts declare module '@orpc/server' { // or '@orpc/contract', or '@orpc/client' interface Registry { throwableError: Error // [!code highlight] } } ``` :::info Avoid using `any` or `unknown` for `throwableError` because doing so prevents the client from inferring [type-safe errors](/docs/client/error-handling#using-safe-and-isdefinederror). Instead, use `null | undefined | {}` (equivalent to `unknown`) for stricter error type inference. ::: :::tip If you configure `throwableError` as `null | undefined | {}`, adjust your code to check the `isSuccess` property instead of `error`: ```ts const { error, data, isSuccess } = await safe(client('input')) if (!isSuccess) { if (isDefinedError(error)) { // handle type-safe error } // handle other errors } else { // handle success } ``` ::: ## Bonus If you use ESLint, enable the [no-throw-literal](https://eslint.org/docs/rules/no-throw-literal) rule to enforce throwing only `Error` instances. --- --- url: /docs/integrations/node.md description: Integrate oRPC with Node's built-in HTTP server --- # Node Integration [Node](https://nodejs.org/) is a popular runtime for server-side applications, offering native modules like `node:http`, `node:https`, and `node:http2`. oRPC supports these modules directly. :::info Rather than converting Node’s native server objects to the Fetch API under the hood like other libraries, oRPC integrates with Node’s built-in modules for optimal performance and minimal overhead. ::: ## `node:http` Server ```ts import { createServer } from 'node:http' import { RPCHandler } from '@orpc/server/node' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin() ] }) 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')) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: ## `node:http2` Server ```ts import { createSecureServer } from 'node:http2' import { RPCHandler } from '@orpc/server/node' import { CORSPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin(), ], }) const server = createSecureServer({ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem'), }, 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')) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/integrations/nuxt.md description: Integrate oRPC with Nuxt.js --- # Nuxt.js Integration [Nuxt.js](https://nuxtjs.org/) is a popular Vue.js framework for building server-side applications. It built on top of [Nitro](https://nitro.build/) server a lightweight, high-performance Node.js runtime. For more details, see the [Node Integration](/docs/integrations/node) guide. ## Basic ::: code-group ```ts [server/routes/rpc/[...].ts] import { RPCHandler } from '@orpc/server/node' const handler = new RPCHandler(router) export default defineEventHandler(async (event) => { const { matched } = await handler.handle( event.node.req, event.node.res, { prefix: '/rpc', context: {}, // Provide initial context if needed } ) if (matched) { return } setResponseStatus(event, 404, 'Not Found') return 'Not found' }) ``` ```ts [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/openapi-handler), or another custom handler. ::: --- --- url: /examples/openai-streaming.md description: Combine oRPC with the OpenAI Streaming API to build a chatbot --- # OpenAI Streaming Example This example shows how to integrate oRPC with the OpenAI Streaming API to build a chatbot. ## Basic Example ```ts twoslash import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' import { os, RouterClient } from '@orpc/server' import { z } from 'zod' // ---cut--- import OpenAI from 'openai' const openapi = new OpenAI() const complete = os .input(z.object({ content: z.string() })) .handler(async function* ({ input }) { const stream = await openapi.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: input.content }], stream: true, }) yield* stream }) const router = { complete } const link = new RPCLink({ url: 'https://example.com/rpc', }) const client: RouterClient = createORPCClient(link) const stream = await client.complete({ content: 'Hello, world!' }) for await (const chunk of stream) { console.log(chunk.choices[0]?.delta?.content || '') } ``` Learn more about [RPCLink](/docs/client/rpc-link) and [Event Iterator](/docs/client/event-iterator). --- --- url: /docs/openapi/error-handling.md description: Handle errors in your OpenAPI-compliant oRPC APIs --- # OpenAPI Error Handling Before you begin, please review our [Error Handling](/docs/error-handling) guide. This document shows you how to align your error responses with OpenAPI standards. ## Default Error Mappings By default, oRPC maps common error codes to standard HTTP status codes: | Error Code | HTTP Status Code | Message | | ---------------------- | ---------------: | ---------------------- | | BAD\_REQUEST | 400 | Bad Request | | UNAUTHORIZED | 401 | Unauthorized | | FORBIDDEN | 403 | Forbidden | | NOT\_FOUND | 404 | Not Found | | METHOD\_NOT\_SUPPORTED | 405 | Method Not Supported | | NOT\_ACCEPTABLE | 406 | Not Acceptable | | TIMEOUT | 408 | Request Timeout | | CONFLICT | 409 | Conflict | | PRECONDITION\_FAILED | 412 | Precondition Failed | | PAYLOAD\_TOO\_LARGE | 413 | Payload Too Large | | UNSUPPORTED\_MEDIA\_TYPE | 415 | Unsupported Media Type | | UNPROCESSABLE\_CONTENT | 422 | Unprocessable Content | | TOO\_MANY\_REQUESTS | 429 | Too Many Requests | | CLIENT\_CLOSED\_REQUEST | 499 | Client Closed Request | | INTERNAL\_SERVER\_ERROR | 500 | Internal Server Error | | NOT\_IMPLEMENTED | 501 | Not Implemented | | BAD\_GATEWAY | 502 | Bad Gateway | | SERVICE\_UNAVAILABLE | 503 | Service Unavailable | | GATEWAY\_TIMEOUT | 504 | Gateway Timeout | Any error not defined above defaults to HTTP status `500` with the error code used as the message. ## Customizing Errors You can override the default mappings by specifying a custom `status` and `message` when creating an error: ```ts const example = os .errors({ RANDOM_ERROR: { status: 503, // <-- override default status message: 'Default error message', // <-- override default message }, }) .handler(() => { throw new ORPCError('ANOTHER_RANDOM_ERROR', { status: 502, // <-- override default status message: 'Custom error message', // <-- override default message }) }) ``` --- --- url: /docs/openapi/openapi-handler.md description: Comprehensive Guide to the OpenAPIHandler in oRPC --- # OpenAPI Handler The `OpenAPIHandler` enables communication with clients over RESTful APIs, adhering to the OpenAPI specification. It is fully compatible with [OpenAPILink](/docs/openapi/client/openapi-link) and the [OpenAPI Specification](/docs/openapi/openapi-specification). ## Supported Data Types `OpenAPIHandler` serializes and deserializes the following JavaScript types: * **string** * **number** (`NaN` → `null`) * **boolean** * **null** * **undefined** (`undefined` in arrays → `null`) * **Date** (`Invalid Date` → `null`) * **BigInt** (`BigInt` → `string`) * **RegExp** (`RegExp` → `string`) * **URL** (`URL` → `string`) * **Record (object)** * **Array** * **Set** (`Set` → `array`) * **Map** (`Map` → `array`) * **Blob** (unsupported in `AsyncIteratorObject`) * **File** (unsupported in `AsyncIteratorObject`) * **AsyncIteratorObject** (only at the root level; powers the [Event Iterator](/docs/event-iterator)) ::: warning If a payload contains `Blob` or `File` outside the root level, it must use `multipart/form-data`. In such cases, oRPC applies [Bracket Notation](/docs/openapi/bracket-notation) and converts other types to strings (exclude `null` and `undefined` will not be represented). ::: :::tip You can extend the list of supported types by [creating a custom serializer](/docs/openapi/advanced/openapi-json-serializer#extending-native-data-types). ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/openapi@latest ``` ```sh [yarn] yarn add @orpc/openapi@latest ``` ```sh [pnpm] pnpm add @orpc/openapi@latest ``` ```sh [bun] bun add @orpc/openapi@latest ``` ```sh [deno] deno install npm:@orpc/openapi@latest ``` ::: ## Setup and Integration ```ts import { OpenAPIHandler } from '@orpc/openapi/fetch' // or '@orpc/server/node' import { CORSPlugin } from '@orpc/server/plugins' import { onError } from '@orpc/server' const handler = new OpenAPIHandler(router, { plugins: [new CORSPlugin()], interceptors: [ onError(error => console.error(error)) ], }) export default async function fetch(request: Request) { const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} // Add initial context if needed }) if (matched) { response } return new Response('Not Found', { status: 404 }) } ``` ## Event Iterator Keep Alive To keep [Event Iterator](/docs/event-iterator) connections alive, `OpenAPIHandler` periodically sends a ping comment to the client. You can configure this behavior using the following options: * `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings. * `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds). * `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping comments. ```ts const handler = new OpenAPIHandler(router, { eventIteratorKeepAliveEnabled: true, eventIteratorKeepAliveInterval: 5000, // 5 seconds eventIteratorKeepAliveComment: '', }) ``` --- --- url: /docs/openapi/advanced/openapi-json-serializer.md description: Extend or override the standard OpenAPI JSON serializer. --- # OpenAPI JSON Serializer This serializer processes JSON payloads for the [OpenAPIHandler](/docs/openapi/openapi-handler) and supports [native data types](/docs/openapi/openapi-handler#supported-data-types). ## Extending Native Data Types Customize serialization by creating your own `StandardOpenAPICustomJsonSerializer` and adding it to the `customJsonSerializers` option. 1. **Define Your Custom Serializer** ```ts twoslash import type { StandardOpenAPICustomJsonSerializer } from '@orpc/openapi-client/standard' export class User { constructor( public readonly id: string, public readonly name: string, public readonly email: string, public readonly age: number, ) {} toJSON() { return { id: this.id, name: this.name, email: this.email, age: this.age, } } } export const userSerializer: StandardOpenAPICustomJsonSerializer = { condition: data => data instanceof User, serialize: data => data.toJSON(), } ``` 2. **Use Your Custom Serializer** ```ts twoslash import type { StandardOpenAPICustomJsonSerializer } from '@orpc/openapi-client/standard' import { OpenAPIHandler } from '@orpc/openapi/fetch' import { OpenAPIGenerator } from '@orpc/openapi' declare const router: Record declare const userSerializer: StandardOpenAPICustomJsonSerializer // ---cut--- const handler = new OpenAPIHandler(router, { customJsonSerializers: [userSerializer], }) const generator = new OpenAPIGenerator({ customJsonSerializers: [userSerializer], }) ``` ::: info It is recommended to add custom serializers to the `OpenAPIGenerator` for consistent serialization in the OpenAPI document. ::: --- --- url: /docs/openapi/plugins/openapi-reference.md description: >- A plugin that serves API reference documentation and the OpenAPI specification for your API. --- # OpenAPI Reference Plugin (Swagger/Scalar) This plugin provides API reference documentation powered by [Scalar](https://github.com/scalar/scalar), along with the OpenAPI specification in JSON format. ::: info This plugin relies on the [OpenAPI Generator](/docs/openapi/openapi-specification). Please review its documentation before using this plugin. ::: ## Setup ```ts import { ZodToJsonSchemaConverter } from '@orpc/zod' import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins' const handler = new OpenAPIHandler(router, { plugins: [ new OpenAPIReferencePlugin({ schemaConverters: [ new ZodToJsonSchemaConverter(), ], specGenerateOptions: { info: { title: 'ORPC Playground', version: '1.0.0', }, }, }), ] }) ``` ::: info By default, the API reference client is served at the root path (`/`), and the OpenAPI specification is available at `/spec.json`. You can customize these paths by providing the `docsPath` and `specPath` options. ::: --- --- url: /docs/openapi/routing.md description: Configure procedure routing with oRPC. --- # Routing Define how procedures map to HTTP methods, paths, and response statuses. :::warning This feature applies only when using [OpenAPIHandler](/docs/openapi/openapi-handler). ::: ## Basic Routing By default, oRPC uses the `POST` method, constructs paths from router keys with `/`, and returns a 200 status on success. Override these defaults with `.route`: ```ts os.route({ method: 'GET', path: '/example', successStatus: 200 }) os.route({ method: 'POST', path: '/example', successStatus: 201 }) ``` :::info The `.route` can be called multiple times; each call [spread merges](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) the new route with the existing route. ::: ## Path Parameters By default, path parameters merge with query/body into a single input object. You can modify this behavior as described in the [Input/Output structure docs](/docs/openapi/input-output-structure). ```ts os.route({ path: '/example/{id}' }) .input(z.object({ id: z.string() })) os.route({ path: '/example/{+path}' }) // Matches slashes (/) .input(z.object({ path: z.string() })) ``` ## Route Prefixes Use `.prefix` to prepend a common path to all procedures in a router that have an explicitly defined `path`: ```ts const router = os.prefix('/planets').router({ list: listPlanet, find: findPlanet, create: createPlanet, }) ``` ::: warning The prefix only applies to procedures that specify a `path`. ::: ## Lazy Router When combining a [Lazy Router](/docs/router#lazy-router) with [OpenAPIHandler](/docs/openapi/openapi-handler), a prefix is required for lazy loading. Without it, the router behaves like a regular router. :::info If you follow the [contract-first approach](/docs/contract-first/define-contract), you can ignore this requirement—oRPC knows the full contract and loads the router lazily properly. ::: ```ts const router = { planet: os.prefix('/planets').lazy(() => import('./planet')) } ``` ## Initial Configuration Customize the initial oRPC routing settings using `.$route`: ```ts const base = os.$route({ method: 'GET' }) ``` --- --- url: /docs/openapi/openapi-specification.md description: Generate OpenAPI specifications for oRPC with ease. --- # OpenAPI Specification oRPC uses the [OpenAPI Specification](https://spec.openapis.org/oas/v3.1.0) to define APIs. It is fully compatible with [OpenAPILink](/docs/openapi/client/openapi-link) and [OpenAPIHandler](/docs/openapi/openapi-handler). ## Installation ::: code-group ```sh [npm] npm install @orpc/openapi@latest ``` ```sh [yarn] yarn add @orpc/openapi@latest ``` ```sh [pnpm] pnpm add @orpc/openapi@latest ``` ```sh [bun] bun add @orpc/openapi@latest ``` ```sh [deno] deno install npm:@orpc/openapi@latest ``` ::: ## Generating Specifications oRPC supports OpenAPI 3.1.1 and integrates seamlessly with popular schema libraries like [Zod](https://zod.dev/), [Valibot](https://valibot.dev), and [ArkType](https://arktype.io/). You can generate specifications from either a [Router](/docs/router) or a [Contract](/docs/contract-first/define-contract): :::info Interested in support for additional schema libraries? [Let us know](https://github.com/unnoq/orpc/discussions/categories/ideas)! ::: ::: details Want to create your own JSON schema converter? You can use any existing `X to JSON Schema` converter to add support for additional schema libraries. For example, if you want to use [Valibot](https://valibot.dev) with oRPC (if not supported), you can create a custom converter to convert Valibot schemas into JSON Schema. ```ts import type { AnySchema } from '@orpc/contract' import type { ConditionalSchemaConverter, JSONSchema, SchemaConvertOptions } from '@orpc/openapi' import type { ConversionConfig } from '@valibot/to-json-schema' import { toJsonSchema } from '@valibot/to-json-schema' export class ValibotToJsonSchemaConverter implements ConditionalSchemaConverter { condition(schema: AnySchema | undefined): boolean { return schema !== undefined && schema['~standard'].vendor === 'valibot' } convert(schema: AnySchema | undefined, _options: SchemaConvertOptions): [required: boolean, jsonSchema: Exclude] { // Most JSON schema converters do not convert the `required` property separately, so returning `true` is acceptable here. return [true, toJsonSchema(schema as any)] } } ``` :::info It's recommended to use the built-in converters because the oRPC implementations handle many edge cases and supports every type that oRPC offers. ::: ```ts twoslash import { contract, router } from './shared/planet' // ---cut--- import { OpenAPIGenerator } from '@orpc/openapi' import { ZodToJsonSchemaConverter } from '@orpc/zod' import { experimental_ValibotToJsonSchemaConverter as ValibotToJsonSchemaConverter } from '@orpc/valibot' import { experimental_ArkTypeToJsonSchemaConverter as ArkTypeToJsonSchemaConverter } from '@orpc/arktype' const openAPIGenerator = new OpenAPIGenerator({ schemaConverters: [ new ZodToJsonSchemaConverter(), // <-- if you use Zod new ValibotToJsonSchemaConverter(), // <-- if you use Valibot new ArkTypeToJsonSchemaConverter(), // <-- if you use ArkType ], }) const specFromContract = await openAPIGenerator.generate(contract, { info: { title: 'My App', version: '0.0.0', }, }) const specFromRouter = await openAPIGenerator.generate(router, { info: { title: 'My App', version: '0.0.0', }, }) ``` :::warning Features prefixed with `experimental_` are unstable and may lack some functionality. ::: ## Operation Metadata You can enrich your API documentation by specifying operation metadata using the `.route` or `.tag`: ```ts const ping = os .route({ summary: 'the summary', description: 'the description', deprecated: false, tags: ['tag'], successDescription: 'the success description', }) .handler(() => {}) // or append tag for entire router const router = os.tag('planets').router({ // ... }) ``` ### Customizing Operation Objects You can also extend the operation object using the `.spec` helper for an `error` or `middleware`: ```ts import { oo } from '@orpc/openapi' const base = os.errors({ UNAUTHORIZED: oo.spec({ data: z.any(), }, { security: [{ 'api-key': [] }], }) }) // OR in middleware const requireAuth = oo.spec( os.middleware(async ({ next, errors }) => { throw new ORPCError('UNAUTHORIZED') return next() }), { security: [{ 'api-key': [] }] } ) ``` Any [procedure](/docs/procedure) that includes the use above `errors` or `middleware` will automatically have the defined `security` property applied :::info The `.spec` helper accepts a callback as its second argument, allowing you to override the entire operation object. ::: ## `@orpc/zod` ### File Schema In the [File Upload/Download](/docs/file-upload-download) guide, `z.instanceof` is used to describe file/blob schemas. However, this method prevents oRPC from recognizing file/blob schema. Instead, use the enhanced file schema approach: ```ts twoslash import { z } from 'zod' import { oz } from '@orpc/zod' const InputSchema = z.object({ file: oz.file(), image: oz.file().type('image/*'), blob: oz.blob() }) ``` ### JSON Schema Customization If Zod alone does not cover your JSON Schema requirements, you can extend or override the generated schema: ```ts twoslash import { z } from 'zod' import { oz } from '@orpc/zod' const InputSchema = oz.openapi( z.object({ name: z.string(), }), { examples: [ { name: 'Earth' }, { name: 'Mars' }, ], // additional options... } ) ``` --- --- url: /docs/openapi/client/openapi-link.md description: Details on using OpenAPILink in oRPC clients. --- # OpenAPILink OpenAPILink enables communication with an [OpenAPIHandler](/docs/openapi/openapi-handler) or any API that follows the [OpenAPI Specification](https://swagger.io/specification/) using HTTP/Fetch. ## Installation ::: code-group ```sh [npm] npm install @orpc/openapi-client@latest ``` ```sh [yarn] yarn add @orpc/openapi-client@latest ``` ```sh [pnpm] pnpm add @orpc/openapi-client@latest ``` ```sh [bun] bun add @orpc/openapi-client@latest ``` ```sh [deno] deno install npm:@orpc/openapi-client@latest ``` ::: ## Setup To use `OpenAPILink`, ensure you have a [contract router](/docs/contract-first/define-contract#contract-router) and that your server is set up with [OpenAPIHandler](/docs/openapi/openapi-handler) or any API that follows the [OpenAPI Specification](https://swagger.io/specification/). ::: info A normal [router](/docs/router) works as a contract router as long as it does not include a [lazy router](/docs/router#lazy-router). You can also unlazy a router using the [unlazyRouter](/docs/advanced/mocking#using-the-implementer) utility. ::: ```ts twoslash import { contract } from './shared/planet' // ---cut--- import type { JsonifiedClient } from '@orpc/openapi-client' import type { ContractRouterClient } from '@orpc/contract' import { createORPCClient } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi-client/fetch' const link = new OpenAPILink(contract, { url: 'http://localhost:3000/api', headers: () => ({ 'x-api-key': 'my-api-key', }), // fetch: <-- polyfill fetch if needed }) const client: JsonifiedClient> = createORPCClient(link) ``` :::warning Wrap your client with `JsonifiedClient` to ensure it accurately reflects the server responses. ::: ## Limitations Unlike [RPCLink](/docs/client/rpc-link), `OpenAPILink` has some constraints: * Payloads containing a `Blob` or `File` (outside the root level) must use `multipart/form-data` and serialized using [Bracket Notation](/docs/openapi/bracket-notation). * For `GET` requests, the payload must be sent as `URLSearchParams` and serialized using [Bracket Notation](/docs/openapi/bracket-notation). :::warning In these cases, both the request and response are subject to the limitations of [Bracket Notation Limitations](/docs/openapi/bracket-notation#limitations). Additionally, oRPC converts data to strings (exclude `null` and `undefined` will not be represented). ::: ## CORS policy `OpenAPILink` requires access to the `Content-Disposition` to distinguish file responses from other responses whe file has a common MIME type like `application/json`, `plain/text`, etc. To enable this, include `Content-Disposition` in your CORS policy's `Access-Control-Expose-Headers`: ```ts const handler = new OpenAPIHandler(router, { plugins: [ new CORSPlugin({ exposeHeaders: ['Content-Disposition'], }), ], }) ``` ## Using Client Context Client context lets you pass extra information when calling procedures and dynamically modify OpenAPILink's behavior. ```ts twoslash import { contract } from './shared/planet' // ---cut--- import type { JsonifiedClient } from '@orpc/openapi-client' import type { ContractRouterClient } from '@orpc/contract' import { createORPCClient } from '@orpc/client' import { OpenAPILink } from '@orpc/openapi-client/fetch' interface ClientContext { something?: string } const link = new OpenAPILink(contract, { url: 'http://localhost:3000/api', headers: async ({ context }) => ({ 'x-api-key': context?.something ?? '' }) }) const client: JsonifiedClient> = createORPCClient(link) const result = await client.planet.list( { limit: 10 }, { context: { something: 'value' } } ) ``` :::info If a property in `ClientContext` is required, oRPC enforces its inclusion when calling procedures. ::: ## Lazy URL You can define `url` as a function, ensuring compatibility with environments that may lack certain runtime APIs. ```ts const link = new OpenAPILink({ url: () => { if (typeof window === 'undefined') { throw new Error('OpenAPILink is not allowed on the server side.') } return new URL('/api', window.location.href) }, }) ``` ## SSE Like Behavior Unlike traditional SSE, the [Event Iterator](/docs/event-iterator) does not automatically retry on error. To enable automatic retries, refer to the [Client Retry Plugin](/docs/plugins/client-retry). ## Event Iterator Keep Alive :::warning These options for sending [Event Iterator](/docs/event-iterator) from **client to the server**, not from **the server to client** as used in [RPCHandler Event Iterator Keep Alive](/docs/rpc-handler#event-iterator-keep-alive) or [OpenAPIHandler Event Iterator Keep Alive](/docs/openapi/openapi-handler#event-iterator-keep-alive). **In 99% of cases, you don't need to configure these options.** ::: To keep [Event Iterator](/docs/event-iterator) connections alive, `OpenAPILink` periodically sends a ping comment to the server. You can configure this behavior using the following options: * `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings. * `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds). * `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping messages. ```ts const link = new OpenAPILink({ eventIteratorKeepAliveEnabled: true, eventIteratorKeepAliveInterval: 5000, // 5 seconds eventIteratorKeepAliveComment: '', }) ``` --- --- url: /docs/best-practices/optimize-ssr.md description: >- Optimize SSR performance in Next.js, SvelteKit, and other frameworks by using oRPC to make direct server-side API calls, avoiding unnecessary network requests. --- # Optimize Server-Side Rendering (SSR) for Fullstack Frameworks This guide demonstrates an optimized approach for setting up Server-Side Rendering (SSR) with oRPC in fullstack frameworks like Next.js, Nuxt, and SvelteKit. This method enhances performance by eliminating redundant network calls during the server rendering process. ## The Problem with Standard SSR Data Fetching In a typical SSR setup within fullstack frameworks, data fetching often involves the server making an HTTP request back to its own API endpoints. ![Standard SSR: Server calls its own API via HTTP.](/images/standard-ssr-diagram.svg) This pattern works, but it introduces unnecessary overhead: the server needs to make an HTTP request to itself to fetch the data, which can add extra latency and consume resources. Ideally, during SSR, the server should fetch data by directly invoking the relevant API logic within the same process. ![Optimized SSR: Server calls API logic directly.](/images/optimized-ssr-diagram.svg) Fortunately, oRPC provides both a [server-side client](/docs/client/server-side) and [client-side client](/docs/client/client-side), so you can leverage the former during SSR and automatically fall back to the latter in the browser. ## Conceptual approach ```ts // Use this for server-side calls const orpc = createRouterClient(router) // Fallback to this for client-side calls const orpc: RouterClient = createORPCClient(someLink) ``` But how? A naive `typeof window === 'undefined'` check works, but exposes your router logic to the client. We need a hack that ensures server‑only code never reaches the browser. ## Implementation We’ll use `globalThis` to share the server client without bundling it into client code. ::: code-group ```ts [lib/orpc.ts] import type { RouterClient } from '@orpc/server' import { RPCLink } from '@orpc/client/fetch' import { createORPCClient } from '@orpc/client' declare global { var $client: RouterClient | undefined } const link = new RPCLink({ url: () => { if (typeof window === 'undefined') { throw new Error('RPCLink is not allowed on the server side.') } return new URL('/rpc', window.location.href) }, }) /** * Fallback to client-side client if server-side client is not available. */ export const client: RouterClient = globalThis.$client ?? createORPCClient(link) ``` ```ts [lib/orpc.server.ts] 'server only' import { createRouterClient } from '@orpc/server' 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(), }), }) ``` ::: Finally, import `lib/orpc.server.ts` before anything else and on the **server only**. For example, in Next.js add it to `app/layout.tsx`: ```ts import '@/lib/orpc.server' // Rest of the code ``` Now, importing `client` from `lib/orpc.ts` gives you a server-side client during SSR and a client-side client on the client without leaking your router logic. ## Using the client The `client` requires no special handling, just use it like regular clients. ```tsx export default async function PlanetListPage() { const planets = await client.planet.list({ limit: 10 }) return (
{planets.map(planet => (
{planet.name}
))}
) } ``` ::: info This example uses Next.js, but you can apply the same pattern in SvelteKit, Nuxt, or any framework. ::: ## TanStack Query Combining this oRPC setup with TanStack Query (React Query, Solid Query, etc.) provides a powerful pattern for data fetching, and state management, especially with Suspense hooks. Refer to these details in [Tanstack Query Integration Guide](/docs/tanstack-query/basic) and [Tanstack Query SSR Guide](https://tanstack.com/query/latest/docs/framework/react/guides/ssr). ```tsx export default function PlanetListPage() { const { data: planets } = useSuspenseQuery( orpc.planet.list.queryOptions({ input: { limit: 10 }, }), ) return (
{planets.map(planet => (
{planet.name}
))}
) } ``` :::warning Above example uses suspense hooks, you might need to wrap your app within `` (or corresponding APIs) to make it work. In Next.js, maybe you need create `loading.tsx`. ::: --- --- url: /index.md description: >- Easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards --- --- --- url: /blog/v1-announcement.md description: >- oRPC v1 is now available - tRPC, ts-rest, next-safe-action, and more alternatives! --- # oRPC v1 Announcement - Typesafe APIs Made Simple 🪄 oRPC is a thing help you build type-safe APIs with TypeScript. It's has own goal but can fairly be compared to other libraries like tRPC, ts-rest, next-safe-action, etc. or even serve as an alternative to them. ## My Story My oRPC journey started in early 2024 after I lost my job. Finding a new one was hard, and I realized a standard job wasn't really what I wanted. I had always dreamed of being an "indie hacker" – someone who makes useful things for others. But looking back at my past work, I noticed something: I often spent more time complaining about the tools I used than focusing on what users needed. Maybe I cared too much about the tools themselves. Because I was often frustrated with existing tools, I changed my plan. I thought, "What if I make a tool for developers, one that fixes the problems I always had?" I hoped that if I built a tool I liked, other developers would like it too. That's how oRPC started. I began working hard on it around September 17, 2024. It wasn't easy; I had to rebuild oRPC three times to get the base right. After about three months, I shared an early version on Reddit ([see post](https://www.reddit.com/r/nextjs/comments/1h13upv/new_introducing_orpc_a_dropin_replacement_for/)). At first, oRPC was just a side project. Then, a turning point came when someone privately offered **$100** to support it. I was surprised and really motivated! A month after that, I decided to stop my other projects and work on oRPC full-time, even though I didn't have another job. My life became: code, eat, sleep, repeat. I had so many ideas for oRPC. I realized it would take all my focus and time, probably until the end of 2025, to make it happen. But !!! Today is a **big step** on that journey. I'm happy and proud to announce that the core of oRPC is now stable, and Version 1.0 is officially out! ::: info V1 means the public API is stable and ready for production use. ::: ## The Idea behind oRPC oRPC philosophy is **powerful simplicity**. Define your API endpoints almost as easily as writing standard functions, yet automatically gain: * End-to-end type safety (includes **errors**) * Server Action compatibility * Full OpenAPI specification generation * Contract-first workflow support * Standard function call compatibility ```ts const getting = os .use(dbProvider) .use(requiredAuth) .use(rateLimit) .use(analytics) .use(sentryMonitoring) .use(retry({ times: 3 })) .route({ method: 'GET', path: '/getting/{id}' }) .input(z.object({ id: z.string() })) .use(canGetting, input => input.id) .errors({ SOME_TYPE_SAFE_ERROR: { data: z.object({ something: z.string() }) } }) .handler(async ({ input, errors, context }) => { // do something }) .actionable() // server action compatible .callable() // regular function compatible ``` Beyond built-in features, oRPC [metadata](https://orpc.unnoq.com/docs/metadata) system allows for community-driven extensions and future possibilities. ## Highlights * **🔗 End-to-End Type Safety**: Ensure type-safe inputs, outputs, and errors from client to server. * **📘 First-Class OpenAPI**: Built-in support that fully adheres to the OpenAPI standard. * **📝 Contract-First Development**: Optionally define your API contract before implementation. * **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte), Pinia Colada, and more. * **🚀 Server Actions**: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms. * **🔠 Standard Schema Support**: Works out of the box with Zod, Valibot, ArkType, and other schema validators. * **🗃️ Native Types**: Supports native types like Date, File, Blob, BigInt, URL, and more. * **⏱️ Lazy Router**: Enhance cold start times with our lazy routing feature. * **📡 SSE & Streaming**: Enjoy full type-safe support for SSE and streaming. * **🌍 Multi-Runtime Support**: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond. * **🔌 Extendability**: Easily extend functionality with plugins, middleware, and interceptors. * **🛡️ Reliability**: Well-tested, TypeScript-based, production-ready, and MIT licensed. ## tRPC alternative I used tRPC extensively and really liked it. However, I needed OpenAPI support for my projects. Although I found `trpc-openapi` to add OpenAPI to tRPC, it didn't work with Edge runtimes and has since been deprecated. This was very frustrating, prompting me to look for alternatives. Also, setting up tRPC sometimes felt too complicated, especially for smaller projects like Cloudflare Durable Objects where I just needed a simple API. Another point is that tRPC mostly supports React Query. That was okay for me, but less helpful if you want to use Vue, Solid, or Svelte. I did some **simple** benchmarks between oRPC and tRPC, and results show (full report [here](https://github.com/unnoq/orpc-benchmarks)): * oRPC is **1,6 times typecheck faster** (5.9s vs 9.3s) * oRPC is **2,8 times runtime faster** (295k reqs vs 104k reqs / 20 sec) * oRPC is **1,26 times less max cpu usage** (102% vs 129%) * oRPC is **2,6 times less max ram usage** (103MB vs 268MB) * oRPC is **2 times smaller in bundle size** ([32.3 kB](https://bundlejs.com/?q=%40orpc%2Fclient%2C%40orpc%2Fclient%2Ffetch%2C%40orpc%2Fserver%2C%40orpc%2Fserver%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) vs [65.5 kB](https://bundlejs.com/?q=%40trpc%2Fclient%2C%40trpc%2Fserver%2C%40trpc%2Fserver%2Fadapters%2Fstandalone%2Csuperjson\&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)) :::warning Benchmark results can vary across environments and depend heavily on factors like your project's size, complexity, and setup. Many conditions can influence the outcome — so treat these numbers as a helpful reference, not a guarantee. ::: ::: info You can read more about comparion [here](/docs/comparison) ::: ## ts-rest alternative After running into the OpenAPI issues with tRPC, I tried ts-rest. While it helped with OpenAPI, I soon found it was missing features I relied on from tRPC, like flexible middleware and easy handling of certain data types (like Dates or Files). After using it for some APIs, I felt it wasn't the complete solution I wanted. This frustration was a key reason I started building oRPC. ::: info You can read more about comparion [here](/docs/comparison) ::: ## next-safe-action alternative I also experimented with `next-safe-action` to test server actions in Next.js, hoping they might be a good replacement for the tRPC style. However, I found they didn't quite fit my needs. I believe a dedicated RPC library like oRPC still provides a better developer experience for building APIs. ## Sponsors In this long journey, I specially thank all my sponsors, they help me to keep going. * [Zuplo - Serverless API Gateway, designed for developers](https://zuplo.link/orpc) * [村上さん](https://github.com/SanMurakami) * [あわわわとーにゅ](https://github.com/u1-liquid) * [motopods](https://github.com/motopods) * [Maxie](https://github.com/MrMaxie) * [Stijn Timmer](https://github.com/Stijn-Timmer) * [Robbe95](https://github.com/Robbe95) * And my first sponsor (private) to start my story If you're interesting in sponsoring oRPC, you can do it [here](https://github.com/sponsors/unnoq). --- --- url: /docs/pinia-colada.md description: Seamlessly integrate oRPC with Pinia Colada --- # Pinia Colada Integration [Pinia Colada](https://pinia-colada.esm.dev/) is the data fetching layer for Pinia and Vue. oRPC’s integration with Pinia Colada is lightweight and straightforward - there’s no extra overhead. ::: warning This documentation assumes you are already familiar with [Pinia Colada](https://pinia-colada.esm.dev/). If you need a refresher, please review the official Pinia Colada documentation before proceeding. ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/vue-colada@latest @pinia/colada@latest ``` ```sh [yarn] yarn add @orpc/vue-colada@latest @pinia/colada@latest ``` ```sh [pnpm] pnpm add @orpc/vue-colada@latest @pinia/colada@latest ``` ```sh [bun] bun add @orpc/vue-colada@latest @pinia/colada@latest ``` ```sh [deno] deno install npm:@orpc/vue-colada@latest npm:@pinia/colada@latest ``` ::: ## Setup Before you begin, ensure you have already configured a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' declare const client: RouterClient // ---cut--- import { createORPCVueColadaUtils } from '@orpc/vue-colada' export const orpc = createORPCVueColadaUtils(client) orpc.planet.find.queryOptions({ input: { id: 123 } }) // ^| // ``` ## Avoiding Query/Mutation Key Conflicts Prevent key conflicts by passing a unique base key when creating your utils: ```ts const userORPC = createORPCVueColadaUtils(userClient, { path: ['user'] }) const postORPC = createORPCVueColadaUtils(postClient, { path: ['post'] }) ``` ## Query Options Utility Use `.queryOptions` to configure queries. Use it with hooks like `useQuery`, `useSuspenseQuery`, or `prefetchQuery`. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { RouterUtils } from '@orpc/vue-colada' import { useQuery } from '@pinia/colada' declare const orpc: RouterUtils> // ---cut--- const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 }, // Specify input if needed context: { cache: true }, // Provide client context if needed // additional options... })) ``` ## Mutation Options Use `.mutationOptions` to create options for mutations. Use it with hooks like `useMutation`. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { RouterUtils } from '@orpc/vue-colada' import { useMutation } from '@pinia/colada' declare const orpc: RouterUtils> // ---cut--- const mutation = useMutation(orpc.planet.create.mutationOptions({ context: { cache: true }, // Provide client context if needed // additional options... })) mutation.mutate({ name: 'Earth' }) ``` ## Query/Mutation Key Use `.key` to generate a `QueryKey` or `MutationKey`. This is useful for tasks such as revalidating queries, checking mutation status, etc. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { RouterUtils } from '@orpc/vue-colada' import { useQueryCache } from '@pinia/colada' declare const orpc: RouterUtils> // ---cut--- const queryCache = useQueryCache() // Invalidate all planet queries queryCache.invalidateQueries({ key: orpc.planet.key(), }) // Invalidate the planet find query with id 123 queryCache.invalidateQueries({ key: orpc.planet.find.key({ input: { id: 123 } }) }) ``` ## Calling Procedure Clients Use `.call` to call a procedure client directly. It's an alias for corresponding procedure client. ```ts const result = orpc.planet.find.call({ id: 123 }) ``` ## Error Handling Easily manage type-safe errors using our built-in `isDefinedError` helper. ```ts import { isDefinedError } from '@orpc/client' const mutation = useMutation(orpc.planet.create.mutationOptions({ onError: (error) => { if (isDefinedError(error)) { // Handle the error here } }, })) mutation.mutate({ name: 'Earth' }) if (mutation.error.value && isDefinedError(mutation.error.value)) { // Handle the error here } ``` For more details, see our [type-safe error handling guide](/docs/error-handling#type‐safe-error-handling). --- --- url: /docs/playgrounds.md description: >- Interactive development environments for exploring and testing oRPC functionality. --- # Playgrounds Explore oRPC implementations through our interactive playgrounds, featuring pre-configured examples accessible instantly via StackBlitz or local setup. ## Available Playgrounds | Environment | StackBlitz | GitHub Source | | ------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | Next.js Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/nextjs) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/nextjs) | | Nuxt.js Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/nuxt) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/nuxt) | | Solid Start Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/solid-start) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/solid-start) | | Svelte Kit Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/svelte-kit) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/svelte-kit) | | Contract-First Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/contract-first) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/contract-first) | :::warning StackBlitz has own limitations, so some features may not work as expected. ::: ## Local Development If you prefer working locally, you can clone any playground using the following commands: ```bash npx degit unnoq/orpc/playgrounds/nextjs orpc-nextjs-playground npx degit unnoq/orpc/playgrounds/nuxt orpc-nuxt-playground npx degit unnoq/orpc/playgrounds/solid-start orpc-solid-start-playground npx degit unnoq/orpc/playgrounds/svelte-kit orpc-svelte-kit-playground npx degit unnoq/orpc/playgrounds/contract-first orpc-contract-first-playground ``` For each project, set up the development environment: ```bash # Install dependencies npm install # Start the development server npm run dev ``` That's it! You can now access the playground at `http://localhost:3000`. --- --- url: /docs/procedure.md description: Understanding procedures in oRPC --- # Procedure in oRPC In oRPC, a procedure like a standard function but comes with built-in support for: * Input/output validation * Middleware * Dependency injection * Other extensibility features ## Overview Here’s an example of defining a procedure in oRPC: ```ts import { os } from '@orpc/server' const example = os .use(aMiddleware) // Apply middleware .input(z.object({ name: z.string() })) // Define input validation .use(aMiddlewareWithInput, input => input.name) // Use middleware with typed input .output(z.object({ id: z.number() })) // Define output validation .handler(async ({ input, context }) => { // Define execution logic return { id: 1 } }) .callable() // Make the procedure callable like a regular function .actionable() // Server Action compatibility ``` :::info The `.handler` method is the only required step. All other chains are optional. ::: ## Input/Output Validation oRPC supports [Zod](https://github.com/colinhacks/zod), [Valibot](https://github.com/fabian-hiller/valibot), [Arktype](https://github.com/arktypeio/arktype), and any other [Standard Schema](https://github.com/standard-schema/standard-schema?tab=readme-ov-file#what-schema-libraries-implement-the-spec) library for input and output validation. ::: tip By explicitly specifying the `.output` or your `handler's return type`, you enable TypeScript to infer the output without parsing the handler's code. This approach can dramatically enhance both type-checking and IDE-suggestion speed. ::: ### `type` Utility For simple use-case without external libraries, use oRPC’s built-in `type` utility. It takes a mapping function as its first argument: ```ts twoslash import { os, type } from '@orpc/server' const example = os .input(type<{ value: number }>()) .output(type<{ value: number }, number>(({ value }) => value)) .handler(async ({ input }) => input) ``` ## Using Middleware The `.use` method allows you to pass [middleware](/docs/middleware), which must call `next` to continue execution. ```ts const aMiddleware = os.middleware(async ({ context, next }) => next()) const example = os .use(aMiddleware) // Apply middleware .use(async ({ context, next }) => next()) // Inline middleware .handler(async ({ context }) => { /* logic */ }) ``` ::: info [Middleware](/docs/middleware) can be applied if the [current context](/docs/context#combining-initial-and-execution-context) meets the [middleware dependent context](/docs/middleware#dependent-context) requirements and does not conflict with the [current context](/docs/context#combining-initial-and-execution-context). ::: ## Initial Configuration Customize the initial input schema using `.$input`: ```ts const base = os.$input(z.void()) const base = os.$input>() ``` Unlike `.input`, the `.$input` method lets you redefine the input schema after its initial configuration. This is useful when you need to enforce a `void` input when no `.input` is specified. ## Reusability Each modification to a builder creates a completely new instance, avoiding reference issues. This makes it easy to reuse and extend procedures efficiently. ```ts const pub = os.use(logMiddleware) // Base setup for procedures that publish const authed = pub.use(authMiddleware) // Extends 'pub' with authentication const pubExample = pub.handler(async ({ context }) => { /* logic */ }) const authedExample = pubExample.use(authMiddleware) ``` This pattern helps prevent duplication while maintaining flexibility. --- --- url: /docs/integrations/react-native.md description: Seamlessly integrate oRPC with React Native --- # React Native Integration [React Native](https://reactnative.dev/) is a framework for building native apps using React. ## Fetch Link React Native includes a [Fetch API](https://reactnative.dev/docs/network), so you can use oRPC out of the box. ::: warning However, the Fetch API in React Native has limitations. oRPC features like `File`, `Blob`, and `AsyncIteratorObject` aren't supported. Follow [Support Stream #27741](https://github.com/facebook/react-native/issues/27741) for updates. ::: ::: tip If you're using `RPCHandler/Link`, you can temporarily add support for `File` and `Blob` by extending the [RPC JSON Serializer](/docs/advanced/rpc-json-serializer#extending-native-data-types) to encode these types as Base64. ::: ```ts import { RPCLink } from '@orpc/client/fetch' const link = new RPCLink({ url: 'http://localhost:3000/rpc', headers: async ({ context }) => ({ 'x-api-key': context?.something ?? '' }) // fetch: <-- polyfill fetch if needed }) ``` ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/client/rpc-link), [OpenAPILink](/docs/openapi/client/openapi-link), or another custom link. ::: --- --- url: /docs/openapi/advanced/redirect-response.md description: Standard HTTP redirect response in oRPC OpenAPI. --- # Redirect Response Easily return a standard HTTP redirect response in oRPC OpenAPI. ## Basic Usage By combining the `successStatus` and `outputStructure` options, you can return a standard HTTP redirect response. ```ts const redirect = os .route({ method: 'GET', path: '/redirect', successStatus: 307, // [!code highlight] outputStructure: 'detailed' // [!code highlight] }) .handler(async () => { return { headers: { location: 'https://orpc.unnoq.com', // [!code highlight] }, } }) ``` ## Limitations When invoking a redirect procedure with [OpenAPILink](/docs/openapi/client/openapi-link), oRPC treats the redirect as a normal response rather than following it. Some environments, such as browsers, may restrict access to the redirect response, **potentially causing errors**. In contrast, server environments like Node.js handle this without issue. --- --- url: /docs/integrations/remix.md description: Integrate oRPC with Remix --- # Remix Integration [Remix](https://remix.run/) is a full stack JavaScript framework for building web applications with React. For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ```ts [app/routes/rpc.$.ts] import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(router) export async function loader({ request }: LoaderFunctionArgs) { const { response } = await handler.handle(request, { prefix: '/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/openapi-handler), or another custom handler. ::: --- --- url: /docs/plugins/response-headers.md description: Response Headers Plugin for oRPC --- # Response Headers Plugin The Response Headers Plugin allows you to set response headers in oRPC. It injects a `resHeaders` instance into the `context`, enabling you to modify response headers easily. ## Context Setup ```ts twoslash import { os } from '@orpc/server' // ---cut--- import { ResponseHeadersPluginContext } from '@orpc/server/plugins' interface ORPCContext extends ResponseHeadersPluginContext {} const base = os.$context() const example = base .use(({ context, next }) => { context.resHeaders?.set('x-custom-header', 'value') return next() }) .handler(({ context }) => { context.resHeaders?.set('x-custom-header', 'value') }) ``` ::: info **Why can `resHeaders` be `undefined`?** This allows procedures to run safely even when `ResponseHeadersPlugin` is not used, such as in direct calls. ::: ## Handler Setup ```ts import { ResponseHeadersPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new ResponseHeadersPlugin() ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/router.md description: Understanding routers in oRPC --- # Router in oRPC Routers in oRPC are simple, nestable objects composed of procedures. They can also modify their own procedures, offering flexibility and modularity when designing your API. ## Overview Routers are defined as plain JavaScript objects where each key corresponds to a procedure. For example: ```ts import { os } from '@orpc/server' const ping = os.handler(async () => 'ping') const pong = os.handler(async () => 'pong') const router = { ping, pong, nested: { ping, pong } } ``` ## Extending Router Routers can be modified to include additional features. For example, to require authentication on all procedures: ```ts const router = os.use(requiredAuth).router({ ping, pong, nested: { ping, pong, } }) ``` ::: warning If you apply middleware using `.use` at both the router and procedure levels, it may execute multiple times. This duplication can lead to performance issues. For guidance on avoiding redundant middleware execution, please see our [best practices for middleware deduplication](/docs/best-practices/dedupe-middleware). ::: ## Lazy Router In oRPC, routers can be lazy-loaded, making them ideal for code splitting and enhancing cold start performance. Lazy loading allows you to defer the initialization of routes until they are actually needed, which reduces the initial load time and improves resource management. ::: code-group ```ts [router.ts] const router = { ping, pong, planet: os.lazy(() => import('./planet')) } ``` ```ts [planet.ts] const PlanetSchema = z.object({ id: z.number().int().min(1), name: z.string(), description: z.string().optional(), }) export const listPlanet = os .input( z.object({ limit: z.number().int().min(1).max(100).optional(), cursor: z.number().int().min(0).default(0), }), ) .handler(async ({ input }) => { // your list code here return [{ id: 1, name: 'name' }] }) export default { list: listPlanet, // ... } ``` ::: ## Utilities ::: info Every [procedure](/docs/procedure) is also a router, so you can apply these utilities to procedures as well. ::: ### Infer Router Inputs ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterInputs } from '@orpc/server' export type Inputs = InferRouterInputs type FindPlanetInput = Inputs['planet']['find'] ``` Infers the expected input types for each procedure in the router. ### Infer Router Outputs ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterOutputs } from '@orpc/server' export type Outputs = InferRouterOutputs type FindPlanetOutput = Outputs['planet']['find'] ``` Infers the expected output types for each procedure in the router. ### Infer Router Initial Contexts ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterInitialContexts } from '@orpc/server' export type InitialContexts = InferRouterInitialContexts type FindPlanetInitialContext = InitialContexts['planet']['find'] ``` Infers the [initial context](/docs/context#initial-context) types defined for each procedure. ### Infer Router Current Contexts ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterCurrentContexts } from '@orpc/server' export type CurrentContexts = InferRouterCurrentContexts type FindPlanetCurrentContext = CurrentContexts['planet']['find'] ``` Infers the [current context](/docs/context#combining-initial-and-execution-context) types, which combine the initial context with the execution context and pass it to the handler. --- --- url: /docs/rpc-handler.md description: Comprehensive Guide to the RPCHandler in oRPC --- # RPC Handler The `RPCHandler` enables communication with clients over oRPC's proprietary [RPC protocol](/docs/advanced/rpc-protocol), built on top of HTTP. While it efficiently transfers native types, the protocol is neither human-readable nor OpenAPI-compatible. For OpenAPI support, use the [OpenAPIHandler](/docs/openapi/openapi-handler). :::warning `RPCHandler` is designed exclusively for [RPCLink](/docs/client/rpc-link) and **does not** support OpenAPI. Avoid sending requests to it manually. ::: ## Supported Data Types `RPCHandler` natively serializes and deserializes the following JavaScript types: * **string** * **number** (including `NaN`) * **boolean** * **null** * **undefined** * **Date** (including `Invalid Date`) * **BigInt** * **RegExp** * **URL** * **Record (object)** * **Array** * **Set** * **Map** * **Blob** (unsupported in `AsyncIteratorObject`) * **File** (unsupported in `AsyncIteratorObject`) * **AsyncIteratorObject** (only at the root level; powers the [Event Iterator](/docs/event-iterator)) :::tip You can extend the list of supported types by [creating a custom serializer](/docs/advanced/rpc-json-serializer#extending-native-data-types). ::: ## Setup and Integration ```ts import { RPCHandler } from '@orpc/server/fetch' // or '@orpc/server/node' import { CORSPlugin } from '@orpc/server/plugins' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { plugins: [ new CORSPlugin() ], interceptors: [ onError((error) => { console.error(error) }) ], }) export default async function fetch(request: Request) { const { matched, response } = await handler.handle(request, { prefix: '/rpc', context: {} // Provide initial context if required }) if (matched) { return response } return new Response('Not Found', { status: 404 }) } ``` ## Event Iterator Keep Alive To keep [Event Iterator](/docs/event-iterator) connections alive, `RPCHandler` periodically sends a ping comment to the client. You can configure this behavior using the following options: * `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings. * `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds). * `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping comments. ```ts const handler = new RPCHandler(router, { eventIteratorKeepAliveEnabled: true, eventIteratorKeepAliveInterval: 5000, // 5 seconds eventIteratorKeepAliveComment: '', }) ``` ## Default Plugins RPCHandler is pre-configured with plugins that help enforce best practices and enhance security out of the box. By default, the following plugin is enabled: * [StrictGetMethodPlugin](/docs/plugins/strict-get-method) - Disable by setting `strictGetMethodPluginEnabled` to `false`. --- --- url: /docs/advanced/rpc-json-serializer.md description: Extend or override the standard RPC JSON serializer. --- # RPC JSON Serializer This serializer handles JSON payloads for the [RPC Protocol](/docs/advanced/rpc-protocol) and supports [native data types](/docs/rpc-handler#supported-data-types). ## Extending Native Data Types Extend native types by creating your own `StandardRPCCustomJsonSerializer` and adding it to the `customJsonSerializers` option. 1. **Define Your Custom Serializer** ```ts twoslash import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard' export class User { constructor( public readonly id: string, public readonly name: string, public readonly email: string, public readonly age: number, ) {} toJSON() { return { id: this.id, name: this.name, email: this.email, age: this.age, } } } export const userSerializer: StandardRPCCustomJsonSerializer = { type: 21, condition: data => data instanceof User, serialize: data => data.toJSON(), deserialize: data => new User(data.id, data.name, data.email, data.age), } ``` ::: warning Ensure the `type` is unique and greater than `20` to avoid conflicts with [built-in types](/docs/advanced/rpc-protocol#supported-types) in the future. ::: 2. **Use Your Custom Serializer** ```ts twoslash import type { StandardRPCCustomJsonSerializer } from '@orpc/client/standard' import { RPCHandler } from '@orpc/server/fetch' import { RPCLink } from '@orpc/client/fetch' declare const router: Record declare const userSerializer: StandardRPCCustomJsonSerializer // ---cut--- const handler = new RPCHandler(router, { customJsonSerializers: [userSerializer], // [!code highlight] }) const link = new RPCLink({ url: 'https://example.com/rpc', customJsonSerializers: [userSerializer], // [!code highlight] }) ``` ## Overriding Built-in Types You can override built-in types by matching their `type` with the [built-in types](/docs/advanced/rpc-protocol#supported-types). For example, oRPC represents `undefined` only in array items and ignores it in objects. To override this behavior: ```ts twoslash import { StandardRPCCustomJsonSerializer } from '@orpc/client/standard' export const undefinedSerializer: StandardRPCCustomJsonSerializer = { type: 3, // Match the built-in undefined type. [!code highlight] condition: data => data === undefined, serialize: data => null, // JSON cannot represent undefined, so use null. deserialize: data => undefined, } ``` --- --- url: /docs/advanced/rpc-protocol.md description: Learn about the RPC protocol used by RPCHandler. --- # RPC Protocol The RPC protocol enables remote procedure calls over HTTP using JSON, supporting native data types. It is used by [RPCHandler](/docs/rpc-handler). ## Routing The procedure to call is determined by the `pathname`. ```bash curl https://example.com/rpc/planet/create ``` This example calls the `planet.create` procedure, with `/rpc` as the prefix. ```ts const router = { planet: { create: os.handler(() => {}) // [!code highlight] } } ``` ## Input Any HTTP method can be used. Input can be provided via URL query parameters or the request body. :::info You can use any method, but by default, [RPCHandler](/docs/rpc-handler) enabled [StrictGetMethodPlugin](/docs/rpc-handler#default-plugins) which blocks GET requests except for procedures explicitly allowed. ::: ### Input in URL Query ```ts const url = new URL('https://example.com/rpc/planet/create') url.searchParams.append('data', JSON.stringify({ json: { name: 'Earth', detached_at: '2022-01-01T00:00:00.000Z' }, meta: [[1, 'detached_at']] })) const response = await fetch(url) ``` ### Input in Request Body ```bash curl -X POST https://example.com/rpc/planet/create \ -H 'Content-Type: application/json' \ -d '{ "json": { "name": "Earth", "detached_at": "2022-01-01T00:00:00.000Z" }, "meta": [[1, "detached_at"]] }' ``` ### Input with File ```ts const form = new FormData() form.set('data', JSON.stringify({ json: { name: 'Earth', thumbnail: {}, images: [{}, {}] }, meta: [[1, 'detached_at']], maps: [['images', 0], ['images', 1]] })) form.set('0', new Blob([''], { type: 'image/png' })) form.set('1', new Blob([''], { type: 'image/png' })) const response = await fetch('https://example.com/rpc/planet/create', { method: 'POST', body: form }) ``` ## Success Response ```http HTTP/1.1 200 OK Content-Type: application/json { "json": { "id": "1", "name": "Earth", "detached_at": "2022-01-01T00:00:00.000Z" }, "meta": [[0, "id"], [1, "detached_at"]] } ``` A success response has an HTTP status code between `200-299` and returns the procedure's output. ## Error Response ```http HTTP/1.1 500 Internal Server Error Content-Type: application/json { "json": { "defined": false, "code": "INTERNAL_SERVER_ERROR", "status": 500, "message": "Internal server error", "data": {} }, "meta": [] } ``` An error response has an HTTP status code between `400-599` and returns an `ORPCError` object. ## Meta The `meta` field describes native data in the format `[type: number, ...path: (string | number)[]]`. * **type**: Data type (see [Supported Types](#supported-types)). * **path**: Path to the data inside `json`. ### Supported Types | Type | Description | | ---- | ----------- | | 0 | bigint | | 1 | date | | 2 | nan | | 3 | undefined | | 4 | url | | 5 | regexp | | 6 | set | | 7 | map | ## Maps The `maps` field is used with `FormData` to map a file or blob to a specific path in `json`. --- --- url: /docs/client/rpc-link.md description: Details on using RPCLink in oRPC clients. --- # RPCLink RPCLink enables communication with an [RPCHandler](/docs/rpc-handler) or any API that follows the [RPC Protocol](/docs/advanced/rpc-protocol) using HTTP/Fetch. ## Overview Before using RPCLink, make sure your server is set up with [RPCHandler](/docs/rpc-handler) or any API that follows the [RPC Protocol](/docs/advanced/rpc-protocol). ```ts import { RPCLink } from '@orpc/client/fetch' const link = new RPCLink({ url: 'http://localhost:3000/rpc', headers: () => ({ 'x-api-key': 'my-api-key' }), // fetch: <-- polyfill fetch if needed }) export const client: RouterClient = createORPCClient(link) ``` ## Using Client Context Client context lets you pass extra information when calling procedures and dynamically modify RPCLink’s behavior. ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' interface ClientContext { something?: string } const link = new RPCLink({ url: 'http://localhost:3000/rpc', headers: async ({ context }) => ({ 'x-api-key': context?.something ?? '' }) }) const client: RouterClient = createORPCClient(link) const result = await client.planet.list( { limit: 10 }, { context: { something: 'value' } } ) ``` :::info If a property in `ClientContext` is required, oRPC enforces its inclusion when calling procedures. ::: ## Custom Request Method By default, RPCLink sends requests via `POST`. You can override this to use methods like `GET` (for browser or CDN caching) based on your requirements. ::: warning By default, [RPCHandler](/docs/rpc-handler) enabled [StrictGetMethodPlugin](/docs/rpc-handler#default-plugins) which blocks GET requests except for procedures explicitly allowed. please refer to [StrictGetMethodPlugin](/docs/plugins/strict-get-method) for more details. ::: ```ts twoslash import { RPCLink } from '@orpc/client/fetch' interface ClientContext { cache?: RequestCache } const link = new RPCLink({ url: 'http://localhost:3000/rpc', method: ({ context }, path) => { if (context?.cache) { return 'GET' } const lastSegment = path.at(-1) if (lastSegment && /get|find|list|search/i.test(lastSegment)) { return 'GET' } return 'POST' }, fetch: (request, init, { context }) => globalThis.fetch(request, { ...init, cache: context?.cache, }), }) ``` ## Lazy URL You can define `url` as a function, ensuring compatibility with environments that may lack certain runtime APIs. ```ts const link = new RPCLink({ url: () => { if (typeof window === 'undefined') { throw new Error('RPCLink is not allowed on the server side.') } return new URL('/rpc', window.location.href) }, }) ``` ## SSE Like Behavior Unlike traditional SSE, the [Event Iterator](/docs/event-iterator) does not automatically retry on error. To enable automatic retries, refer to the [Client Retry Plugin](/docs/plugins/client-retry). ## Event Iterator Keep Alive :::warning These options for sending [Event Iterator](/docs/event-iterator) from **client to the server**, not from **the server to client** as used in [RPCHandler Event Iterator Keep Alive](/docs/rpc-handler#event-iterator-keep-alive) or [OpenAPIHandler Event Iterator Keep Alive](/docs/openapi/openapi-handler#event-iterator-keep-alive). **In 99% of cases, you don't need to configure these options.** ::: To keep [Event Iterator](/docs/event-iterator) connections alive, `RPCLink` periodically sends a ping comment to the server. You can configure this behavior using the following options: * `eventIteratorKeepAliveEnabled` (default: `true`) – Enables or disables pings. * `eventIteratorKeepAliveInterval` (default: `5000`) – Time between pings (in milliseconds). * `eventIteratorKeepAliveComment` (default: `''`) – Custom content for ping messages. ```ts const link = new RPCLink({ eventIteratorKeepAliveEnabled: true, eventIteratorKeepAliveInterval: 5000, // 5 seconds eventIteratorKeepAliveComment: '', }) ``` --- --- url: /docs/openapi/scalar.md description: Create a beautiful API client for your oRPC effortlessly. --- # Scalar (Swagger) Leverage the [OpenAPI Specification](/docs/openapi/openapi-specification) to generate a stunning API client for your oRPC using [Scalar](https://github.com/scalar/scalar). ::: info This guide covers the basics. For a simpler setup, consider using the [OpenAPI Reference Plugin](/docs/openapi/plugins/openapi-reference), which serves both the API reference UI and the OpenAPI specification. ::: ## Basic Example ```ts import { createServer } from 'node:http' import { OpenAPIGenerator } from '@orpc/openapi' import { OpenAPIHandler } from '@orpc/openapi/node' import { CORSPlugin } from '@orpc/server/plugins' import { ZodSmartCoercionPlugin, ZodToJsonSchemaConverter } from '@orpc/zod' const openAPIHandler = new OpenAPIHandler(router, { plugins: [ new CORSPlugin(), new ZodSmartCoercionPlugin(), ], }) const openAPIGenerator = new OpenAPIGenerator({ schemaConverters: [ new ZodToJsonSchemaConverter(), ], }) const server = createServer(async (req, res) => { const { matched } = await openAPIHandler.handle(req, res, { prefix: '/api', }) if (matched) { return } if (req.url === '/spec.json') { const spec = await openAPIGenerator.generate(router, { info: { title: 'My Playground', version: '1.0.0', }, servers: [ { url: '/api' }, /** Should use absolute URLs in production */ ], security: [{ bearerAuth: [] }], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', }, }, }, }) res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify(spec)) return } const html = ` My Client ` res.writeHead(200, { 'Content-Type': 'text/html' }) res.end(html) }) server.listen(3000, () => { console.log('Playground is available at http://localhost:3000') }) ``` Access the playground at `http://localhost:3000` to view your API client. --- --- url: /docs/server-action.md description: Integrate oRPC procedures with React Server Actions --- # Server Action React [Server Actions](https://react.dev/reference/rsc/server-functions) let client components invoke asynchronous server functions. With oRPC, you simply append the `.actionable` modifier to enable Server Action compatibility. ## Server Side Define your procedure with `.actionable` for Server Action support. ```ts twoslash import { onError, onSuccess, os } from '@orpc/server' import { z } from 'zod' // ---cut--- 'use server' import { redirect } from 'next/navigation' export const ping = os .input(z.object({ name: z.string() })) .handler(async ({ input }) => `Hello, ${input.name}`) .actionable({ context: async () => ({}), // Optional: provide initial context if needed interceptors: [ onSuccess(output => redirect(`/some-where`)), onError(error => console.error(error)), ], }) ``` :::tip We recommend using [Runtime Context](/docs/context#execution-context) instead of [Initial Context](/docs/context#initial-context) when working with Server Actions. ::: ## Client Side On the client, import and call your procedure as follows: ```tsx 'use client' import { ping } from './actions' export function MyComponent() { const [name, setName] = useState('') const handleSubmit = async (e: FormEvent) => { e.preventDefault() const [error, data] = await ping({ name }) console.log(error, data) } return (
setName(e.target.value)} />
) } ``` This approach seamlessly integrates server-side procedures with client components via Server Actions. ## Type‑Safe Error Handling The `.actionable` modifier supports type-safe error handling with a JSON-like error object. ```ts twoslash import { os } from '@orpc/server' import { z } from 'zod' export const someAction = os .input(z.object({ name: z.string() })) .errors({ SOME_ERROR: { message: 'Some error message', data: z.object({ some: z.string() }), }, }) .handler(async ({ input }) => `Hello, ${input.name}`) .actionable() // ---cut--- 'use client' const [error, data] = await someAction({ name: 'John' }) if (error) { if (error.defined) { console.log(error.data) // ^ Typed error data } // Handle unknown errors } else { // Handle success console.log(data) } ``` ## `@orpc/react` Package The `@orpc/react` package offers utilities to integrate oRPC with React and React Server Actions. ### Installation ::: code-group ```sh [npm] npm install @orpc/react@latest ``` ```sh [yarn] yarn add @orpc/react@latest ``` ```sh [pnpm] pnpm add @orpc/react@latest ``` ```sh [bun] bun add @orpc/react@latest ``` ```sh [deno] deno install npm:@orpc/react@latest ``` ::: ### `useServerAction` Hook The `useServerAction` hook simplifies invoking server actions in React. ```tsx twoslash import * as React from 'react' import { os } from '@orpc/server' import { z } from 'zod' export const someAction = os .input(z.object({ name: z.string() })) .errors({ SOME_ERROR: { message: 'Some error message', data: z.object({ some: z.string() }), }, }) .handler(async ({ input }) => `Hello, ${input.name}`) .actionable() // ---cut--- 'use client' import { useServerAction } from '@orpc/react/hooks' import { isDefinedError, onError } from '@orpc/client' export function MyComponent() { const { execute, data, error, status } = useServerAction(someAction, { interceptors: [ onError((error) => { if (isDefinedError(error)) { console.error(error.data) // ^ Typed error data } }), ], }) const action = async (form: FormData) => { const name = form.get('name') as string execute({ name }) } return (
{status === 'pending' &&

Loading...

}
) } ``` ### `createFormAction` Utility The `createFormAction` utility accepts a [procedure](/docs/procedure) and returns a function to handle form submissions. It uses [Bracket Notation](/docs/openapi/bracket-notation) to deserialize form data. ```tsx import { createFormAction } from '@orpc/react' const dosomething = os .input( z.object({ user: z.object({ name: z.string(), age: z.coerce.number(), }), }) ) .handler(({ input }) => { console.log('Form action called!') console.log(input) }) export const redirectSomeWhereForm = createFormAction(dosomething, { interceptors: [ onSuccess(async () => { redirect('/some-where') }), ], }) export function MyComponent() { return (
) } ``` By moving the `redirect('/some-where')` logic into `createFormAction` rather than the procedure, you enhance the procedure's reusability beyond Server Actions. ::: info When using `createFormAction`, any `ORPCError` with a status of `401`, `403`, or `404` is automatically converted into the corresponding Next.js error responses: [unauthorized](https://nextjs.org/docs/app/api-reference/functions/unauthorized), [forbidden](https://nextjs.org/docs/app/api-reference/functions/forbidden), and [not found](https://nextjs.org/docs/app/api-reference/functions/not-found). ::: --- --- url: /docs/client/server-side.md description: >- Call your oRPC procedures in the same environment as your server like native functions. --- # Server-Side Clients Call your [procedures](/docs/procedure) in the same environment as your server—no proxies required like native functions. ## Calling Procedures oRPC offers multiple methods to invoke a [procedure](/docs/procedure). ### Using `.callable` Define your procedure and turn it into a callable procedure: ```ts twoslash import { os } from '@orpc/server' import { z } from 'zod' const getProcedure = os .input(z.object({ id: z.string() })) .handler(async ({ input }) => ({ id: input.id })) .callable({ context: {} // Provide initial context if needed }) const result = await getProcedure({ id: '123' }) ``` ### Using the `call` Utility Alternatively, call your procedure using the `call` helper: ```ts twoslash import { z } from 'zod' import { call, os } from '@orpc/server' const getProcedure = os .input(z.object({ id: z.string() })) .handler(async ({ input }) => ({ id: input.id })) const result = await call(getProcedure, { id: '123' }, { context: {} // Provide initial context if needed }) ``` ## Router Client Create a [router](/docs/router) based client to access multiple procedures: ```ts twoslash import { z } from 'zod' // ---cut--- import { createRouterClient, os } from '@orpc/server' const ping = os.handler(() => 'pong') const pong = os.handler(() => 'ping') const client = createRouterClient({ ping, pong }, { context: {} // Provide initial context if needed }) const result = await client.ping() ``` ### Client Context You can define a client context to pass additional information when calling procedures. This is useful for modifying procedure behavior dynamically. ```ts twoslash import { z } from 'zod' import { createRouterClient, os } from '@orpc/server' // ---cut--- interface ClientContext { cache?: boolean } const ping = os.handler(() => 'pong') const pong = os.handler(() => 'ping') const client = createRouterClient({ ping, pong }, { context: ({ cache }: ClientContext) => { // [!code highlight] if (cache) { return {} // <-- context when cache enabled } return {} // <-- context when cache disabled } }) const result = await client.ping(undefined, { context: { cache: true } }) ``` :::info If `ClientContext` contains a required property, oRPC enforces that the client provides it when calling a procedure. ::: --- --- url: /docs/plugins/simple-csrf-protection.md description: >- Add basic Cross-Site Request Forgery (CSRF) protection to your oRPC application. It helps ensure that requests to your procedures originate from JavaScript code, not from other sources like standard HTML forms or direct browser navigation. --- # Simple CSRF Protection Plugin This plugin adds basic [Cross-Site Request Forgery (CSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSRF_prevention) protection to your oRPC application. It helps ensure that requests to your procedures originate from JavaScript code, not from other sources like standard HTML forms or direct browser navigation. ## When to Use This plugin is beneficial if your application stores sensitive data (like session or auth tokens) in Cookie storage using `SameSite=Lax` (the default) or `SameSite=None`. ## Setup This plugin requires configuration on both the server and client sides. ### Server ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { SimpleCsrfProtectionHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new SimpleCsrfProtectionHandlerPlugin() ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or custom implementations. ::: ### Client ```ts twoslash import { RPCLink } from '@orpc/client/fetch' // ---cut--- import { SimpleCsrfProtectionLinkPlugin } from '@orpc/client/plugins' const link = new RPCLink({ url: 'https://api.example.com/rpc', plugins: [ new SimpleCsrfProtectionLinkPlugin(), ], }) ``` ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/client/rpc-link), [OpenAPILink](/docs/openapi/client/openapi-link), or custom implementations. ::: --- --- url: /docs/integrations/solid-start.md description: Integrate oRPC with SolidStart --- # SolidStart Integration [SolidStart](https://start.solidjs.com/) is a full stack JavaScript framework for building web applications with SolidJS. For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ::: code-group ```ts [src/routes/rpc/[...rest].ts] import type { APIEvent } from '@solidjs/start/server' import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(router) 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 GET = handle export const POST = handle export const PUT = handle export const PATCH = handle export const DELETE = handle ``` ```ts [src/routes/rpc/index.ts] export * from './[...rest]' ``` ::: ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler. ::: --- --- url: /docs/plugins/strict-get-method.md description: >- Enhance security by ensuring only procedures explicitly marked to accept `GET` requests can be called using the HTTP `GET` method for RPC Protocol. This helps prevent certain types of Cross-Site Request Forgery (CSRF) attacks. --- # Strict GET Method Plugin This plugin enhances security by ensuring only procedures explicitly marked to accept `GET` requests can be called using the HTTP `GET` method for [RPC Protocol](/docs/advanced/rpc-protocol). This helps prevent certain types of [Cross-Site Request Forgery (CSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSRF_prevention) attacks. ::: info [RPCHandler](/docs/rpc-handler) enabled this plugin by default. ::: ## When to Use This plugin is beneficial if your application stores sensitive data (like session or auth tokens) in Cookie storage using `SameSite=Lax` (the default) or `SameSite=None`. ## How it works The plugin enforces a simple rule: only procedures explicitly configured with `method: 'GET'` can be invoked via a `GET` request. All other procedures will reject `GET` requests. ```ts import { os } from '@orpc/server' const ping = os .route({ method: 'GET' }) // [!code highlight] .handler(() => 'pong') ``` ## Setup ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { StrictGetMethodPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new StrictGetMethodPlugin() ], }) ``` --- --- url: /docs/advanced/superjson.md description: Replace the default oRPC RPC serializer with SuperJson. --- # SuperJson This guide explains how to replace the default oRPC RPC serializer with [SuperJson](https://github.com/blitz-js/superjson). :::info While the default oRPC serializer is faster and more efficient, SuperJson is widely adopted and may be preferred for compatibility. ::: ## SuperJson Serializer :::warning The `SuperJsonSerializer` supports only the data types that SuperJson handles, plus `AsyncIteratorObject` at the root level for [Event Iterator](/docs/event-iterator). It does not support all [RPC supported types](/docs/rpc-handler#supported-data-types). ::: ```ts twoslash import { createORPCErrorFromJson, ErrorEvent, isORPCErrorJson, mapEventIterator, toORPCError } from '@orpc/client' import type { StandardRPCSerializer } from '@orpc/client/standard' import { isAsyncIteratorObject } from '@orpc/shared' import SuperJSON from 'superjson' export class SuperJSONSerializer implements Pick { serialize(data: unknown): object { if (isAsyncIteratorObject(data)) { return mapEventIterator(data, { value: async (value: unknown) => SuperJSON.serialize(value), error: async (e) => { return new ErrorEvent({ data: SuperJSON.serialize(toORPCError(e).toJSON()), cause: e, }) }, }) } return SuperJSON.serialize(data) } deserialize(data: any): unknown { if (isAsyncIteratorObject(data)) { return mapEventIterator(data, { value: async value => SuperJSON.deserialize(value), error: async (e) => { if (!(e instanceof ErrorEvent)) return e const deserialized = SuperJSON.deserialize(e.data as any) if (isORPCErrorJson(deserialized)) { return createORPCErrorFromJson(deserialized, { cause: e }) } return new ErrorEvent({ data: deserialized, cause: e, }) }, }) } return SuperJSON.deserialize(data) } } ``` ## SuperJson Handler ```ts twoslash declare class SuperJSONSerializer implements Pick { serialize(data: unknown): object deserialize(data: unknown): unknown } // ---cut--- import type { StandardRPCSerializer } from '@orpc/client/standard' import type { Context, Router } from '@orpc/server' import type { FetchHandlerOptions } from '@orpc/server/fetch' import { FetchHandler } from '@orpc/server/fetch' import { StrictGetMethodPlugin } from '@orpc/server/plugins' import type { StandardHandlerOptions } from '@orpc/server/standard' import { StandardHandler, StandardRPCCodec, StandardRPCMatcher } from '@orpc/server/standard' export interface SuperJSONHandlerOptions extends StandardHandlerOptions { /** * Enable or disable the StrictGetMethodPlugin. * * @default true */ strictGetMethodPluginEnabled?: boolean } export class SuperJSONHandler extends FetchHandler { constructor(router: Router, options: NoInfer & SuperJSONHandlerOptions> = {}) { options.plugins ??= [] const strictGetMethodPluginEnabled = options.strictGetMethodPluginEnabled ?? true if (strictGetMethodPluginEnabled) { options.plugins.push(new StrictGetMethodPlugin()) } const serializer = new SuperJSONSerializer() const matcher = new StandardRPCMatcher() const codec = new StandardRPCCodec(serializer as any) super(new StandardHandler(router, matcher, codec, options), options) } } ``` ## SuperJson Link ```ts twoslash declare class SuperJSONSerializer implements Pick { serialize(data: unknown): object deserialize(data: unknown): unknown } // ---cut--- import type { ClientContext } from '@orpc/client' import { StandardLink, StandardRPCLinkCodec } from '@orpc/client/standard' import type { StandardLinkOptions, StandardRPCLinkCodecOptions, StandardRPCSerializer } from '@orpc/client/standard' import type { LinkFetchClientOptions } from '@orpc/client/fetch' import { LinkFetchClient } from '@orpc/client/fetch' export interface SuperJSONLinkOptions extends StandardLinkOptions, StandardRPCLinkCodecOptions, LinkFetchClientOptions { } export class SuperJSONLink extends StandardLink { constructor(options: SuperJSONLinkOptions) { const linkClient = new LinkFetchClient(options) const serializer = new SuperJSONSerializer() const linkCodec = new StandardRPCLinkCodec(serializer as any, options) super(linkCodec, linkClient, options) } } ``` --- --- url: /docs/integrations/svelte-kit.md description: Integrate oRPC with SvelteKit --- # SvelteKit Integration [SvelteKit](https://svelte.dev/docs/kit/introduction) is a framework for rapidly developing robust, performant web applications using Svelte. For additional context, refer to the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Basic ::: code-group ```ts [src/routes/rpc/[...rest]/+server.ts] import { error } from '@sveltejs/kit' import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(router) 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/openapi-handler), or another custom handler. ::: --- --- url: /docs/tanstack-query/basic.md description: Seamlessly integrate oRPC with Tanstack Query --- # Tanstack Query Integration [Tanstack Query](https://tanstack.com/query/latest) is a robust solution for asynchronous state management. oRPC’s integration with Tanstack Query is lightweight and straightforward - there’s no extra overhead. | Library | Tanstack Query | oRPC Integration | | ------- | -------------- | ----------------------------------------------------- | | React | ✅ | ✅ | | Vue | ✅ | ✅ | | Angular | ✅ | [Vote here](https://github.com/unnoq/orpc/issues/157) | | Solid | ✅ | ✅ | | Svelte | ✅ | ✅ | ::: warning This documentation assumes you are already familiar with [Tanstack Query](https://tanstack.com/query/latest). If you need a refresher, please review the official Tanstack Query documentation before proceeding. ::: ## Query Options Utility Use `.queryOptions` to configure queries. Use it with hooks like `useQuery`, `useSuspenseQuery`, or `prefetchQuery`. ```ts const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 }, // Specify input if needed context: { cache: true }, // Provide client context if needed // additional options... })) ``` ## Infinite Query Options Utility Use `.infiniteOptions` to configure infinite queries. Use it with hooks like `useInfiniteQuery`, `useSuspenseInfiniteQuery`, or `prefetchInfiniteQuery`. ::: info The `input` parameter must be a function that accepts the page parameter and returns the query input. Be sure to define the type for `pageParam` if it can be `null` or `undefined`. ::: ```ts const query = useInfiniteQuery(orpc.planet.list.infiniteOptions({ input: (pageParam: number | undefined) => ({ limit: 10, offset: pageParam }), context: { cache: true }, // Provide client context if needed initialPageParam: undefined, getNextPageParam: lastPage => lastPage.nextPageParam, // additional options... })) ``` ## Mutation Options Use `.mutationOptions` to create options for mutations. Use it with hooks like `useMutation`. ```ts const mutation = useMutation(orpc.planet.create.mutationOptions({ context: { cache: true }, // Provide client context if needed // additional options... })) mutation.mutate({ name: 'Earth' }) ``` ## Query/Mutation Key Use `.key` to generate a `QueryKey` or `MutationKey`. This is useful for tasks such as revalidating queries, checking mutation status, etc. :::info The `.key` accepts partial deep input—there’s no need to supply full input. ::: ```ts const queryClient = useQueryClient() // Invalidate all planet queries queryClient.invalidateQueries({ queryKey: orpc.planet.key(), }) // Invalidate only regular (non-infinite) planet queries queryClient.invalidateQueries({ queryKey: orpc.planet.key({ type: 'query' }) }) // Invalidate the planet find query with id 123 queryClient.invalidateQueries({ queryKey: orpc.planet.find.key({ input: { id: 123 } }) }) ``` ## Calling Procedure Clients Use `.call` to call a procedure client directly. It's an alias for corresponding procedure client. ```ts const result = orpc.planet.find.call({ id: 123 }) ``` ## Error Handling Easily manage type-safe errors using our built-in `isDefinedError` helper. ```ts import { isDefinedError } from '@orpc/client' const mutation = useMutation(orpc.planet.create.mutationOptions({ onError: (error) => { if (isDefinedError(error)) { // Handle the error here } } })) mutation.mutate({ name: 'Earth' }) if (mutation.error && isDefinedError(mutation.error)) { // Handle the error here } ``` For more details, see our [type-safe error handling guide](/docs/error-handling#type‐safe-error-handling). --- --- url: /docs/tanstack-query/react.md description: Seamlessly integrate oRPC with Tanstack Query for React --- # Tanstack Query Integration For React This guide shows how to integrate oRPC with Tanstack Query for React. For an introduction, please review the [Basic Guide](/docs/tanstack-query/basic) first. ## Installation ::: code-group ```sh [npm] npm install @orpc/react-query@latest @tanstack/react-query@latest ``` ```sh [yarn] yarn add @orpc/react-query@latest @tanstack/react-query@latest ``` ```sh [pnpm] pnpm add @orpc/react-query@latest @tanstack/react-query@latest ``` ```sh [bun] bun add @orpc/react-query@latest @tanstack/react-query@latest ``` ```sh [deno] deno install npm:@orpc/react-query@latest npm:@tanstack/react-query@latest ``` ::: ## Setup Before you begin, ensure you have already configured a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' declare const client: RouterClient // ---cut--- import { createORPCReactQueryUtils } from '@orpc/react-query' export const orpc = createORPCReactQueryUtils(client) orpc.planet.find.queryOptions({ input: { id: 123 } }) // ^| // ``` ## Avoiding Query/Mutation Key Conflicts Prevent key conflicts by passing a unique base key when creating your utils: ```ts const userORPC = createORPCReactQueryUtils(userClient, { path: ['user'] }) const postORPC = createORPCReactQueryUtils(postClient, { path: ['post'] }) ``` ## Using React Context Integrate oRPC React Query utils into your React app with Context: 1. **Create the Context:** ```ts twoslash import { router } from './shared/planet' // ---cut--- import { createContext, use } from 'react' import { RouterUtils } from '@orpc/react-query' import { RouterClient } from '@orpc/server' type ORPCReactUtils = RouterUtils> export const ORPCContext = createContext(undefined) export function useORPC(): ORPCReactUtils { const orpc = use(ORPCContext) if (!orpc) { throw new Error('ORPCContext is not set up properly') } return orpc } ``` 2. **Provide the Context in Your App:** ```tsx export function App() { const [client] = useState>(() => createORPCClient(link)) const [orpc] = useState(() => createORPCReactQueryUtils(client)) return ( ) } ``` 3. **Use the Utils in Components:** ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' import { RouterUtils } from '@orpc/react-query' import { useQuery } from '@tanstack/react-query' declare function useORPC(): RouterUtils> // ---cut--- const orpc = useORPC() const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 } })) ``` ## `skipToken` for Disabling Queries You can still use [skipToken](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries/#typesafe-disabling-of-queries-using-skiptoken) by conditionally overriding the `queryFn` property: ```ts twoslash import type { router } from './shared/planet' import type { RouterClient } from '@orpc/server' import type { RouterUtils } from '@orpc/react-query' declare const orpc: RouterUtils> declare const condition: boolean // ---cut--- import { skipToken, useQuery } from '@tanstack/react-query' const options = orpc.planet.find.queryOptions({ input: { id: 123 }, }) const query = useQuery({ ...options, queryFn: condition ? skipToken : options.queryFn, }) ``` --- --- url: /docs/tanstack-query/solid.md description: Seamlessly integrate oRPC with Tanstack Query for Solid --- # Tanstack Query Integration For Solid This guide shows how to integrate oRPC with Tanstack Query for Solid. For an introduction, please review the [Basic Guide](/docs/tanstack-query/basic) first. ## Installation ::: code-group ```sh [npm] npm install @orpc/solid-query@latest @tanstack/solid-query@latest ``` ```sh [yarn] yarn add @orpc/solid-query@latest @tanstack/solid-query@latest ``` ```sh [pnpm] pnpm add @orpc/solid-query@latest @tanstack/solid-query@latest ``` ```sh [bun] bun add @orpc/solid-query@latest @tanstack/solid-query@latest ``` ```sh [deno] deno install npm:@orpc/solid-query@latest npm:@tanstack/solid-query@latest ``` ::: ## Setup Before you begin, ensure you have already configured a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' declare const client: RouterClient // ---cut--- import { createORPCSolidQueryUtils } from '@orpc/solid-query' export const orpc = createORPCSolidQueryUtils(client) orpc.planet.find.queryOptions({ input: { id: 123 } }) // ^| // ``` ## Avoiding Query/Mutation Key Conflicts Prevent key conflicts by passing a unique base key when creating your utils: ```ts const userORPC = createORPCSolidQueryUtils(userClient, { path: ['user'] }) const postORPC = createORPCSolidQueryUtils(postClient, { path: ['post'] }) ``` ## Usage :::warning Unlike the React version, when creating a Solid Query Signal, the first argument must be a callback. ::: ```ts twoslash import type { router } from './shared/planet' import type { RouterClient } from '@orpc/server' import type { RouterUtils } from '@orpc/solid-query' declare const orpc: RouterUtils> declare const condition: boolean // ---cut--- import { createQuery } from '@tanstack/solid-query' const query = createQuery( () => orpc.planet.find.queryOptions({ input: { id: 123 } }) ) ``` --- --- url: /docs/tanstack-query/svelte.md description: Seamlessly integrate oRPC with Tanstack Query for Svelte --- # Tanstack Query Integration For Svelte This guide shows how to integrate oRPC with Tanstack Query for Svelte. For an introduction, please review the [Basic Guide](/docs/tanstack-query/basic) first. ## Installation ::: code-group ```sh [npm] npm install @orpc/svelte-query@latest @tanstack/svelte-query@latest ``` ```sh [yarn] yarn add @orpc/svelte-query@latest @tanstack/svelte-query@latest ``` ```sh [pnpm] pnpm add @orpc/svelte-query@latest @tanstack/svelte-query@latest ``` ```sh [bun] bun add @orpc/svelte-query@latest @tanstack/svelte-query@latest ``` ```sh [deno] deno install npm:@orpc/svelte-query@latest npm:@tanstack/svelte-query@latest ``` ::: ## Setup Before you begin, ensure you have already configured a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' declare const client: RouterClient // ---cut--- import { createORPCSvelteQueryUtils } from '@orpc/svelte-query' export const orpc = createORPCSvelteQueryUtils(client) orpc.planet.find.queryOptions({ input: { id: 123 } }) // ^| // ``` ## Avoiding Query/Mutation Key Conflicts Prevent key conflicts by passing a unique base key when creating your utils: ```ts const userORPC = createORPCSvelteQueryUtils(userClient, { path: ['user'] }) const postORPC = createORPCSvelteQueryUtils(postClient, { path: ['post'] }) ``` ## Reactivity To create reactive queries, use Svelte's legacy `derived` API from `svelte/store`. With the [Tanstack Svelte v5 branch](https://github.com/TanStack/query/discussions/7413), oRPC should work out of the box. ```ts twoslash import type { router } from './shared/planet' import type { RouterClient } from '@orpc/server' import type { RouterUtils } from '@orpc/svelte-query' declare const orpc: RouterUtils> declare const condition: boolean // ---cut--- import { createQuery } from '@tanstack/svelte-query' import { derived, writable } from 'svelte/store' const id = writable(123) const query = createQuery( derived(id, $id => orpc.planet.find.queryOptions({ input: { id: $id } })), ) ``` --- --- url: /docs/tanstack-query/vue.md description: Seamlessly integrate oRPC with Tanstack Query for Vue --- # Tanstack Query Integration For Vue This guide shows how to integrate oRPC with Tanstack Query for Vue. For an introduction, please review the [Basic Guide](/docs/tanstack-query/basic) first. ## Installation ::: code-group ```sh [npm] npm install @orpc/vue-query@latest @tanstack/vue-query@latest ``` ```sh [yarn] yarn add @orpc/vue-query@latest @tanstack/vue-query@latest ``` ```sh [pnpm] pnpm add @orpc/vue-query@latest @tanstack/vue-query@latest ``` ```sh [bun] bun add @orpc/vue-query@latest @tanstack/vue-query@latest ``` ```sh [deno] deno install npm:@orpc/vue-query@latest npm:@tanstack/vue-query@latest ``` ::: ## Setup Before you begin, ensure you have already configured a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts twoslash import { router } from './shared/planet' import { RouterClient } from '@orpc/server' declare const client: RouterClient // ---cut--- import { createORPCVueQueryUtils } from '@orpc/vue-query' export const orpc = createORPCVueQueryUtils(client) orpc.planet.find.queryOptions({ input: { id: 123 } }) // ^| // ``` ## Avoiding Query/Mutation Key Conflicts Prevent key conflicts by passing a unique base key when creating your utils: ```ts const userORPC = createORPCVueQueryUtils(userClient, { path: ['user'] }) const postORPC = createORPCVueQueryUtils(postClient, { path: ['post'] }) ``` --- --- url: /docs/integrations/tanstack-start.md description: Integrate oRPC with TanStack Start --- # TanStack Start Integration [TanStack Start](https://tanstack.com/start) is a full-stack React framework built on [Nitro](https://nitro.build/) and the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). For additional context, see the [Fetch Server Integration](/docs/integrations/fetch-server) guide. ## Server You can integrate oRPC handlers with TanStack Start using its [API Routes](https://tanstack.com/start/latest/docs/framework/react/api-routes). ::: code-group ```ts [app/routes/api/rpc.$.ts] import { RPCHandler } from '@orpc/server/fetch' import { createAPIFileRoute } from '@tanstack/start/api' const handler = new RPCHandler(router) async function handle({ request }: { request: Request }) { const { response } = await handler.handle(request, { prefix: '/api/rpc', context: {} // Provide initial context if needed }) return response ?? new Response('Not Found', { status: 404 }) } export const APIRoute = createAPIFileRoute('/api/rpc/$')({ GET: handle, POST: handle, PUT: handle, PATCH: handle, DELETE: handle, }) ``` ```ts [app/routes/api/rpc.ts] import { createAPIFileRoute } from '@tanstack/start/api' import { APIRoute as BaseAPIRoute } from './rpc.$' export const APIRoute = createAPIFileRoute('/api/rpc')(BaseAPIRoute.methods) ``` ::: ::: info The `handler` can be any supported oRPC handler, including [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or a custom handler. ::: Alternatively, you can use any other supported server integration for your oRPC handlers while using TanStack Start solely for client-side functionality. ## Client Once you've set up the client following the [Client-Side Clients](/docs/client/client-side) guide, you can use it directly in your TanStack Start [Routes](https://tanstack.com/start/latest/docs/framework/react/learn-the-basics#routes) for both data fetching and mutations: ::: code-group ```tsx [src/routes/index.tsx] import { createFileRoute, useRouter } from '@tanstack/react-router' import { client } from '@/lib/client' export const Route = createFileRoute('/')({ component: Home, loader: async () => { return await client.counter.get() }, }) function Home() { const router = useRouter() const state = Route.useLoaderData() const updateCount = async () => { await client.counter.increment() router.invalidate() } return (
{state.value}
) } ``` ```ts [src/lib/contract.ts] import { oc } from '@orpc/contract' import { z } from 'zod' export const Count = z.object({ value: z.number().int().min(0), updatedAt: z.date(), }) export const incrementCountContract = oc .route({ method: 'POST', path: '/count:increment' }) .output(Count) export const getCountContract = oc .route({ method: 'GET', path: '/count' }) .output(Count) export const contract = { counter: { increment: incrementCountContract, get: getCountContract }, } ``` ::: ::: info In this example, the oRPC client provides functionality similar to TanStack Start's [Server Functions](https://tanstack.com/start/latest/docs/framework/react/server-functions). ::: ## SSR During server-side rendering, the server (not the browser) calls the route loader. When your handlers require browser headers or cookies, you'll need to forward them to the oRPC client using the `headers` option and the `getHeaders` function from TanStack Start. If your handlers run on a separate server or if you're comfortable with multiple network requests, you can reuse your client-side client instance: ```ts [src/lib/client.ts] import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' import { ContractRouterClient } from '@orpc/contract' import { getHeaders } from '@tanstack/react-start/server' import { contract } from '@/lib/contract' export const link = new RPCLink({ url: 'http://localhost:8080/rpc', headers: () => { // For server-side rendering if (typeof window === 'undefined') { return getHeaders() } // For client-side rendering return {} }, }) export const api: ContractRouterClient = createORPCClient(link) ``` Alternatively, you can create an isomorphic client by combining both the [Client-Side Client](/docs/client/client-side.md) and [Server-Side Client](/docs/client/server-side.md) approaches. --- --- url: /docs/advanced/validation-errors.md description: Learn about oRPC's built-in validation errors and how to customize them. --- # Validation Errors oRPC provides built-in validation errors that work well by default. However, you might sometimes want to customize them. ## Customizing with Client Interceptors [Client Interceptors](/docs/lifecycle) are preferred because they run before error validation, ensuring that your custom errors are properly validated. ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { onError, ORPCError, ValidationError } from '@orpc/server' import { ZodError } from 'zod' import type { ZodIssue } from 'zod' const handler = new RPCHandler(router, { clientInterceptors: [ onError((error) => { if ( error instanceof ORPCError && error.code === 'BAD_REQUEST' && error.cause instanceof ValidationError ) { // If you only use Zod you can safely cast to ZodIssue[] const zodError = new ZodError(error.cause.issues as ZodIssue[]) throw new ORPCError('INPUT_VALIDATION_FAILED', { status: 422, data: zodError.flatten(), cause: error.cause, }) } if ( error instanceof ORPCError && error.code === 'INTERNAL_SERVER_ERROR' && error.cause instanceof ValidationError ) { throw new ORPCError('OUTPUT_VALIDATION_FAILED', { cause: error.cause, }) } }), ], }) ``` ## Customizing with Middleware ```ts twoslash import { z, ZodError } from 'zod' import type { ZodIssue } from 'zod' import { onError, ORPCError, os, ValidationError } from '@orpc/server' const base = os.use(onError((error) => { if ( error instanceof ORPCError && error.code === 'BAD_REQUEST' && error.cause instanceof ValidationError ) { // If you only use Zod you can safely cast to ZodIssue[] const zodError = new ZodError(error.cause.issues as ZodIssue[]) throw new ORPCError('INPUT_VALIDATION_FAILED', { status: 422, data: zodError.flatten(), cause: error.cause, }) } if ( error instanceof ORPCError && error.code === 'INTERNAL_SERVER_ERROR' && error.cause instanceof ValidationError ) { throw new ORPCError('OUTPUT_VALIDATION_FAILED', { cause: error.cause, }) } })) const getting = base .input(z.object({ id: z.string().uuid() })) .output(z.object({ id: z.string().uuid(), name: z.string() })) .handler(async ({ input, context }) => { return { id: input.id, name: 'name' } }) ``` Every [procedure](/docs/procedure) built from `base` now uses these customized validation errors. :::warning Middleware applied before `.input`/`.output` catches validation errors by default, but this behavior can be configured. ::: ## Type‑Safe Validation Errors As explained in the [error handling guide](/docs/error-handling#combining-both-approaches), when you throw an `ORPCError` instance, if the `code` and `data` match with the errors defined in the `.errors` method, oRPC will treat it exactly as if you had thrown `errors.[code]` using the type‑safe approach. ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' // ---cut--- import { onError, ORPCError, os, ValidationError } from '@orpc/server' import { z, ZodError } from 'zod' import type { ZodIssue } from 'zod' const base = os.errors({ INPUT_VALIDATION_FAILED: { data: z.object({ formErrors: z.array(z.string()), fieldErrors: z.record(z.string(), z.array(z.string()).optional()), }), }, }) const example = base .input(z.object({ id: z.string().uuid() })) .handler(() => { /** do something */ }) const handler = new RPCHandler({ example }, { clientInterceptors: [ onError((error) => { if ( error instanceof ORPCError && error.code === 'BAD_REQUEST' && error.cause instanceof ValidationError ) { // If you only use Zod you can safely cast to ZodIssue[] const zodError = new ZodError(error.cause.issues as ZodIssue[]) throw new ORPCError('INPUT_VALIDATION_FAILED', { status: 422, data: zodError.flatten(), cause: error.cause, }) } }), ], }) ``` --- --- url: /docs/openapi/plugins/zod-smart-coercion.md description: >- A refined alternative to `z.coerce` that automatically converts inputs to the expected type without modifying the input schema. --- # Zod Smart Coercion A Plugin refined alternative to `z.coerce` that automatically converts inputs to the expected type without modifying the input schema. ## Installation ::: code-group ```sh [npm] npm install @orpc/zod@latest ``` ```sh [yarn] yarn add @orpc/zod@latest ``` ```sh [pnpm] pnpm add @orpc/zod@latest ``` ```sh [bun] bun add @orpc/zod@latest ``` ```sh [deno] deno install npm:@orpc/zod@latest ``` ::: ## Setup ```ts import { OpenAPIHandler } from '@orpc/openapi/fetch' import { ZodSmartCoercionPlugin } from '@orpc/zod' const handler = new OpenAPIHandler(router, { plugins: [new ZodSmartCoercionPlugin()] }) ``` :::warning Do not use this plugin with [RPCHandler](/docs/rpc-handler) as it may negatively impact performance. ::: ## Safe and Predictable Conversion Zod Smart Coercion converts data only when: 1. The schema expects a specific type and the input can be converted. 2. The input does not already match the schema. For example: * If the input is `'true'` but the schema does not expect a boolean, no conversion occurs. * If the schema accepts both boolean and string, `'true'` will not be coerced to a boolean. ### Conversion Rules #### Boolean Converts string representations of boolean values: ```ts const raw = 'true' // Input const coerced = true // Output ``` Supported values: * `'true'`, `'on'`, `'t'` → `true` * `'false'`, `'off'`, `'f'` → `false` #### Number Converts numeric strings: ```ts const raw = '42' // Input const coerced = 42 // Output ``` #### BigInt Converts strings representing valid BigInt values: ```ts const raw = '12345678901234567890' // Input const coerced = 12345678901234567890n // Output ``` #### Date Converts valid date strings into Date objects: ```ts const raw = '2024-11-27T00:00:00.000Z' // Input const coerced = new Date('2024-11-27T00:00:00.000Z') // Output ``` Supported formats: * Full ISO date-time (e.g., `2024-11-27T00:00:00.000Z`) * Date only (e.g., `2024-11-27`) #### RegExp Converts strings representing regular expressions: ```ts const raw = '/^abc$/i' // Input const coerced = /^abc$/i // Output ``` #### URL Converts valid URL strings into URL objects: ```ts const raw = 'https://example.com' // Input const coerced = new URL('https://example.com') // Output ``` #### Set Converts arrays into Set objects, removing duplicates: ```ts const raw = ['apple', 'banana', 'apple'] // Input const coerced = new Set(['apple', 'banana']) // Output ``` #### Map Converts arrays of key-value pairs into Map objects: ```ts const raw = [ ['key1', 'value1'], ['key2', 'value2'] ] // Input const coerced = new Map([ ['key1', 'value1'], ['key2', 'value2'] ]) // Output ```