--- url: /docs/getting-started.md --- # Getting Started // TODO --- --- url: /docs/procedure.md --- # Procedure Procedures are the core building blocks of oRPC. They define the logic for handling specific operations, including input validation, output validation, and middleware application. Each procedure is created using a builder pattern that allows for flexible composition and reuse. ## Overview ```ts twoslash import { z } from 'zod' import type { AnyMetaPlugin } from '@orpc/server' declare const someMeta: AnyMetaPlugin const requireAuth = os .middleware(({ context, next }) => { return next({ context: { user: { id: 1 } } }) }) const canEdit = os .$context<{ user: { id: number } }>() .middleware(async ({ next }, id: number) => { return next() }) // ---cut--- import { os } from '@orpc/server' const example = os .$context<{ something?: string }>() // <- define initial context .meta(someMeta) // <- attach metadata .errors({ NOT_FOUND: {} }) // <- define errors .use(requireAuth) // <- apply middleware .input(z.object({ id: z.number(), name: z.string() })) // <- input validation .use(canEdit.adaptInput(input => input.id)) // <- middleware with typed input .output(z.object({ id: z.number(), name: z.string() })) // <- output validation .handler(async ({ input, context, errors }) => { // <- handler logic return { id: 1, name: 'example' } }) ``` :::info The `.handler` method is the only required step. All other chains are optional. ::: ## Initial Context Use `.$context` to declare the initial context required for a procedure to execute. Learn more in the [Context Documentation](/docs/context). ## Metadata Use `.meta` to attach metadata to a procedure. You can access this metadata later in middleware or plugins. Learn more in the [Metadata Documentation](/docs/metadata). ## Typesafe Errors Use `.errors` to define error definitions for a procedure. These errors can be thrown in the handler or middleware and will be properly typed on the client. Learn more in the [Typesafe Error Handling documentation](/docs/error-handling#typesafe-errors). ## Input/Output Validation oRPC supports [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), [Arktype](https://arktype.io/), and any other [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library for validation. ::: tip By specifying `.output` or the handler's return type, TypeScript can infer the output without analyzing the handler body. This can significantly improve type-checking and IDE suggestion performance for complex handlers. ::: ### Multiple Schemas `.input` and `.output` can be called multiple times. Each call adds another schema instead of replacing an earlier one. ```ts const example = os .input(z.looseObject({ name: z.string() })) .input(z.looseObject({ id: z.number() })) .output(z.looseObject({ name: z.string() })) .output(z.looseObject({ id: z.number() })) .handler(async ({ input }) => { return { id: 1, name: 'example' } }) ``` ::: warning When you stack schemas, the input or output must satisfy all of them, so the schemas need to be compatible. For example, with Zod, prefer `z.looseObject` over `z.object` to allow unknown properties. ::: ### `type` Utility For simple use cases without external libraries, use oRPC's built-in `type` utility. It takes a mapping function as its first argument: ```ts import { type } from '@orpc/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 */ }) ``` ::: warning [Middleware](/docs/middleware) can only be applied when the [current context](/docs/context#combining-initial-and-middleware-context) satisfies the [middleware's initial context](/docs/middleware#initial-context) and does not conflict with the context the middleware adds. ::: ::: info You can use [`.adaptInput`](/docs/middleware#middleware-input) when applying middleware to adapt the input to a different shape that the middleware expects. ```ts const canEdit = os.middleware(async ({ next }, id: string) => { if (!canUserEdit(id)) { throw new ORPCError('UNAUTHORIZED') } return next() }) const example = os .input(z.object({ id: z.string(), name: z.string() })) .use(canEdit.adaptInput(input => input.id)) // Adapt input to match middleware's expected shape .handler(async ({ context }) => { /* logic */ }) ``` ::: ## 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(requireAuth) // Extends 'pub' with authentication const pubExample = pub .handler(async ({ context }) => { /* logic */ }) const authedExample = authed .handler(async ({ context }) => { /* logic */ }) ``` This pattern helps prevent duplication while maintaining flexibility. --- --- url: /docs/router.md --- # Router A router is a plain, nestable object made up of procedures. Routers can also modify those procedures, which makes it easy to organize and extend your API. ::: info A standalone [procedure](/docs/procedure) is also a router, so you can use all router features on individual procedures too. ::: ## Overview Define a router as a plain JavaScript object where each key maps to a procedure: ```ts twoslash import { os } from '@orpc/server' const ping = os.handler(async () => 'ping') const pong = os.handler(async () => 'pong') export const router = { ping, pong, nested: { ping, pong } } ``` ::: warning For compatibility, do not use these router keys: `then`, `bind`, `valueOf`, `toString`, `toJSON`. ::: ## Extending Router You can extend a router with shared behavior. For example, by applying authentication middleware or attaching metadata to every procedure: ```ts const router = os.use(requiredAuth).meta(requireAuthMeta).router({ ping, pong, nested: { ping, pong, } }) ``` ::: danger If you apply middleware with `.use` at both the router and procedure levels, it may run more than once. That duplication can hurt performance. To avoid redundant middleware execution, see our [best practices for middleware deduplication](/docs/best-practices/dedupe-middleware). ::: ## Lazy Router Routers can also be lazy-loaded. This is useful for code splitting and can improve cold start performance by deferring route initialization until it is needed. ::: 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 A standalone [procedure](/docs/procedure) is also a router, so these utilities work with procedures too. ::: ### Infer Router Inputs Infers the input type for each procedure in the router. ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterInputs } from '@orpc/server' export type Inputs = InferRouterInputs type FindPlanetInput = Inputs['planet']['find'] ``` ### Infer Router Outputs Infers the output type for each procedure in the router. ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterOutputs } from '@orpc/server' export type Outputs = InferRouterOutputs type FindPlanetOutput = Outputs['planet']['find'] ``` ### Infer Router Initial Contexts Infers the [initial context](/docs/context#initial-context) for each procedure in the router. ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterInitialContexts } from '@orpc/server' export type InitialContexts = InferRouterInitialContexts type FindPlanetInitialContext = InitialContexts['planet']['find'] ``` ### Infer Router Final Contexts Infers the final context for each procedure in the router by combining the [initial and injected context](/docs/context#combining-initial-and-injected-context). This is the closest match to the context the procedure's handler receives. ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterFinalContexts } from '@orpc/server' export type FinalContexts = InferRouterFinalContexts type FindPlanetFinalContext = FinalContexts['planet']['find'] ``` ### Infer Router Errors Infers the throwable errors each procedure in a router can produce. ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterErrors } from '@orpc/server' export type Errors = InferRouterErrors type FindPlanetError = Errors['planet']['find'] ``` ### Infer Router Error Infers all possible throwable errors the entire router can produce. This is useful when you want a single type for router-wide error handling. ```ts twoslash import type { router } from './shared/planet' // ---cut--- import type { InferRouterError } from '@orpc/server' export type RouterError = InferRouterError ``` --- --- url: /docs/middleware.md --- # Middleware Middleware is a powerful mechanism in oRPC that allows you to execute code before and after your procedure handlers, enabling features like authentication, logging, caching, and more. It provides a way to modify the context, input, and output of procedures in a flexible and composable manner. ## Overview ```ts twoslash import type { AnyMetaPlugin } from '@orpc/server' declare const someMeta: AnyMetaPlugin // ---cut--- import { os } from '@orpc/server' const example = os .$context<{ something?: string }>() // <- define initial context .meta(someMeta) // <- attach metadata .errors({ RATE_LIMITED: {} }) // <- attach errors .middleware(async ({ context, next, errors }) => { // <- middleware logic try { // `await` is required to catch async errors return await next({ context: { // <- Inject additional context user: { id: 1, name: 'John' } } }) } catch (error) { console.error(error) throw error } finally { // Cleanup logic after execution } }) ``` ## Initial Context Use `.$context` to declare the initial context required when middleware is applied. Learn more in the [Context Documentation](/docs/context). ## Metadata Use `.meta` to attach metadata to middleware. This metadata is applied to any procedures that use the middleware. Learn more in the [Metadata documentation](/docs/metadata). ## Typesafe Errors Use `.errors` to attach error definitions to middleware. These errors are available in the middleware and any procedures that use it. Learn more in the [Typesafe Error Handling documentation](/docs/error-handling#typesafe-errors). ## Middleware Context Middleware can be used to inject or guard the [context](/docs/context). ```ts twoslash import { ORPCError, os } from '@orpc/server' declare function auth(): { userId: number } | null // ---cut--- const setting = os .use(async ({ context, next }) => { return next({ context: { auth: await auth() // <- inject auth } }) }) .use(async ({ context, next }) => { if (!context.auth) { // <- guard auth throw new ORPCError('UNAUTHORIZED') } return next({ context: { auth: context.auth // <- override auth (now guaranteed to be non-null) } }) }) .handler(async ({ context }) => { console.log(context.auth) // <- auth is guaranteed to be non-null here }) ``` ::: warning Context passed to `next` must not conflict with the existing context; it is merged at runtime. ::: ## Middleware Input Middleware can access input in type-safe manner, 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) // <- input already matches middleware's expected shape .handler(async ({ input }) => { // Handler logic }) const pong = os .input(z.object({ id: z.number() })) .use(canUpdate.adaptInput(input => input.id)) // <- adapt input to match middleware's expected shape .handler(async ({ input }) => { // Handler logic }) ``` ::: info You can adapt a middleware to accept a different input shape by using `.adaptInput`. ```ts const canUpdate = os.middleware(async ({ context, next }, input: number) => { return next() }) // Transform middleware to accept a new input shape const adaptedCanUpdate = canUpdate.adaptInput((input: { id: number }) => input.id) ``` ::: ## Middleware Output Middleware can also modify the output of a handler, such as implementing caching mechanisms. ```ts const cache = os.middleware(async ({ context, next, path }, input, done) => { const cacheKey = path.join('/') + JSON.stringify(input) if (db.has(cacheKey)) { return done({ output: db.get(cacheKey) }) } const result = await next({}) db.set(cacheKey, result.output) return result }) ``` ## Inline Middleware Middleware is simply a function that can be defined inline with `.use`, which is useful for simple middleware cases. ```ts const example = os .use(async ({ context, next }) => { // Execute logic before the handler return next() }) .handler(async ({ context }) => { // Handler logic }) ``` ## Combining Middleware Multiple middleware functions can be combined using `.use`. ```ts const mergedMiddleware = aMiddleware .use(async ({ next }) => next()) .use(anotherMiddleware) ``` ::: info To concatenate two middlewares with different input types, use `.adaptInput` to align their inputs first. ::: --- --- url: /docs/context.md --- # Context The context mechanism provides a type-safe dependency injection pattern. It lets you provide required dependencies explicitly or inject them dynamically through middleware. ## Initial Context Use initial context for values that come from the environment. Declare it with `.$context`, then provide it when executing the procedure: ```ts twoslash import { os } from '@orpc/server' // ---cut--- const base = os.$context<{ env: { DB_URL: string } }>() export const getting = base .handler(async ({ context }) => { console.log(context.env) }) ``` ::: info When a procedure requires initial context when calling, you must manually pass it: ```ts twoslash import { call, os } from '@orpc/server' const base = os.$context<{ env: { DB_URL: string } }>() const getting = base.handler(async ({ context }) => {}) // ---cut--- const output = await call(getting, undefined, { context: { // <- initial context must be passed when calling env: { DB_URL: 'postgres://...' }, }, }) ``` ::: ### Default Initial Context To avoid repeating `.$context` declarations, you can define a default initial context type globally. ```ts declare module '@orpc/server' { export interface DefaultInitialContext { env: { DB_URL: string } } } ``` ## Injected Context Injected context is injected at runtime through [middleware](/docs/middleware#middleware-context): ```ts twoslash import { os } from '@orpc/server' declare const env: { DB_URL: string } // ---cut--- const base = os.use(async ({ next }) => next({ context: { env: { DB_URL: env.DB_URL }, }, })) export const getting = base.handler(async ({ context }) => { console.log(context.env) }) ``` ::: info When you use middleware context, you do not need to pass context manually when calling: ```ts twoslash import { call, os } from '@orpc/server' declare const env: { DB_URL: string } const base = os.use(async ({ next }) => next({ context: { env: { DB_URL: env.DB_URL }, }, })) const getting = base.handler(async ({ context }) => {}) // ---cut--- // no need to pass context manually when calling const output = await call(getting) ``` ::: ## Combining Initial and Injected Context In many cases, you will use both. Use initial context for environment-specific values, such as database URLs, and injected context for runtime data, such as authenticated users. ```ts twoslash import { ORPCError, os } from '@orpc/server' declare function parseJWT(token: string | undefined, secret: string): { userId: number } | null // ---cut--- const base = os.$context<{ headers: Headers, env: { JWT_SECRET: string } }>() const requireAuth = base.middleware(async ({ context, next }) => { const user = parseJWT( context.headers.get('authorization')?.split(' ')[1], context.env.JWT_SECRET ) if (!user) { throw new ORPCError('UNAUTHORIZED') } return next({ context: { user } }) }) const getting = base .use(requireAuth) .handler(async ({ context }) => { console.log(context.env) console.log(context.user) }) ``` --- --- url: /docs/error-handling.md --- # Error Handling Error handling in oRPC is flexible and consistent. You can use the `ORPCError` class, define typesafe errors, and adapt custom error classes while still returning meaningful feedback to clients. ## `ORPCError` Class `ORPCError` is the standard error type in oRPC. It includes a `code`, plus optional `message` and `data` fields. ::: danger `message` and `data` are sent to the client. Do not include sensitive information in either field. ::: ```ts twoslash declare const notFound: boolean // ---cut--- import { ORPCError, os } from '@orpc/server' const rateLimitMiddleware = os.middleware(async ({ next }) => { throw new ORPCError('RATE_LIMITED', { message: 'You are being rate limited', data: { retryAfter: 60 } }) return next() }) const example = os .use(rateLimitMiddleware) .handler(async ({ input }) => { if (notFound) { throw new ORPCError('NOT_FOUND') } }) ``` ## Typesafe Errors For end-to-end type safety, define your errors with `.errors` or [return `ORPCError`](#returning-an-orpcerror). This lets the client infer each error's shape and handle it safely. You can use any [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library to validate error data. ::: danger `message` and `data` are sent to the client. Do not include sensitive information in either field. ::: ```ts twoslash import { os } from '@orpc/server' import * as z from 'zod' declare const notFound: boolean // ---cut--- const rateLimitMiddleware = os .errors({ RATE_LIMITED: { data: z.object({ retryAfter: z.number(), }), }, }) .middleware(async ({ next, errors }) => { throw errors.RATE_LIMITED({ message: 'You are being rate limited', data: { retryAfter: 60 } }) return next() }) const exampleProcedure = os .use(rateLimitMiddleware) .errors({ NOT_FOUND: { message: 'The resource was not found', // <- default message }, }) .handler(async ({ input, errors }) => { if (notFound) { throw errors.NOT_FOUND() } }) ``` ::: tip You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like `UNAUTHORIZED` or `RATE_LIMITED`, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance. ::: ### ORPCError Compatibility If you cannot access the `errors` object, for example in a utility function or another module, you can still throw `ORPCError`. oRPC will try to convert it to the matching typesafe error when its `code` and `data` match a defined error. If no match is found, it is treated as an unknown error. ```ts const exampleProcedure = os .errors({ NOT_FOUND: { message: 'The resource was not found', }, }) .handler(async ({ errors }) => { throw errors.NOT_FOUND() // Treated as errors.NOT_FOUND because the code and data match throw new ORPCError('NOT_FOUND') // Treated as an unknown error because it does not match any defined error throw new ORPCError('BAD_REQUEST') }) ``` ### Returning an `ORPCError` As an alternative to `.errors`, you can return an `ORPCError` directly from your handler or middleware to achieve end-to-end type safety. ::: warning When [implementing a contract](/docs/contract/implementation), returning an `ORPCError` is equivalent to throwing one. ::: ```ts const exampleProcedure = os .handler(async ({ errors }) => { if (reachRateLimit) { return new ORPCError('RATE_LIMITED', { message: 'You are being rate limited', data: { retryAfter: 60 } }) } return 'Success' }) ``` ::: danger `message` and `data` are sent to the client. Do not include sensitive information in either field. ::: ## ORPC Error Codes By default, oRPC allows any string as an error code and suggests common HTTP codes like `NOT_FOUND` and `UNAUTHORIZED`. You can override this with your own set of allowed error codes for better type safety and consistency. ```ts declare module '@orpc/server' { // or '@orpc/client' interface Registry { ORPCErrorCode: 'NOT_FOUND' | 'UNAUTHORIZED' | 'RATE_LIMITED' | 'MY_CUSTOM_ERROR' | (string & {}) } } ``` With this configuration, only `NOT_FOUND`, `UNAUTHORIZED`, `RATE_LIMITED`, and `MY_CUSTOM_ERROR` will be suggested as error codes. The `(string & {})` fallback ensures you can still use any string value when needed. ## Using Custom Error Classes You do not have to use `ORPCError` directly in your business logic. You can throw your own error classes and convert them to `ORPCError` in middleware or interceptors. ::: info By default, oRPC can convert non-`ORPCError` into an `ORPCError` with code `INTERNAL_SERVER_ERROR`, or leave them unchanged depending on the client you are using. ::: ```ts class MyCustomError extends Error { } const customErrorConverterMiddleware = os.middleware(async ({ next }) => { try { return await next() } catch (err) { if (err instanceof MyCustomError) { throw new ORPCError('MY_CUSTOM_ERROR', { message: err.message, cause: err }) } throw err } }) ``` ## Client Error Handling To learn how to handle errors on the client side, see the [Client Error Handling documentation](/docs/client/error-handling). --- --- url: /docs/binary-data.md --- # Binary Data [File](https://developer.mozilla.org/en-US/docs/Web/API/File), [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob), and [ReadableStream\](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) are supported by the [RPC Serializer](/docs/rpc/serializer) and [OpenAPI Serializer](/docs/openapi/serializer). Use them to handle binary data in your procedures. ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## `File` and `Blob` Procedures can accept `File` and `Blob` as input and return them directly or inside nested structures. ::: warning `File` and `Blob` are usually buffered in memory by default. For large files, we recommend [extending the body parser](/docs/advanced/extend-body-parser) for better performance and reliability. ::: ```ts twoslash import { os } from '@orpc/server' import * as z from 'zod' // ---cut--- const example = os .input(z.file()) .output(z.object({ anyFieldName: z.instanceof(File) })) .handler(async ({ input }) => { const file = input console.log(file.name) return { anyFieldName: new File(['Hello World'], 'hello.txt', { type: 'text/plain' }), } }) ``` ## `ReadableStream` Procedures can return `ReadableStream` to stream binary responses. The example below uses the [Response Headers Plugin](/docs/plugins/response-headers) to set the appropriate `Content-Type` header. ```ts twoslash import { os } from '@orpc/server' import { ResponseHeadersHandlerPluginContext } from '@orpc/server/plugins' import * as z from 'zod' interface ServerContext extends ResponseHeadersHandlerPluginContext {} const base = os.$context() // ---cut--- const example = base .output(z.instanceof(ReadableStream)) .handler(async ({ context }) => { context.resHeaders?.set('Content-Type', 'text/plain') const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('Hello World')) controller.close() } }) return stream }) ``` --- --- url: /docs/event-iterator.md --- # Event Iterator (SSE) Event Iterator enables **typesafe**, **realtime data streaming**. It is the recommended approach for building features like live notifications, chat messages, progress updates, and data feeds. ## Overview An event iterator is implemented as an [asynchronous generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) (or a compatible implementation). In the example below, the handler emits a new event every second: ```ts const example = os .handler(async function* ({ input, signal, lastEventId }) { while (true) { signal?.throwIfAborted() yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } }) ``` ::: info Learn how to consume event iterators from the client in the [client guide](/docs/client/event-iterator). ::: ## Validating Events Use the built‑in `eventIterator` helper that works with any [Standard Schema](https://standardschema.dev/schema#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, signal, lastEventId }) { while (true) { signal?.throwIfAborted() yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } }) ``` ## Last Event ID & Event Metadata Using the `withEventMeta` helper, you can attach [additional event metadata](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) (such as an event ID or retry interval) to each event. When the client reconnects properly, the last received event ID is sent back to the server in `lastEventId`, allowing the stream to resume from where it left off. ::: info When used with the [Retry Plugin](/docs/plugins/retry) or [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), reconnection with the last event ID is handled automatically. ::: ```ts import { withEventMeta } from '@orpc/server' const example = os .handler(async function* ({ input, signal, lastEventId }) { if (lastEventId) { // Resume streaming from lastEventId } else { while (true) { signal?.throwIfAborted() yield withEventMeta( { message: 'Hello, world!' }, { id: 'some-id', retry: 10_000 } ) await new Promise(resolve => setTimeout(resolve, 1000)) } } }) ``` ## Stop Event Iterator To end the stream, use either a `return` or `throw` statement. oRPC marks the stream as completed when the handler returns. :::warning This behavior is specific to oRPC. Standard [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) clients, such as [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), do not recognize this completion signal and will automatically attempt to reconnect. For details, see the [Standard Server documentation](https://github.com/middleapi/standardserver#event-stream-body). ::: ```ts const example = os .handler(async function* ({ input, signal, lastEventId }) { while (true) { signal?.throwIfAborted() if (done) { return } yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } }) ``` ## Signal and Side-Effects When the client closes the connection or an unexpected error occurs, oRPC aborts the provided `signal`. Use it to exit loops and avoid resource leaks. Put cleanup logic in a `finally` block so it runs whether the stream ends normally, errors, or is cancelled. ```ts const example = os .handler(async function* ({ input, signal, lastEventId }) { try { while (true) { signal?.throwIfAborted() yield { message: 'Hello, world!' } await new Promise(resolve => setTimeout(resolve, 1000)) } } finally { console.log('Cleanup logic here') } }) ``` ## Publisher Helper You can combine the event iterator with the [Publisher Helper](/docs/helpers/publisher) to build real-time features like chat, notifications, or live updates with resume support. ```ts const publisher = new MemoryPublisher<{ 'something-updated': { id: string } }>() const live = os .handler(async function* ({ input, signal, lastEventId }) { const iterator = publisher.subscribe('something-updated', { signal, lastEventId }) for await (const payload of iterator) { // Handle payload here or yield directly to client yield payload } }) const publish = os .input(z.object({ id: z.string() })) .handler(async ({ input }) => { await publisher.publish('something-updated', { id: input.id }) }) ``` --- --- url: /docs/metadata.md --- # Metadata Metadata lets you attach extra information to procedures. Middleware, plugins, and tooling can read it later to control behavior. ## Quickly Define Meta In most cases, use `defineMeta` to create a metadata plugin. It takes a unique name and a merge function that defines how metadata is combined across repeated calls, then returns a tuple of `[metaPlugin, getMeta]`: ```ts twoslash import { os } from '@orpc/server' declare const store: Map // ---cut--- import { defineMeta } from '@orpc/server' type CacheMeta = boolean const [cacheMeta, getCacheMeta] = defineMeta( // [!code highlight] 'cache', // [!code highlight] (incoming: CacheMeta, current) => incoming, // [!code highlight] ) // [!code highlight] const base = os.use(async ({ procedure, next, path }, input, done) => { if (getCacheMeta(procedure) !== true) { // [!code highlight] return next() } const key = path.join('/') + JSON.stringify(input) if (store.has(key)) { return done({ output: store.get(key)! }) } const result = await next() store.set(key, result.output) return result }) const cachedProcedure = base .meta(cacheMeta(true)) // [!code highlight] .handler(async () => { return 'Earth' }) ``` ## Manually Define Meta If `defineMeta` is not flexible enough, define a plugin directly with `MetaPlugin`. This gives you full control and lets the plugin infer or restrict procedure types. ```ts twoslash import { os } from '@orpc/server' import z from 'zod' // ---cut--- import type { AnySchema, ErrorMap, InferSchemaInput, InferSchemaOutput, Meta, MetaPlugin, } from '@orpc/server' interface ExampleMeta< TInputSchema extends AnySchema, TOutputSchema extends AnySchema, TErrorMap extends ErrorMap > { inputExamples?: InferSchemaInput[] outputExamples?: InferSchemaOutput[] } interface ExampleMetaPlugin< TInputSchema extends AnySchema, TOutputSchema extends AnySchema, TErrorMap extends ErrorMap > extends MetaPlugin { name: 'example' } function exampleMeta< TInputSchema extends AnySchema, TOutputSchema extends AnySchema, TErrorMap extends ErrorMap, >( incoming: ExampleMeta ): ExampleMetaPlugin { return { name: 'example', apply(meta) { const current = meta.example as ExampleMeta | undefined return { ...meta, example: { ...current, ...incoming, } } }, } } function getExampleMeta( procedureOrLazy: { '~orpc': { meta: Meta } } ): ExampleMeta | undefined { return procedureOrLazy['~orpc'].meta.example as ExampleMeta | undefined } const procedure = os .input(z.object({ name: z.string() })) .output(z.object({ id: z.string(), name: z.string() })) .meta(exampleMeta({ inputExamples: [{ name: 'Alice' }], // <- typesafe outputExamples: [{ id: '1', name: 'Alice' }], // <- typesafe })) .handler(async ({ input }) => { return { id: '1', name: 'Alice' } }) ``` --- --- url: /docs/playgrounds.md --- # 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/middleapi/orpc/tree/main/playgrounds/next) | [View Source](https://github.com/middleapi/orpc/tree/main/playgrounds/next) | :::warning StackBlitz has its own limitations, so some features may not work as expected. ::: ## Local Development Prefer working locally? Clone the playground with: ```bash npx degit middleapi/orpc/playgrounds/next orpc-next-playground ``` Then install dependencies and start the dev server: ```bash # Install dependencies npm install # Start the development server npm run dev ``` * Visit `http://localhost:3000` to view the app. * Visit `http://localhost:3000/api` to explore the OpenAPI client. ### OpenTelemetry Collect OpenTelemetry traces with [Jaeger](https://www.jaegertracing.io/) by running this in a separate terminal: ```bash npm run jaeger:run ``` Then play with your app and open `http://localhost:16686` to see the traces in the Jaeger dashboard. --- --- url: /docs/rpc/protocol.md --- # RPC Protocol The RPC protocol is a lightweight protocol for remote procedure calls. It supports more native types than plain JSON and is used by [RPC Handler](/docs/rpc/handler) and [RPC Link](/docs/rpc/link). ## Serializer Most of the protocol's flexibility comes from its serializer. In addition to JSON-compatible values, it supports native types such as `Date`, `BigInt`, `RegExp`, `URL`, `Set`, `Map`, `Blob`, `File`, `AsyncIteratorObject`, and `ReadableStream`. To learn more, including how to extend it, see [RPC Serializer](/docs/rpc/serializer). ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Routing The request `pathname` identifies which procedure to call. ```bash curl https://example.com/rpc/planet/create ``` This calls the `planet.create` procedure when `/rpc` is the prefix: ```ts const router = { planet: { create: os.handler(() => {}) // [!code highlight] } } ``` ## Sending Input You can use any HTTP method. Send input in the query string or request body, depending on the method. ::: info Request payloads depend on the serializer and are not plain JSON. Learn more in [RPC Serializer Format](/docs/rpc/serializer#serialization-format). ::: ### Query String ```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: [['date', 'detached_at']] })) const response = await fetch(url) ``` ### 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": [["date", "detached_at"]] }' ``` ### With Files ```ts const form = new FormData() form.set('data', JSON.stringify({ json: { name: 'Earth', thumbnail: {}, images: [{}], }, maps: [['thumbnail'], ['images', 0]] })) 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": [["bigint", "id"], ["date", "detached_at"]] } ``` A successful response uses an HTTP status code in the `200-299` range and returns the procedure output. ::: info Response bodies depend on the serializer and are not plain JSON. Learn more in [RPC Serializer Format](/docs/rpc/serializer#serialization-format). ::: ## Error Response ```http HTTP/1.1 500 Internal Server Error Content-Type: application/json { "json": { "defined": false, "inferable": false, "code": "INTERNAL_SERVER_ERROR", "message": "Internal server error", "data": { "id": "1234567890" } }, "meta": [["bigint", "data", "id"]] } ``` An error response uses an HTTP status code in the `400-599` range and returns an `ORPCError` object. ::: info Response bodies depend on the serializer and are not plain JSON. Learn more in [RPC Serializer Format](/docs/rpc/serializer#serialization-format). ::: --- --- url: /docs/rpc/serializer.md --- # RPC Serializer RPC Serializers handle the serialization and deserialization of data sent between the client and server. They allow you to support complex data types beyond plain JSON, such as `Date`, `BigInt`, `Set`, and even custom classes. ## Supported Data Types `RPCSerializer` supports the following types by default: | Type | Handler key | Notes | | ---------------------------------------- | ----------- | ----------------------------- | | **string** | | | | **number** | | | | **NaN** | `nan` | | | **boolean** | | | | **null** | | | | **undefined** | `undefined` | Ignore `undefined` properties | | **Date** | `date` | Includes `Invalid Date`. | | **BigInt** | `bigint` | | | **RegExp** | `regexp` | | | **URL** | `url` | | | **Record (object)** | | `toJSON` methods are ignored | | **Array** | | | | **Set** | `set` | | | **Map** | `map` | | | **Blob** | | Unsupported in Event Iterator | | **File** | None | Unsupported in Event Iterator | | **Event Iterator (AsyncIteratorObject)** | | Only at the root level | | **ReadableStream\** | | Only at the root level | ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Custom Serializers Add custom handlers with unique keys to support additional types, or reuse a built-in key to override the default behavior. ```ts twoslash class Person { constructor( public name: string, public age: number, ) {} } // ---cut--- import { RPCSerializer } from '@orpc/client' const serializer = new RPCSerializer({ handlers: { person: { // <- add support for Person condition: v => v instanceof Person, serialize: (v: Person) => ({ name: v.name, age: v.age }), deserialize: v => new Person(v.name, v.age), }, date: { // <- replace the default Date handler condition: v => v instanceof Date, serialize: (v: Date) => v.getTime(), deserialize: v => new Date(v), }, }, }) ``` ::: info Use a custom serializer with RPCHandler and RPCLink ```ts const handler = new RPCHandler(router, { serializer, }) const link = new RPCLink({ serializer, }) ``` ::: ## Serialization Format In most cases, serialized data includes two optional fields: `json` and `meta`. `json` contains JSON-serializable data. `meta` contains the metadata needed to deserialize values. ::: info `meta` is stored in the format `[handler: string, ...path: (string | number)[]]`. * **handler**: The handler key used for serialization (see [Supported Data Types](#supported-data-types)). * **path**: Path to the value inside `json`. ::: ```json { "json": { "name": "John", "age": 30, "createdAt": "2024-01-01T00:00:00.000Z" }, "meta": [ ["date", "createdAt"] ] } ``` ### With Files If the data includes `Blob` or `File`, the serializer returns a `FormData` object. The `data` field contains a JSON string with `json`, `meta`, and `maps`, and the remaining fields contain the file parts. ::: info `maps` is stored in the format `[...path: (string | number)[]]`, and its order corresponds to the file parts in the `FormData`. For example, `[['thumbnail'], ['images', 0]]` means the first file part corresponds to `json.thumbnail` at `form.get('0')`, and the second file part corresponds to `json.images[0]` at `form.get('1')`. ::: ```ts const form = new FormData() form.set('data', JSON.stringify({ json: { name: 'Earth', thumbnail: {}, images: [{}], createdAt: '2022-01-01T00:00:00.000Z' }, meta: [['date', 'createdAt']], maps: [['thumbnail'], ['images', 0]] })) form.set('0', new Blob([''], { type: 'image/png' })) form.set('1', new Blob([''], { type: 'image/png' })) ``` ### Direct File If the entire data is a single `Blob` or `File`, it can be sent as-is without wrapping in `FormData`. ```http HTTP/1.1 200 OK Content-Type: image/png Content-Disposition: attachment; filename="earth.png" Content-Length: 12345 Standard-Server: file ``` ::: info If the receiver mistakenly handles this payload as a regular (non-file) body, set the `standard-server` header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the [Standard Server Documentation](https://github.com/middleapi/standardserver#resolving-body). ::: ### Event Iterator (AsyncIteratorObject) When the output is an event iterator (`AsyncIteratorObject`), it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data. ```http HTTP/1.1 200 OK Content-Type: text/event-stream event: message data: {"json":{"name":"John","createdAt":"2024-01-01T00:00:00.000Z"},"meta":[["date","createdAt"]]} event: message data: {"json":{"name":"Jane","createdAt":"2024-01-02T00:00:00.000Z"},"meta":[["date","createdAt"]]} ``` ### ReadableStream\ A `ReadableStream` is passed through as-is and streamed as binary data. ```http HTTP/1.1 200 OK Content-Type: application/octet-stream Standard-Server: octet-stream ``` ::: info If the receiver mistakenly handles this payload as a regular (non-stream) body, set the `standard-server` header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the [Standard Server Documentation](https://github.com/middleapi/standardserver#resolving-body). ::: ## Learn More The serializer is a small, self-contained module, making it easy to understand. To explore its behavior in detail, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/rpc-serializer.ts). --- --- url: /docs/rpc/handler.md --- # RPC Handler Use `RPCHandler` to communicate with [RPC Link](/docs/rpc/link) and other clients that implement the [RPC protocol](/docs/rpc/protocol). ## Overview ```ts const handler = new RPCHandler(router, { interceptors: [ async ({ next, path }) => { console.time(path.join('.')) try { return await next() } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } } ], plugins: [ new CORSHandlerPlugin() ], }) ``` ::: info The actual usage of `RPCHandler` depends on the adapter you use. For example, when using the fetch adapter, the handler is used like this: ```ts export async function fetch(request: Request) { const { response } = await handler.fetch(request, { prefix: '/rpc', context: {} // <- provide initial context if needed }) return response ?? new Response('Not Found', { status: 404 }) } ``` ::: ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Interceptors Interceptors let you observe or change different stages of an RPC request. Common use cases include logging, error handling, and metrics. ### Routing Interceptors Routing interceptors run on every request before routing. Use them when you need to handle all requests, including requests that do not match a procedure. ```ts const handler = new RPCHandler(router, { routingInterceptors: [ async ({ next, request, context }) => { if (condition) { return { matched: false } } const { matched, response } = await next() return { matched, response } }, ], }) ``` ### Interceptors These interceptors run only for matched requests, after routing and before error handling (but can't use `ORPCError` for [typesafe errors](/docs/error-handling#orpcerror-compatibility)). Use them when you need access to the matched procedure. ::: tip In most cases, `interceptors` are the best choice. They provide more context, are easier to work with, and run before error handling. ::: ```ts const handler = new RPCHandler(router, { interceptors: [ async ({ next, request, procedure, context }) => { try { const response = await next() return response } catch (err) { if (err instanceof CustomError) { throw new ORPCError('CUSTOM_ERROR', { message: err.message, cause: err }) } throw err } }, async ({ next, path }) => { console.time(path.join('.')) try { const response = await next() return response } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } }, ], }) ``` ### Client Interceptors Client interceptors run only for matched requests, after input decoding, before output encoding and can use `ORPCError` for [typesafe errors](/docs/error-handling#orpcerror-compatibility). Use them when you need access to the procedure, input, and output. ```ts const handler = new RPCHandler(router, { clientInterceptors: [ async ({ next, input, context, procedure }) => { const output = await next() return output }, ], }) ``` ### Adapter Interceptors Some `RPCHandler` implementations, such as fetch or node adapters, also support adapter interceptors. These run before [Routing Interceptors](#routing-interceptors) and let you work with the adapter's native request and response objects. ```ts const handler = new RPCHandler(router, { fetchInterceptors: [ async ({ next, request }) => { const { matched, response } = await next() return { matched, response } }, ], }) ``` ::: info This example uses the fetch adapter. For other adapters, refer to their JSDoc or adapter-specific documentation. ::: ## Plugins Plugins package reusable interceptors. For example, [CORS Plugin](/docs/plugins/cors) adds a [routing interceptor](#routing-interceptors) to handle preflight requests and adds CORS headers to every response. ```ts const handler = new RPCHandler(router, { plugins: [ new CORSHandlerPlugin() ], }) ``` ::: info HTTP-based `RPCHandler` implementations enable the [CSRF Guard Plugin](/docs/plugins/csrf-guard) by default to protect RPC requests from CSRF attacks. Disable it with `csrfGuardHandlerPlugin.enabled`. ```ts const handler = new RPCHandler(router, { csrfGuardHandlerPlugin: { enabled: false, }, }) ``` ::: ## Custom Serializer `RPCHandler` uses a built-in serializer that supports many native types. Provide a custom serializer when you need extra types or different encoding behavior. For more details, see [RPC Serializer](/docs/rpc/serializer). ```ts const handler = new RPCHandler(router, { serializer: new RPCSerializer({ handlers: { // ...custom handlers }, }), }) ``` ## Filtering Procedures Use the `filter` option to exclude procedures from matching: ```ts const handler = new RPCHandler(router, { filter: (contract, path) => getIsInternalMeta(contract) !== true, }) ``` ## Custom Error Response By default, `RPCHandler` uses `COMMON_ERROR_STATUS_MAP` to determine response status codes. Use `errorStatusMap` to customize them: ```ts import { COMMON_ERROR_STATUS_MAP } from '@orpc/server' const handler = new RPCHandler(router, { errorStatusMap: { ...COMMON_ERROR_STATUS_MAP, CUSTOM_ERROR: 599, }, }) ``` ::: details Common Error Status Map | Error Code | HTTP Status Code | | ---------------------- | ---------------: | | BAD\_REQUEST | 400 | | UNAUTHORIZED | 401 | | PAYMENT\_REQUIRED | 402 | | FORBIDDEN | 403 | | NOT\_FOUND | 404 | | METHOD\_NOT\_SUPPORTED | 405 | | NOT\_ACCEPTABLE | 406 | | TIMEOUT | 408 | | CONFLICT | 409 | | GONE | 410 | | PRECONDITION\_FAILED | 412 | | PAYLOAD\_TOO\_LARGE | 413 | | UNSUPPORTED\_MEDIA\_TYPE | 415 | | UNPROCESSABLE\_CONTENT | 422 | | PRECONDITION\_REQUIRED | 428 | | TOO\_MANY\_REQUESTS | 429 | | CLIENT\_CLOSED\_REQUEST | 499 | | INTERNAL\_SERVER\_ERROR | 500 | | NOT\_IMPLEMENTED | 501 | | BAD\_GATEWAY | 502 | | SERVICE\_UNAVAILABLE | 503 | | GATEWAY\_TIMEOUT | 504 | ::: ## Event Stream Options Configure how [event iterators](/docs/event-iterator) are streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const handler = new RPCHandler(router, { toFetchResponse: { eventStream: { initialComment: { /** * If true, an initial comment is sent immediately upon stream start to flush headers. * This allows the receiving side to establish the connection without waiting for the first event. * * @default true */ enabled: true, /** * The content of the initial comment sent upon stream start. Must not include newline characters. * * @default '' */ comment: '', }, keepAlive: { /** * If true, a ping comment is sent periodically to keep the connection alive. * * @default true */ enabled: true, /** * Interval (in milliseconds) between ping comments sent after the last event. * * @default 5000 */ interval: 5000, /** * The content of the ping comment. Must not include newline characters. * * @default '' */ comment: '', }, /** * If true, a `close` event is sent even when the iterator completes with `undefined`. * When the iterator returns a value, a `close` event is always emitted regardless of this setting. * * @default true */ emptyCloseEventEnabled: true, }, }, }) ``` ## Lifecycle TODO: add lifecycle diagram --- --- url: /docs/rpc/link.md --- # RPC Link Use `RPCLink` to communicate with [RPC Handler](/docs/rpc/handler) and other servers that implement the [RPC protocol](/docs/rpc/protocol). ## Overview ```ts const link = new RPCLink({ origin: 'https://example.com', url: '/rpc', headers: ({ context }) => ({ authorization: `Bearer ${token}`, }), interceptors: [ async ({ next, path }) => { console.time(path.join('.')) try { return await next() } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } }, ], plugins: [ new RetryAfterLinkPlugin(), ], fetch: (request, init) => { // <- only available in fetch adapter return globalThis.fetch(request, { ...init, credentials: 'include', // Include cookies on cross-origin requests }) }, }) export const client = createORPCClient(link) ``` ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Typesafe Clients After you create an `RPCLink`, pass it to `createORPCClient` to build a typesafe client for either a [contract](/docs/contract/router) or a [router](/docs/router): ```ts import { createORPCClient } from '@orpc/client' import { RouterContractClient } from '@orpc/contract' import { RouterClient } from '@orpc/server' // if you are following contract-first approach const contractClient: RouterContractClient = createORPCClient(link) // if you are following normal approach const normalClient: RouterClient = createORPCClient(link) ``` ## Client Context Client context lets you pass per-call values, such as auth tokens or cache hints. This context is available in link options, interceptors, plugins, and other extensibility points. ```ts interface ClientContext { token?: string } const link = new RPCLink({ headers: ({ context }) => ({ authorization: context?.token ? `Bearer ${context.token}` : undefined, }), interceptors: [ async ({ next, context }) => { console.log('Client context:', context) return await next() }, ], }) ``` ::: info Pass `ClientContext` when creating the client, then provide context on each call as needed: ```ts // if you are using the contract-first approach const client: RouterContractClient = createORPCClient(link) // if you are using the standard approach const client: RouterClient = createORPCClient(link) const output = await client.someProcedure(input, { context: { token: 'abc123', }, }) ``` ::: ## URL and Header Options Use `origin`, `url`, and `headers` to control request destination and headers. * `origin`: Server protocol and domain. Omit in the browser to use the current origin. * `url`: Usually a path prefix like `/api`. May include query params that are added to every request. * `headers`: Headers sent with every request, such as auth or trace IDs. Keys should be lowercase. ```ts const link = new RPCLink({ origin: 'https://api.example.com', url: '/rpc?v=2', headers: { authorization: `Bearer ${getAuthToken()}`, }, }) ``` ::: info Each option can also be a function to dynamically customize values per request. For example, routing to a different `origin` based on the procedure path, or injecting headers from client context: ```ts const link = new RPCLink({ origin: ({ path, context }) => { if (path[0] === 'internal') { return 'https://internal.example.com' } return 'https://api.example.com' }, headers: ({ context }) => ({ authorization: context?.token ? `Bearer ${context.token}` : undefined, }), }) ``` ::: ## Interceptors Interceptors let you observe or change different stages of an RPC request. Common use cases include logging, retries, auth, batching, and transport customization. ### Interceptors Interceptors run around the entire call, including input encoding, transport, and response decoding. Use them when you need access to the path, input, output, or error. ```ts const link = new RPCLink({ interceptors: [ async ({ next, path, input }) => { console.time(path.join('.')) try { const output = await next() return output } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } }, ], }) ``` ### Transport Interceptors Interceptors run after input encoding and before response decoding. Use them to inspect or rewrite the request. ```ts const link = new RPCLink({ transportInterceptors: [ async (options) => { const response = await options.next({ ...options, request: { ...options.request, headers: { ...options.request.headers, 'x-request-id': crypto.randomUUID(), }, }, }) return response }, ], }) ``` ### Adapter Interceptors Some `RPCLink` implementations also support adapter-specific interceptors. The fetch adapter exposes `fetchInterceptors`, which run right before `fetch` and give you access to the final `url` and `RequestInit`. ```ts const link = new RPCLink({ fetchInterceptors: [ async (options) => { const response = await options.next({ ...options, init: { ...options.init, credentials: 'include', }, }) return response }, ], }) ``` ::: info This example uses the fetch adapter. For other adapters, refer to their JSDoc or adapter-specific documentation. ::: ## Plugins Plugins package reusable interceptors. For example, [Retry After Plugin](/docs/plugins/retry-after) adds retry behavior based on the `retry-after` response header. ```ts const link = new RPCLink({ plugins: [ new RetryAfterLinkPlugin(), ], }) ``` ## Custom Serializer `RPCLink` uses a built-in serializer that supports many native types. Provide a custom serializer when you need to extend or override the default behavior. For more details, see [RPC Serializer](/docs/rpc/serializer). ```ts const link = new RPCLink({ serializer: new RPCSerializer({ handlers: { // ...custom handlers }, }), }) ``` ## Request Method `RPCLink` sends requests with `POST` by default. Use `method` to choose the method per call. ```ts type ClientContext = { cache?: RequestCache } const link = new RPCLink({ url: '/rpc', method: ({ context }, path) => { if (context.cache) { return 'GET' } if (path.at(-1)?.match(/^(?:get|find|list|search)(?:[A-Z].*)?$/)) { return 'GET' } return 'POST' }, fetch: (url, init, { context }) => { return fetch(url, { ...init, cache: context.cache, }) }, }) ``` ## Event Stream Options Configure how [event iterators](/docs/event-iterator) are streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const link = new RPCLink({ toFetchBody: { eventStream: { initialComment: { /** * If true, an initial comment is sent immediately upon stream start to flush headers. * This allows the receiving side to establish the connection without waiting for the first event. * * @default true */ enabled: true, /** * The content of the initial comment sent upon stream start. Must not include newline characters. * * @default '' */ comment: '', }, keepAlive: { /** * If true, a ping comment is sent periodically to keep the connection alive. * * @default true */ enabled: true, /** * Interval (in milliseconds) between ping comments sent after the last event. * * @default 5000 */ interval: 5000, /** * The content of the ping comment. Must not include newline characters. * * @default '' */ comment: '', }, /** * If true, a `close` event is sent even when the iterator completes with `undefined`. * When the iterator returns a value, a `close` event is always emitted regardless of this setting. * * @default true */ emptyCloseEventEnabled: true, }, }, }) ``` ## Lifecycle TODO: add lifecycle diagram --- --- url: /docs/openapi/routing.md --- # OpenAPI Routing Use `openapi` metadata to control how a procedure is exposed over HTTP. ## Basic Routing If you do not set OpenAPI routing metadata, a procedure is exposed as a `POST` endpoint whose path is derived from the router structure. For example: ```ts twoslash import { os } from '@orpc/server' // ---cut--- import { openapi } from '@orpc/openapi' const router = { planet: { list: os .meta(openapi({ method: 'GET', path: '/planets' })) .handler(async () => [{ id: 'earth', name: 'Earth' }]), create: os .handler(async () => ({})), } } ``` In this example, `list` is exposed as `GET /planets` because it overrides the default method and path. `create` keeps the default behavior, so it is exposed as `POST /planet/create`. ## Path Parameters To define a path parameter, use `{name}` in the `path` and add the same field as a required key in the input schema: ```ts import { z } from 'zod' const getPlanet = os .meta(openapi({ method: 'GET', path: '/planets/{id}' })) .input(z.object({ id: z.string() })) ``` For catch-all path segments that may include `/`, use `{+name}`: ```ts const getFile = os .meta(openapi({ method: 'GET', path: '/files/{+path}' })) .input(z.object({ path: z.string() })) ``` ::: info To customize path parameter encoding and decoding, see [Path Parameter Styles](/docs/openapi/input-and-output-mapping#path-parameter-styles). ::: ## Prefixes Define `prefix` to prepend a path to a procedure, or an entire router: ```ts const planetBuilder = os.meta(openapi({ prefix: '/planets' })) const listPlanets = planetBuilder .meta(openapi({ method: 'GET', path: '/' })) .handler(async () => [{ id: 'earth', name: 'Earth' }]) const createPlanet = planetBuilder .handler(async () => ({})) const router = os.meta(openapi({ prefix: '/api/v2' })).router({ planet: { list: listPlanets, create: createPlanet, }, }) ``` In this example, `listPlanets` is exposed as `GET /api/v2/planets/`. `createPlanet` is exposed as `POST /api/v2/planets/planet/create`. ### Path Parameters in Prefixes Prefixes can also include path parameters, but they must be defined as required fields in the input schema. ```ts const base = os .meta(openapi({ prefix: '/{workspaceId}' })) .input(z.looseObject({ workspaceId: z.string() })) .use(({ next }, { workspaceId }) => { console.log('Workspace ID:', workspaceId) return next() }) const procedure = base .meta(openapi({ method: 'GET', path: '/planets/{id}' })) .input(z.looseObject({ id: z.string() })) .handler(async ({ input }) => { console.log('Workspace ID:', input.workspaceId) console.log('Planet ID:', input.id) }) ``` ## Lazy Router When using a [lazy router](/docs/router#lazy-router), define a `prefix` so lazy loading is triggered only for relevant requests: ```ts const router = { project: os .meta(openapi({ prefix: '/projects' })) .lazy(() => import('./project')), } ``` ## Metadata Merging When `openapi` is applied multiple times, `prefix` values are concatenated. `method`, `path`, and `successStatus` are overridden by the most recent call. For full merge behavior, see the [source code](https://github.com/orpc/orpc/blob/main/packages/openapi/src/meta.ts). ```ts const router = os .meta(openapi({ prefix: '/api/v2' })) .router({ get: os .meta(openapi({ prefix: '/planets' })) .meta(openapi({ method: 'GET', path: '/planets/{id}' })) .meta(openapi({ path: '/{id}' })) .input(z.object({ id: z.string() })) .handler(async () => ({})), }) ``` These calls are equivalent to: ```ts const router = { get: os .meta(openapi({ prefix: '/api/v2/planets', method: 'GET', path: '/{id}', })) .handler(async () => ({})), } ``` ::: info Metadata resets to its default behavior when set to `undefined` in subsequent calls: ```ts const example = os .meta(openapi({ prefix: '/api/v2' })) .meta(openapi({ prefix: undefined })) ``` In this example, the final `prefix` is `undefined`, so no prefix is applied to `example`. ::: ## Shorthands For common cases, use the shorthand helpers: ```ts const listPlanets = os .meta(openapi.prefix('/planets')) .meta(openapi.method('GET')) .meta(openapi.path('/')) ``` ## `.route` extension Import `@orpc/openapi/extensions/route` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds a `.route` method to the builder, allowing you to define OpenAPI metadata directly without wrapping it in `.meta(openapi(...))`. ::: code-group ```ts [usage] const ping = base .route({ method: 'GET', path: '/ping', }) .input(z.object({ name: z.string(), })) .handler(async ({ input }) => { return `Hello ${input.name}!` }) ``` ```ts [setup] import '@orpc/openapi/extensions/route' import { os } from '@orpc/server' export const base = os ``` ::: --- --- url: /docs/openapi/input-and-output-mapping.md --- # OpenAPI Input and Output Mapping oRPC lets you map OpenAPI requests and responses to procedure inputs and outputs in a few different ways. ## Input Mapping By default, oRPC uses `compact` mode where path parameters are merged with either query parameters or the request body, depending on the HTTP method. ```ts const searchPlanets = os .meta(openapi({ method: 'GET', path: '/planets/{id}' })) .input(z.object({ id: z.string(), q: z.string().optional(), })) .handler(async ({ input }) => { return { id: input.id, q: input.q } }) ``` For `GET /planets/earth?q=life`, the procedure receives: ```json { "id": "earth", "q": "life" } ``` ::: info Some requests cannot be merged into a single object. For example, `POST /planets/earth` with a non-object body cannot be merged. In that case, the full input becomes the body. Use [detailed input structure](#detailed-input-structure) if you also need path params. ::: ### Detailed Input Structure In `detailed` mode, the input is an object with separate `params`, `query`, `headers`, and `body` fields. ```ts const updatePlanet = os .meta(openapi({ method: 'POST', path: '/planets/{id}', inputStructure: 'detailed', })) .input(z.object({ params: z.object({ id: z.string() }), query: z.object({ dryRun: z.coerce.boolean().optional() }).optional(), headers: z.object({ 'x-trace-id': z.string() }).optional(), body: z.object({ name: z.string() }), })) .handler(async ({ input }) => { return input }) ``` For `POST /planets/earth?dryRun=true` with header `x-trace-id: abc123` and body `{ "name": "Earth" }`, the procedure receives: ```json { "params": { "id": "earth" }, "query": { "dryRun": true }, "headers": { "x-trace-id": "abc123" }, "body": { "name": "Earth" } } ``` ::: info You only need to define the fields you want to access. For example, if you only care about path params and the request body, your input schema can include just `params` and `body`. ::: ### Path Parameter Styles By default, path parameters are decoded as plain strings. Use `paramsStyles` to override how each path parameter is encoded and decoded. ```ts const getPlanets = os .meta(openapi({ method: 'GET', path: '/planets/{ids}/{filters}', paramsStyles: { ids: 'comma-delimited-array', filters: 'comma-delimited-object', }, })) .input(z.object({ ids: z.array(z.string()), filters: z.object({ type: z.string(), status: z.string(), }), })) .handler(async () => []) ``` Supported path parameter styles: | Style | Example path segment | Decoded value | | ------------------------ | ---------------------------------- | ------------------------------------------------- | | `primitive` *(default)* | `/planets/earth` | `{ id: 'earth' }` | | `comma-delimited-array` | `/planets/earth,mars` | `{ ids: ['earth', 'mars'] }` | | `comma-delimited-object` | `/planets/type,rocky,status,known` | `{ filters: { type: 'rocky', status: 'known' } }` | ::: warning When using delimited styles, do not use delimiter characters like `,` in keys or values. They can make the parameter ambiguous. ::: ### Query Styles By default, query parameters are decoded with [bracket notation](/docs/openapi/bracket-notation). Use `queryStyles` to override how each query parameter is encoded and decoded. ```ts const searchPlanets = os .meta(openapi({ method: 'GET', path: '/planets', queryStyles: { keyword: 'primitive', tags: 'comma-delimited-array', filters: 'comma-delimited-object', meta: 'json', }, })) .handler(async () => []) ``` Supported query styles: | Style | Example | Decoded value | | ------------------------ | ----------------------------------------------- | --------------------------------------------------------- | | `primitive` | `?tag=a&tag=b` | `{ tag: 'b' }` | | `array` | `?tag=a&tag=b` | `{ tag: ['a', 'b'] }` | | `comma-delimited-array` | `?tags=red,blue` | `{ tags: ['red', 'blue'] }` | | `comma-delimited-object` | `?filter=size,large,brand,nike` | `{ filter: { size: 'large', brand: 'nike' } }` | | `space-delimited-array` | `?tags=red blue` | `{ tags: ['red', 'blue'] }` | | `space-delimited-object` | `?filter=size large brand nike` | `{ filter: { size: 'large', brand: 'nike' } }` | | `pipe-delimited-array` | `?tags=red\|blue` | `{ tags: ['red', 'blue'] }` | | `pipe-delimited-object` | `?filter=size\|large\|brand\|nike` | `{ filter: { size: 'large', brand: 'nike' } }` | | `json` | `?meta={"enabled":true}` | `{ meta: { enabled: true } }` | | *default* | `?tags[]=red&tags[]=blue&filter[status]=active` | `{ tags: ['red', 'blue'], filter: { status: 'active' } }` | ::: warning When using delimited styles, do not use delimiter characters like `,`, ` `, or `|` in keys or values. They can make the parameter ambiguous. ::: ## Output Mapping By default, oRPC uses `compact` mode. The procedure's return value becomes the response body, and the status code comes from `successStatus`, which defaults to `200`. ```ts const getPlanet = os .meta(openapi({ method: 'GET', path: '/planets', successStatus: 200 })) .handler(async () => { return { id: 'earth', name: 'Earth' } }) ``` ### Detailed Output Structure In `detailed` mode, return an object with the following fields: * `status`: optional success status code *(defaults to `successStatus`)* * `headers`: optional response headers in lower-case keys * `body`: optional response body ```ts const savePlanet = os .meta(openapi({ method: 'PUT', path: '/planets/{id}', outputStructure: 'detailed', successStatus: 200, })) .input(z.object({ id: z.string() })) .output(z.union([ z.object({ status: z.literal(201).meta({ description: 'Created' }), body: z.object({ id: z.string(), name: z.string() }), }), z.object({ status: z.literal(200).meta({ description: 'Updated' }), body: z.object({ id: z.string(), name: z.string() }), }), ])) .handler(async ({ input }) => { if (!isExistingPlanet(input.id)) { return { status: 201, headers: { 'x-created': 'true' }, body: { id: 'earth', name: 'Earth' }, } } return { body: { id: 'earth', name: 'Earth' }, } }) ``` ## Body Hints The body parser normally uses `Content-Type`, `Content-Length`, `Content-Disposition`, and `Standard-Server` headers to decide how to parse the body. If that information is missing or misleading, use `requestBodyHint` to tell [OpenAPI Handler](/docs/openapi/handler) how to parse the request body. Likewise, use `responseBodyHint` to tell [OpenAPI Link](/docs/openapi/link) how to parse the response body. ```ts const uploadLargeFile = os .meta(openapi({ requestBodyHint: 'octet-stream', responseBodyHint: 'json', })) .input(z.instanceof(ReadableStream)) .handler(async ({ input }) => { for await (const chunk of input) { // process chunk } return { ok: true } }) ``` Supported body hints: | Hint | Parsed Result | | ------------------- | --------------------------------------------------------------------------------- | | `json` | JSON value | | `form-data` | `FormData` decoded with [bracket notation](/docs/openapi/bracket-notation) | | `url-search-params` | `URLSearchParams` decoded with [bracket notation](/docs/openapi/bracket-notation) | | `event-stream` | [Event Iterator](/docs/event-iterator) | | `octet-stream` | `ReadableStream` for streamed binary data | | `file` | `File` for binary data | | `none` | `undefined` | ::: info Learn more about body hints in the [Standard Server documentation](https://github.com/middleapi/standardserver#standard-body) ::: ## Metadata Merging When `openapi` is applied multiple times, `paramsStyles` and `queryStyles` are spreading-merged, while `inputStructure`, `outputStructure`, `responseBodyHint`, and `requestBodyHint` are overridden by the most recent call. For full merge behavior, see the [source code](https://github.com/orpc/orpc/blob/main/packages/openapi/src/meta.ts). ```ts const router = os .meta(openapi({ inputStructure: 'detailed' })) .router({ get: os .meta(openapi({ method: 'GET', path: '/planets', inputStructure: 'compact' })) .meta(openapi({ queryStyles: { tags: 'comma-delimited-array' } })) .meta(openapi({ queryStyles: { q: 'primitive' } })) .input(z.object({ tags: z.array(z.string()), q: z.string().optional() })) .handler(async () => ([])), }) ``` These are equivalent to: ```ts const router = { get: os .meta(openapi({ method: 'GET', path: '/planets', inputStructure: 'compact', queryStyles: { tags: 'comma-delimited-array', q: 'primitive', }, })) .input(z.object({ tags: z.array(z.string()), q: z.string().optional() })) .handler(async () => ([])), } ``` ::: info Metadata resets to its default behavior when set to `undefined` in subsequent calls: ```ts const example = os .meta(openapi({ queryStyles: { tags: 'comma-delimited-array' } })) .meta(openapi({ queryStyles: undefined })) ``` In this example, the final `queryStyles` is `undefined`, so query parameters are parsed with the default bracket notation. ::: --- --- url: /docs/openapi/bracket-notation.md --- # Bracket Notation Bracket notation encodes structured data in flat key-value formats such as query strings and form data. [OpenAPI Serializer](/docs/openapi/serializer), [OpenAPI Handler](/docs/openapi/handler), and [OpenAPI Link](/docs/openapi/link) use it whenever nested data must be represented outside plain JSON. ## Rules 1. **Repeated keys become arrays.** ``` color=red&color=blue -> { color: ['red', 'blue'] } ``` 2. **Append `[]` to push into an array.** ``` color[]=red&color[]=blue -> { color: ['red', 'blue'] } ``` 3. **Append `[number]` to target an explicit array index.** ``` color[0]=red&color[2]=blue -> { color: ['red', , 'blue'] } ``` ::: info Missing indexes create sparse arrays. Explicit indexes greater than `999` are treated as object keys by default to avoid huge sparse arrays during deserialization. To change that limit, configure `maxExplicitDeserializingArrayIndex`: ```ts const serializer = new OpenAPISerializer({ bracketNotation: { maxExplicitDeserializingArrayIndex: 1999, } }) ``` ::: 4. **Append `[key]` to target an object property.** ``` color[red]=true&color[blue]=false -> { color: { red: 'true', blue: 'false' } } ``` ## Limitations Bracket notation is designed to express structured data in constrained environments, so it has a few unavoidable limitations: * Cannot represent empty structures like empty objects `{}` or empty arrays `[]`. * Cannot represent an array at the root level. For example, `0=red&1=blue` becomes `{ 0: 'red', 1: 'blue' }`, not `['red', 'blue']`. * Cannot represent objects whose keys are all numbers, because they can be mistaken for array indexes. ::: info If bracket notation is used in query strings or form data, it also inherits the limitations of those formats. For example, values are always strings or files, and `null` or `undefined` cannot be represented. ::: ## 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": ["", ""] } } ``` ## Learn More The bracket notation is a small, self-contained module, making it easy to understand. To explore its behavior in detail, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/openapi/src/bracket-notation.ts). --- --- url: /docs/openapi/serializer.md --- # OpenAPI Serializer OpenAPI Serializers handle one-way serialization to JSON-friendly formats. They let you partially support complex data types beyond plain JSON, such as `Date`, `BigInt`, `Set`, and even custom classes. ## Supported Data Types `OpenAPISerializer` supports the following types by default: | Type | Handler key | Serialized | Notes | | ---------------------------------------- | ----------- | ------------------ | ----------------------------- | | **string** | | | | | **number** | | | | | **NaN** | `nan` | `null` | | | **boolean** | | | | | **null** | | | | | **undefined** | `undefined` | `null` | Ignore `undefined` properties | | **Date** | `date` | ISO String, `null` | | | **BigInt** | `bigint` | string | | | **RegExp** | `regexp` | string | | | **URL** | `url` | string | | | **Record (object)** | | | `toJSON` methods are ignored | | **Array** | | | | | **Set** | `set` | array | | | **Map** | `map` | array | | | **Blob** | | | Unsupported in Event Iterator | | **File** | | | Unsupported in Event Iterator | | **Event Iterator (AsyncIteratorObject)** | | | Only at the root level | | **ReadableStream\** | | | Only at the root level | ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Limitations OpenAPI Serializers are designed for one-way serialization to JSON-friendly formats. For example, a `Date` is serialized to an ISO string and remains a string after deserialization unless you add custom logic or plugins. In complex cases like mixed files with other data or nested structures in query strings, OpenAPI Serializer might use bracket notation to represent nested data, which has its own limitations. See [Bracket Notation Limitations](/docs/openapi/bracket-notation#limitations) for details. ::: tip If you use [OpenAPI Link](/docs/openapi/link) to connect your client and server, follow [Expanding Type Support for OpenAPI Link](/docs/advanced/expanding-type-support-for-openapi-link) to restore native types on the client. ::: ## Custom Serializers Add custom handlers with unique keys to support additional types, or reuse a built-in key to override the default behavior. ```ts twoslash class Person { constructor( public name: string, public age: number, ) {} } // ---cut--- import { OpenAPISerializer } from '@orpc/openapi' const serializer = new OpenAPISerializer({ handlers: { person: { // <- add support for Person condition: v => v instanceof Person, serialize: (v: Person) => ({ name: v.name, age: v.age }), }, date: { // <- replace the default Date handler condition: v => v instanceof Date, serialize: (v: Date) => v.getTime(), }, }, }) ``` ::: info Use a custom serializer with OpenAPIHandler and OpenAPILink ```ts const handler = new OpenAPIHandler(router, { serializer, }) const link = new OpenAPILink(contract, { serializer, }) ``` ::: ## Serialization Format In most cases, serialized data is JSON-serializable. ```json { "name": "John", "age": 30, "createdAt": "2024-01-01T00:00:00.000Z" } ``` ### With Files If the data includes nested `Blob` or `File`, the serializer returns a `FormData` object using [Bracket Notation](/docs/openapi/bracket-notation). Non-file values are converted to strings, and `null` or `undefined` fields are omitted. ```ts const form = new FormData() form.append('name', 'Earth') form.append('thumbnail', new Blob([''], { type: 'image/png' })) form.append('images[0]', new Blob([''], { type: 'image/png' })) form.append('createdAt', '2022-01-01T00:00:00.000Z') ``` ::: info `images[0]` means the first item in `images` array. ::: ### Direct File If the entire data is a single `Blob` or `File`, it can be sent as-is without wrapping in `FormData`. ```http HTTP/1.1 200 OK Content-Type: image/png Content-Disposition: attachment; filename="earth.png" Content-Length: 12345 Standard-Server: file ``` ::: info If the receiver mistakenly handles this payload as a regular (non-file) body, set the `standard-server` header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the [Standard Server Documentation](https://github.com/middleapi/standardserver#resolving-body). ::: ### Event Iterator (AsyncIteratorObject) When the output is an event iterator (`AsyncIteratorObject`), it is sent as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each event contains one serialized chunk of data. ```http HTTP/1.1 200 OK Content-Type: text/event-stream event: message data: {"name":"John","createdAt":"2024-01-01T00:00:00.000Z"} event: message data: {"name":"Jane","createdAt":"2024-01-02T00:00:00.000Z"} ``` ### ReadableStream\ A `ReadableStream` is passed through as-is and streamed as binary data. ```http HTTP/1.1 200 OK Content-Type: application/octet-stream Standard-Server: octet-stream ``` ::: info If the receiver mistakenly handles this payload as a regular (non-stream) body, set the `standard-server` header to help the receiver detect the actual data type and handle it correctly. Learn more about this header in the [Standard Server Documentation](https://github.com/middleapi/standardserver#resolving-body). ::: ## Learn More The serializer is a small, self-contained module, making it easy to understand. To explore its behavior in detail, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/openapi/src/openapi-serializer.ts). --- --- url: /docs/openapi/handler.md --- # OpenAPI Handler Use `OpenAPIHandler` to expose HTTP endpoints or communicate with [OpenAPI Link](/docs/openapi/link) and other OpenAPI-compliant clients. ## Overview ```ts const handler = new OpenAPIHandler(router, { interceptors: [ async ({ next, path }) => { console.time(path.join('.')) try { return await next() } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } } ], plugins: [ new CORSHandlerPlugin() ], }) ``` ::: info The actual usage of `OpenAPIHandler` depends on the adapter you use. For example, when using the fetch adapter, the handler is used like this: ```ts export async function fetch(request: Request) { const { response } = await handler.fetch(request, { prefix: '/api', context: {} // <- provide initial context if needed }) return response ?? new Response('Not Found', { status: 404 }) } ``` ::: ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Interceptors Interceptors let you observe or change different stages of an OpenAPI request. Common use cases include logging, error handling, and metrics. ### Routing Interceptors Routing interceptors run on every request before routing. Use them when you need to handle all requests, including requests that do not match a procedure. ```ts const handler = new OpenAPIHandler(router, { routingInterceptors: [ async ({ next, request, context }) => { if (condition) { return { matched: false } } const { matched, response } = await next() return { matched, response } }, ], }) ``` ### Interceptors These interceptors run only for matched requests, after routing and before error handling (but can't use `ORPCError` for [typesafe errors](/docs/error-handling#orpcerror-compatibility)). Use them when you need access to the matched procedure. ::: tip In most cases, `interceptors` are the best choice. They provide more context, are easier to work with, and run before error handling. ::: ```ts const handler = new OpenAPIHandler(router, { interceptors: [ async ({ next, request, procedure, context }) => { try { const response = await next() return response } catch (err) { if (err instanceof CustomError) { throw new ORPCError('CUSTOM_ERROR', { message: err.message, cause: err }) } throw err } }, async ({ next, path }) => { console.time(path.join('.')) try { const response = await next() return response } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } }, ], }) ``` ### Client Interceptors Client interceptors run only for matched requests, after input decoding, before output encoding and can use `ORPCError` for [typesafe errors](/docs/error-handling#orpcerror-compatibility). Use them when you need access to the procedure, input, and output. ```ts const handler = new OpenAPIHandler(router, { clientInterceptors: [ async ({ next, input, context, procedure }) => { const output = await next() return output }, ], }) ``` ### Adapter Interceptors Some `OpenAPIHandler` implementations, such as fetch or node adapters, also support adapter interceptors. These run before [Routing Interceptors](#routing-interceptors) and let you work with the adapter's native request and response objects. ```ts const handler = new OpenAPIHandler(router, { fetchInterceptors: [ async ({ next, request }) => { const { matched, response } = await next() return { matched, response } }, ], }) ``` ::: info This example uses the fetch adapter. For other adapters, refer to their JSDoc or adapter-specific documentation. ::: ## Plugins Plugins package reusable interceptors. For example, [CORS Plugin](/docs/plugins/cors) adds a [routing interceptor](#routing-interceptors) to handle preflight requests and adds CORS headers to every response. ```ts const handler = new OpenAPIHandler(router, { plugins: [ new CORSHandlerPlugin() ], }) ``` ## Custom Serializer Provide a custom serializer when you need to extend or override the default serialization behavior. For more details, see [OpenAPI Serializer](/docs/openapi/serializer). ```ts const handler = new OpenAPIHandler(router, { serializer: new OpenAPISerializer({ handlers: { // ...custom handlers }, }), }) ``` ## Filtering Procedures Use the `filter` option to exclude procedures from matching: ```ts const handler = new OpenAPIHandler(router, { filter: (contract, path) => getIsInternalMeta(contract) !== true, }) ``` ## Custom Error Response By default, `OpenAPIHandler` determines response status codes using `COMMON_ERROR_STATUS_MAP` and encodes error bodies in the ORPC error format. Use `errorStatusMap` and `customErrorResponseBodyEncoder` to customize this behavior: ```ts import { COMMON_ERROR_STATUS_MAP } from '@orpc/openapi' const handler = new OpenAPIHandler(router, { errorStatusMap: { ...COMMON_ERROR_STATUS_MAP, CUSTOM_ERROR: 599, }, customErrorResponseBodyEncoder: (error) => { if (error.code === 'CUSTOM_ERROR') { return { customMessage: error.message, customCode: error.code, } } // fallback to default by returning null or undefined return null }, }) ``` ::: details Common Error Status Map | Error Code | HTTP Status Code | | ---------------------- | ---------------: | | BAD\_REQUEST | 400 | | UNAUTHORIZED | 401 | | PAYMENT\_REQUIRED | 402 | | FORBIDDEN | 403 | | NOT\_FOUND | 404 | | METHOD\_NOT\_SUPPORTED | 405 | | NOT\_ACCEPTABLE | 406 | | TIMEOUT | 408 | | CONFLICT | 409 | | GONE | 410 | | PRECONDITION\_FAILED | 412 | | PAYLOAD\_TOO\_LARGE | 413 | | UNSUPPORTED\_MEDIA\_TYPE | 415 | | UNPROCESSABLE\_CONTENT | 422 | | PRECONDITION\_REQUIRED | 428 | | TOO\_MANY\_REQUESTS | 429 | | CLIENT\_CLOSED\_REQUEST | 499 | | INTERNAL\_SERVER\_ERROR | 500 | | NOT\_IMPLEMENTED | 501 | | BAD\_GATEWAY | 502 | | SERVICE\_UNAVAILABLE | 503 | | GATEWAY\_TIMEOUT | 504 | ::: ::: info If you use `OpenAPILink` with a custom server-side error format, make sure to configure [Custom Error Decoding](/docs/openapi/link#custom-error-decoding). ::: ## Event Stream Options Configure how [event iterators](/docs/event-iterator) are streamed to the client. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const handler = new OpenAPIHandler(router, { toFetchResponse: { eventStream: { initialComment: { /** * If true, an initial comment is sent immediately upon stream start to flush headers. * This allows the receiving side to establish the connection without waiting for the first event. * * @default true */ enabled: true, /** * The content of the initial comment sent upon stream start. Must not include newline characters. * * @default '' */ comment: '', }, keepAlive: { /** * If true, a ping comment is sent periodically to keep the connection alive. * * @default true */ enabled: true, /** * Interval (in milliseconds) between ping comments sent after the last event. * * @default 5000 */ interval: 5000, /** * The content of the ping comment. Must not include newline characters. * * @default '' */ comment: '', }, /** * If true, a `close` event is sent even when the iterator completes with `undefined`. * When the iterator returns a value, a `close` event is always emitted regardless of this setting. * * @default true */ emptyCloseEventEnabled: true, }, }, }) ``` ## Lifecycle TODO: add lifecycle diagram --- --- url: /docs/openapi/link.md --- # OpenAPI Link Use `OpenAPILink` to call HTTP endpoints served by [OpenAPI Handler](/docs/openapi/handler) and other OpenAPI-compliant servers. ## Overview ```ts const link = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api', headers: ({ context }) => ({ authorization: context?.token ? `Bearer ${context.token}` : undefined, }), interceptors: [ async ({ next, path }) => { console.time(path.join('.')) try { return await next() } finally { console.timeEnd(path.join('.')) } }, ], plugins: [ new RetryAfterLinkPlugin(), ], fetch: (request, init) => { // <- only available in fetch adapter return globalThis.fetch(request, { ...init, credentials: 'include', // Include cookies on cross-origin requests }) }, }) ``` ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Typesafe Clients After you create an `OpenAPILink`, pass it to `createORPCClient` to build a typesafe client for either a [contract](/docs/contract/router) or a [router](/docs/router): ```ts import { createORPCClient } from '@orpc/client' import { JsonifiedClient, RouterContractClient } from '@orpc/contract' import { RouterClient } from '@orpc/server' // if you are following contract-first approach const contractClient: JsonifiedClient> = createORPCClient(link) // if you are following normal approach const routerClient: JsonifiedClient> = createORPCClient(link) ``` ::: info `JsonifiedClient` is required because of [OpenAPI serializer limitations](/docs/openapi/serializer#limitations). If you want to avoid `JsonifiedClient`, see [Expanding Type Support for OpenAPI Link](/docs/advanced/expanding-type-support-for-openapi-link). ::: ## Client Context Client context lets you pass per-call values, such as auth tokens or cache hints. This context is available in link options, interceptors, plugins, and other extensibility points. ```ts type ClientContext = { token?: string } const link = new OpenAPILink(contract, { headers: ({ context }) => ({ authorization: context?.token ? `Bearer ${context.token}` : undefined, }), }) ``` ::: info Pass `ClientContext` when creating the client, then provide context on each call as needed: ```ts // if you are using the contract-first approach const client: RouterContractClient = createORPCClient(link) // if you are using the standard approach const client: RouterClient = createORPCClient(link) const output = await client.someProcedure(input, { context: { token: 'abc123', }, }) ``` ::: ## URL and Header Options Use `origin`, `url`, and `headers` to control request destination and headers. * `origin`: Server protocol and domain. Omit in the browser to use the current origin. * `url`: Usually a path prefix like `/api`. May include query params that are added to every request. * `headers`: Headers sent with every request, such as auth or trace IDs. Keys should be lowercase. ```ts const link = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api?v=2', headers: { authorization: `Bearer ${getAuthToken()}`, }, }) ``` ::: info Each option can also be a function to dynamically customize values per request. For example, routing to a different `origin` based on the procedure path, or injecting headers from client context: ```ts const link = new OpenAPILink(contract, { origin: ({ path, context }) => { if (path[0] === 'internal') { return 'https://internal.example.com' } return 'https://api.example.com' }, headers: ({ context }) => ({ authorization: context?.token ? `Bearer ${context.token}` : undefined, }), }) ``` ::: ## Interceptors Interceptors let you observe or customize different stages of an OpenAPI call. Common use cases include logging, retries, auth, batching, and transport customization. ### Interceptors Interceptors run around the entire call, including input encoding, transport, and response decoding. Use them when you need access to the path, input, output, or error. ```ts const link = new OpenAPILink(contract, { interceptors: [ async ({ next, path, input }) => { console.time(path.join('.')) try { const output = await next() return output } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } }, ], }) ``` ### Transport Interceptors Interceptors run after input encoding and before response decoding. Use them to inspect or rewrite the request. ```ts const link = new OpenAPILink(contract, { transportInterceptors: [ async (options) => { const response = await options.next({ ...options, request: { ...options.request, headers: { ...options.request.headers, 'x-request-id': crypto.randomUUID(), }, }, }) return response }, ], }) ``` ### Adapter Interceptors Some `OpenAPILink` implementations also support adapter-specific interceptors. The fetch adapter exposes `fetchInterceptors`, which run right before `fetch` and give you access to the final `url` and `RequestInit`. ```ts const link = new OpenAPILink(contract, { fetchInterceptors: [ async (options) => { const response = await options.next({ ...options, init: { ...options.init, credentials: 'include', }, }) return response }, ], }) ``` ::: info This example uses the fetch adapter. For other adapters, refer to their JSDoc or adapter-specific documentation. ::: ## Plugins Plugins package reusable interceptors. For example, [Retry After Plugin](/docs/plugins/retry-after) adds retry behavior based on the `retry-after` response header. ```ts const link = new OpenAPILink(contract, { plugins: [ new RetryAfterLinkPlugin(), ], }) ``` ## Custom Serializer Provide a custom serializer when you need to extend or override the default serialization behavior. For more details, see [OpenAPI Serializer](/docs/openapi/serializer). ```ts const link = new OpenAPILink(contract, { serializer: new OpenAPISerializer({ handlers: { // ...custom handlers }, }), }) ``` ## Custom Error Decoding If your server returns error responses that don't match oRPC's expected format, use `customErrorResponseBodyDecoder` to customize the decoding logic. This works together with [Custom Error Response](/docs/openapi/handler#custom-error-response) on the server. ```ts const link = new OpenAPILink(contract, { customErrorResponseBodyDecoder: (body, response) => { if (response.status === 422 && typeof body === 'object' && body && 'detail' in body) { return new ORPCError('BAD_REQUEST', { message: String(body.detail), }) } // fallback to default error decoding logic by returning null or undefined return null }, }) ``` ## Event Stream Options Configure how [event iterators](/docs/event-iterator) are streamed to the server. Available options depend on the adapter. For example, the fetch adapter supports: ```ts const link = new OpenAPILink(contract, { toFetchBody: { eventStream: { initialComment: { /** * If true, an initial comment is sent immediately upon stream start to flush headers. * This allows the receiving side to establish the connection without waiting for the first event. * * @default true */ enabled: true, /** * The content of the initial comment sent upon stream start. Must not include newline characters. * * @default '' */ comment: '', }, keepAlive: { /** * If true, a ping comment is sent periodically to keep the connection alive. * * @default true */ enabled: true, /** * Interval (in milliseconds) between ping comments sent after the last event. * * @default 5000 */ interval: 5000, /** * The content of the ping comment. Must not include newline characters. * * @default '' */ comment: '', }, /** * If true, a `close` event is sent even when the iterator completes with `undefined`. * When the iterator returns a value, a `close` event is always emitted regardless of this setting. * * @default true */ emptyCloseEventEnabled: true, }, }, }) ``` ## Lifecycle TODO: add lifecycle diagram --- --- url: /docs/openapi/specification.md --- # OpenAPI Specification Learn how to configure metadata and generate OpenAPI documents from your oRPC [contracts](/docs/contract/router) and [routers](/docs/router). ## Metadata Use `openapi` metadata to control how a procedure appears in the generated OpenAPI document: ```ts import { oc } from '@orpc/contract' import { openapi } from '@orpc/openapi' import { z } from 'zod' const getPlanet = oc .meta(openapi({ method: 'GET', path: '/planets/{id}', operationId: 'getPlanet', summary: 'Get a planet', description: 'Returns a single planet.', tags: ['planets'], successStatus: 200, successDescription: 'Planet payload', })) .input(z.object({ id: z.string(), })) .output(z.object({ id: z.string(), name: z.string(), })) ``` ::: info For routing metadata, you can learn more in [OpenAPI Routing](/docs/openapi/routing). For input and output mapping metadata, see [OpenAPI Input and Output Mapping](/docs/openapi/input-and-output-mapping). ::: ### Customizing the Operation Object Use `spec` to customize the generated operation object. If `spec` is an object, it replaces the generated operation object entirely. If `spec` is a callback, it receives the final operation object and returns an extended version. ```ts const getPlanet = oc .meta(openapi({ method: 'GET', path: '/planets/{id}', spec: current => ({ ...current, security: [{ bearerAuth: [] }], }), })) .input(z.object({ id: z.string() })) ``` ### Metadata Merging When `openapi` is applied multiple times, `tags`, `spec`, `prefix`, `paramsStyle`, and `queryStyles` are deep-merged, while `operationId`, `summary`, `description`, `successDescription`, `method`, `path`, `successStatus`, `inputStructure`, `outputStructure`, `responseBodyHint`, and `requestBodyHint` are overridden by the most recent call. For full merge behavior, see the [source code](https://github.com/orpc/orpc/blob/main/packages/openapi/src/meta.ts). ```ts const router = os .meta(openapi({ tags: ['planets'], spec: current => ({ ...current, security: [{ bearerAuth: [] }], }), })) .router({ list: os .meta(openapi({ method: 'GET', summary: 'List planets', tags: ['list'] })) .meta(openapi({ spec: { operationId: 'getPlanet', summary: 'List planets', responses: { 200: { description: 'List of planets', }, } } })) .input(z.object({ q: z.string().optional() })) .handler(async () => ([])), }) ``` These are equivalent to: ```ts const router = { list: os .meta(openapi({ method: 'GET', tags: ['planets', 'list'], summary: 'List planets', spec: { operationId: 'getPlanet', summary: 'List planets', responses: { 200: { description: 'List of planets', }, }, security: [{ bearerAuth: [] }], }, })) .input(z.object({ q: z.string().optional() })) .handler(async () => ([])), } ``` ::: info Metadata resets to its default behavior when set to `undefined` in subsequent calls: ```ts const example = os .meta(openapi({ tags: ['planets'] })) .meta(openapi({ tags: undefined })) ``` In this example, the final `tags` is `undefined`, so no tags are applied to `example`. ::: ## OpenAPI Generator `OpenAPIGenerator` accepts either a [contract](/docs/contract/router) or a [router](/docs/router) and generates an OpenAPI 3.1 document. ```ts import { OpenAPIGenerator } from '@orpc/openapi' const generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()], }) const spec = await generator.generate(router, { base: { info: { title: 'Planet API', version: '1.0.0', }, servers: [ { url: 'https://example.com/api' }, ], }, }) ``` ### Custom Serializer If your [OpenAPI Handler](/docs/openapi/handler#custom-serializer) uses a custom serializer, configure `OpenAPIGenerator` with the same serializer so the generated document matches the actual formats. For details, see [OpenAPI Serializer](/docs/openapi/serializer). ```ts const handler = new OpenAPIGenerator({ serializer: new OpenAPISerializer({ handlers: { // ...custom handlers }, }), }) ``` ### Filtering Procedures Use `filter` to exclude procedures from the generated document: ```ts const spec = await generator.generate(router, { filter: (_procedure, path) => !path.includes('internal'), }) ``` ### Hoisting `$defs` By default, root-level `$defs` generated by your converters are moved into `components.schemas`. Use `shouldHoistDef` to keep selected definitions inline: ```ts const spec = await generator.generate(router, { shouldHoistDef: defName => !defName.startsWith('_'), }) ``` #### Custom Error Response Schemas If your [OpenAPI Handler](/docs/openapi/handler#custom-error-response) uses custom error response formats, configure `OpenAPIGenerator` with the same logic so the generated document matches the actual error response formats. ```ts import { COMMON_ERROR_STATUS_MAP } from '@orpc/openapi' const spec = await generator.generate(router, { errorStatusMap: { ...COMMON_ERROR_STATUS_MAP, PLANET_GONE: 410, }, customErrorResponseBodySchema: (definedErrors, status) => { if (status === 410) { return { type: 'object', properties: { code: { type: 'string' }, message: { type: 'string' }, }, required: ['code', 'message'], } } // fallback to default by returning null or undefined return null }, }) ``` ### Json Schema Converters `OpenAPIGenerator` relies on JSON Schema converters to translate your input, output, and error schemas into JSON Schemas. oRPC provides built-in for [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), and [Arktype](https://arktype.io/): ```ts import { ZodToJsonSchemaConverter } from '@orpc/zod' import { ValibotToJsonSchemaConverter } from '@orpc/valibot' import { ArkTypeToJsonSchemaConverter } from '@orpc/arktype' const generator = new OpenAPIGenerator({ converters: [ new ZodToJsonSchemaConverter(), new ValibotToJsonSchemaConverter(), new ArkTypeToJsonSchemaConverter(), ], }) ``` ::: info `OpenAPIGenerator` falls back to [Standard Json Schema](https://standardschema.dev/json-schema) conversion when the required converter is missing. ::: ::: details Building Your Own Converter? Building your own converter is straightforward. You can add support for another [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library by implementing the `JsonSchemaConverter` interface: ```ts import type { AnySchema } from '@orpc/contract' import type { JsonSchema, JsonSchemaConverter, JsonSchemaConverterDirection } from '@orpc/json-schema' import { toJsonSchema } from '@valibot/to-json-schema' class MyCustomConverter implements JsonSchemaConverter { condition(schema: AnySchema | undefined, _direction: JsonSchemaConverterDirection): boolean { return schema?.['~standard'].vendor === 'valibot' } convert( schema: AnySchema | undefined, direction: JsonSchemaConverterDirection ): [jsonSchema: JsonSchema, optional: boolean] { // In most cases, treating the schema as required is acceptable. return [toJsonSchema(schema as any), false] as any } } ``` ::: #### Customizing `ZodToJsonSchemaConverter` `ZodToJsonSchemaConverter` wraps [Zod's built-in toJSONSchema](https://zod.dev/json-schema?id=ztojsonschema#ztojsonschema) and adds support for additional types. See the [source code](https://github.com/middleapi/orpc/blob/main/packages/zod/src/converter.ts) for implementation details. A common pattern is defining reusable schemas with `id` metadata. The converter places them in `$defs`, which `OpenAPIGenerator` then [hoists](#hoisting-defs) into `components.schemas`. For more on `id` and `$ref` in Zod, see [Zod JSON Schema Registries](https://zod.dev/json-schema?id=registries#registries). ```ts import { z } from 'zod' const PlanetSchema = z.object({ id: z.string(), name: z.string(), }).meta({ id: 'Planet' }) ``` #### Customizing `ValibotToJsonSchemaConverter` `ValibotToJsonSchemaConverter` wraps [Valibot's built-in toJsonSchema](https://github.com/open-circle/valibot/blob/main/packages/to-json-schema/README.md). See the [source code](https://github.com/middleapi/orpc/blob/main/packages/valibot/src/converter.ts) for implementation details. A common pattern is defining reusable or recursive schemas via definitions. The converter preserves them in `$defs`, which `OpenAPIGenerator` can then [hoist](#hoisting-defs) into `components.schemas`. For more on how definitions work in Valibot, see [Valibot JSON Schema Definitions](https://github.com/open-circle/valibot/blob/main/packages/to-json-schema/README.md#definitions). ```ts import * as v from 'valibot' const PlanetSchema = v.object({ id: v.string(), name: v.string(), }) const generator = new OpenAPIGenerator({ converters: [ new ValibotToJsonSchemaConverter({ definitions: { PlanetSchema }, }), ], }) ``` #### Customizing `ArkTypeToJsonSchemaConverter` `ArkTypeToJsonSchemaConverter` wraps [ArkType's built-in toJsonSchema](https://arktype.io/docs/type-api#tojsonschema). See the [source code](https://github.com/middleapi/orpc/blob/main/packages/arktype/src/converter.ts) and ArkType's [JSON Schema configuration docs](https://arktype.io/docs/configuration#tojsonschema) for implementation details. A common pattern is defining reusable or recursive types using scopes. The converter preserves them in `$defs`, which `OpenAPIGenerator` can then [hoist](#hoisting-defs) into `components.schemas`. ```ts import { scope } from 'arktype' const types = scope({ Planet: { name: 'string', neighbors: 'Planet[]', }, }) const PlanetSchema = types.export().Planet ``` --- --- url: /docs/openapi/scalar.md --- # Scalar (Swagger) Use [Scalar](https://github.com/scalar/scalar) to serve an interactive API reference for your oRPC API from an [OpenAPI specification](/docs/openapi/specification). ::: info This guide shows a manual setup. If you want a simpler option, use the [OpenAPI Reference Plugin](/docs/plugins/openapi-reference), which serves both the API reference UI and the OpenAPI specification for you. ::: ## Basic Example This example serves the OpenAPI document at `/spec.json` and renders Scalar at `/`. ```ts import { createServer } from 'node:http' import { OpenAPIGenerator } from '@orpc/openapi' import { OpenAPIHandler } from '@orpc/openapi/node' import { CORSPlugin } from '@orpc/server/plugins' import { ZodToJsonSchemaConverter } from '@orpc/zod' const openAPIHandler = new OpenAPIHandler(router, { plugins: [ new CORSHandlerPlugin(), ], }) 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' }, /** Use an absolute URL 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') }) ``` Open `http://localhost:3000` to view the API reference UI. --- --- url: /docs/contract/procedure.md --- # Procedure Contract Procedure contracts define the expected shape of a [procedure](/docs/procedure) without including any business logic. They are useful for documentation, testing, and keeping multiple implementations of the same procedure aligned. ## Overview ```ts twoslash import { z } from 'zod' import type { AnyMetaPlugin } from '@orpc/contract' declare const someMeta: AnyMetaPlugin // ---cut--- import { oc } from '@orpc/contract' const example = oc .meta(someMeta) // <- attach metadata .errors({ NOT_FOUND: {} }) // <- define errors .input(z.object({ id: z.number(), name: z.string() })) // <- input validation .output(z.object({ id: z.number(), name: z.string() })) // <- output validation ``` :::info All of these chains are optional. You can create an empty contract with just `oc`. ::: ## Metadata Use `.meta` to attach metadata to a contract. Middleware and plugins can read it later when you implement the contract. Learn more in the [Metadata documentation](/docs/metadata). ## Typesafe Errors Use `.errors` to define the errors a contract can produce. These errors can be thrown from handlers or middleware when you implement the contract and remain properly typed on the client. Learn more in the [Typesafe Error Handling documentation](/docs/error-handling#typesafe-errors). ## Input/Output Validation oRPC supports [Zod](https://zod.dev/), [Valibot](https://valibot.dev/), [Arktype](https://arktype.io/), and any other [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec) library for validation. ::: info Unlike a [procedure](/docs/procedure), a contract has no `.handler` chain. If you want the client to infer the output type correctly, define `.output`. Otherwise, the output type will be `unknown`. ::: ### Multiple Schemas `.input` and `.output` can be called multiple times. Each call adds another schema instead of replacing an earlier one. ```ts const example = oc .input(z.looseObject({ name: z.string() })) .input(z.looseObject({ id: z.number() })) .output(z.looseObject({ name: z.string() })) .output(z.looseObject({ id: z.number() })) ``` ::: warning When you stack schemas, the input or output must satisfy all of them, so the schemas need to be compatible. For example, with Zod, prefer `z.looseObject` over `z.object` to allow unknown properties. ::: ### `type` Utility For simple use cases without external libraries, use oRPC's built-in `type` utility. It takes a mapping function as its first argument: ```ts import { type } from '@orpc/contract' const example = oc .input(type<{ value: number }>()) .output(type<{ value: number }, number>(({ value }) => value)) ``` ## Reusability Each builder call creates a new instance, which avoids reference issues and makes contracts easy to reuse and extend. ```ts const pub = oc // Base setup for procedures that publish const authed = pub.meta(requireAuthMeta) // Extends 'pub' with authentication const pubExample = pub .input(z.object({ name: z.string() })) const authedExample = authed .input(z.object({ id: z.number() })) ``` This pattern helps prevent duplication while maintaining flexibility. --- --- url: /docs/contract/router.md --- # Router Contract Router contracts define the shape of a [router](/docs/router) without including any business logic. Use them for documentation, testing, and keeping multiple implementations of the same router aligned. ::: info A standalone [procedure contract](/docs/contract/procedure) is also a router contract, so you can use the same features with individual procedure contracts. ::: ## Overview Define a router contract as a plain JavaScript object where each key maps to a procedure contract: ```ts twoslash import { z } from 'zod' // ---cut--- import { oc } from '@orpc/contract' const ping = oc.output(z.string()) const pong = oc.output(z.string()) export const router = { ping, pong, nested: { ping, pong } } ``` ::: warning For compatibility, do not use these router keys: `then`, `bind`, `valueOf`, `toString`, `toJSON`. ::: ## Extending Router You can extend a router contract with shared configuration, such as attaching metadata to every procedure: ```ts const router = oc.meta(requireAuthMeta).router({ ping, pong, nested: { ping, pong, } }) ``` ## Router to Contract A normal [router](/docs/router) can be used as a contract router as long as it does not include a [lazy router](/docs/router#lazy-router). If necessary, use `unlazyRouter` to fully resolve it and make it contract-compatible. ```ts import { unlazyRouter } from '@orpc/server' const compatibleContract = await unlazyRouter(router) ``` ### Safely Importing Router on the Client Sometimes you need to import the contract on the client, for example when using [OpenAPI Link](/docs/openapi/link). If you derive the contract from a [router](/docs/router), importing it directly can be heavy and may expose internal logic. To avoid this, follow the steps below to safely minify and export the contract. 1. **Minify the Contract Router and Export to JSON** ```ts import fs from 'node:fs' import { unlazyRouter } from '@orpc/server' import { minifyRouterContract } from '@orpc/contract' const compatibleContract = await unlazyRouter(router) const minifiedRouter = minifyRouterContract(compatibleContract) fs.writeFileSync('./contract.json', JSON.stringify(minifiedRouter)) ``` ::: info `minifyRouterContract` preserves only the metadata needed by the client; all other data is stripped out. ::: 2. **Import the Contract JSON on the Client Side** ```ts import contract from './contract.json' // [!code highlight] const link = new OpenAPILink(contract as typeof router) ``` ::: info Cast `contract` to `typeof router` to preserve type safety, since standard schema types cannot be serialized to JSON and must be cast manually. ::: ## Utilities ::: info A standalone [procedure contract](/docs/contract/procedure) is also a router contract, so these utilities work with individual procedure contracts too. ::: ### Infer Router Contract Inputs Infers the input type of each procedure contract in a router contract. ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferRouterContractInputs } from '@orpc/contract' export type Inputs = InferRouterContractInputs type FindPlanetInput = Inputs['planet']['find'] ``` ### Infer Router Contract Outputs Infers the output type of each procedure contract in a router contract. ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferRouterContractOutputs } from '@orpc/contract' export type Outputs = InferRouterContractOutputs type FindPlanetOutput = Outputs['planet']['find'] ``` ### Infer Router Contract Error Map Collects the error maps from every procedure contract in a router contract into a single type. ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferRouterContractErrorMap } from '@orpc/contract' export type ErrorMap = InferRouterContractErrorMap ``` ### Infer Router Contract Errors Infers the throwable errors each procedure contract in a router contract can describe. ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferRouterContractErrors } from '@orpc/contract' export type Errors = InferRouterContractErrors type FindPlanetError = Errors['planet']['find'] ``` ### Infer Router Contract Error Infers all possible throwable errors the entire router contract can describe. This is useful when you want a single type for contract-wide error handling. ```ts twoslash import type { contract } from './shared/planet' // ---cut--- import type { InferRouterContractError } from '@orpc/contract' export type ContractError = InferRouterContractError ``` --- --- url: /docs/contract/implementation.md --- # Contract Implementation Implementing a contract means adding business logic to each procedure defined in that contract. It ensures every implementation stays consistent by verifying that each handler matches the procedure's expected shape. ## Implementer The `implement` function turns a contract into an implementer. Use it to build procedures, routers, and create middleware with full type safety. ```ts twoslash import { contract } from './shared/planet' // ---cut--- import { implement } from '@orpc/server' const implementer = implement(contract) .$context<{ something?: string }>() // <- define initial context implementer.planet.list // ^| // // // // ``` ### Initial Context Use `.$context` to declare the initial context required for a procedure to execute. Learn more in the [Context Documentation](/docs/context). ## Implementing Procedures Define a `.handler` for a procedure contract to provide its business logic. ```ts twoslash import { contract } from './shared/planet' import { implement } from '@orpc/server' const implementer = implement(contract) const requireAuth = implementer.middleware(({ next }) => next()) // ---cut--- const listPlanet = implementer.planet.list .use(requireAuth) // <- Apply authentication middleware .handler(({ input }) => { // Your logic for listing planets return [] }) ``` ::: info If middleware needs to wrap validation, apply it at the router level instead. In this example, use `implementer.use` to apply it globally or `implementer.planet.use` to apply it to the `planet` router before `.list`. ```ts const listPlanet = implementer .planet .use(requireAuth) // <- middleware wraps validation .list .handler(({ input }) => { // Your logic for listing planets return [] }) ``` ::: ## Implementing Routers Create the root router with `.router` to assemble your API. This enables full type-checking and runtime contract enforcement. ```ts const router = implementer.router({ planet: { list: listPlanet, find: findPlanet, create: createPlanet, }, }) ``` ### Extending Router Like a normal [router](/docs/router), an implementer router can also be extended with shared behavior. For example, you can apply authentication middleware to every procedure: ```ts const router = implementer.use(requireAuth).router({ planet: { list: listPlanet, find: findPlanet, create: createPlanet, }, }) ``` ::: danger If you apply middleware with `.use` at both the router and procedure levels, it may run more than once. That duplication can hurt performance. To avoid redundant middleware execution, see our [best practices for middleware deduplication](/docs/best-practices/dedupe-middleware). ::: ## Creating Middleware The implementer can also create [middleware](/docs/middleware). Middleware created this way can infer the contract's [typesafe errors](/docs/error-handling#typesafe-errors). If not all contracts define the same errors, use the `in` operator to check that an error exists before using it. ```ts const ratelimit = implementer.middleware(async ({ next, errors }) => { if ('TOO_MANY_REQUESTS' in errors) { // Apply rate limiting only when TOO_MANY_REQUESTS is defined by the contract. if (isRatelimitReached) { throw errors.TOO_MANY_REQUESTS() } } return next() }) ``` ::: info You do not have to create middleware from the implementer. Any type-compatible middleware can be used. ::: ## Reusability Each implementer call creates a new instance, which avoids reference issues and makes contracts easy to reuse and extend. ```ts const pub = implementer // Base setup for procedures that publish const authed = implementer.use(requireAuth) // Extends 'pub' with authentication const listPlanets = pub.planet.list.handler(({ input }) => { // Your logic for listing planets without authentication return [] }) const createPlanet = authed.planet.create.handler(({ input }) => { // Your logic for creating planets with authentication return { } }) ``` This pattern helps prevent duplication while maintaining flexibility. --- --- url: /docs/client/server-side.md --- # Server-Side Clients Server-side clients call procedures locally, within the same process. They are useful in microservices, serverless functions, or any setup where the caller and procedures run in the same environment. ## One-Off Calls Use `call` when you need to invoke a single procedure without creating a client instance. ```ts twoslash import * as z from 'zod' const exampleProcedure = os .input(z.string()) .handler(async ({ input }) => ({ id: input })) // ---cut--- import { call, os } from '@orpc/server' const result = await call(exampleProcedure, 'input', { context: {} // <- provide initial context if needed }) ``` ## Router Clients Use `createRouterClient` to create a client for your [router](/docs/router). This is useful when you want to call multiple procedures. ```ts twoslash import * as z from 'zod' import { os } from '@orpc/server' const router = { ping: os.handler(() => 'pong'), pong: os.handler(() => 'ping'), } // ---cut--- import { createRouterClient } from '@orpc/server' const client = createRouterClient(router, { context: {}, // <- provide initial context if needed, can be async function interceptors: [ async ({ next, path }) => { console.time(path.join('.')) try { return await next() } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } } ] }) const result = await client.ping() ``` ### Client Context Client context is passed with each call. Use it to switch between contexts, such as different users or tenants, without creating multiple client instances. ```ts twoslash import * as z from 'zod' import { createRouterClient, os } from '@orpc/server' const router = { ping: os.handler(() => 'pong'), pong: os.handler(() => 'ping'), } // ---cut--- interface ClientContext { cache?: boolean } const client = createRouterClient(router, { context: ({ cache }: ClientContext) => { // [!code highlight] if (cache) { return {} // <- context when cache enabled } return {} } }) const result = await client.ping(undefined, { context: { cache: true } }) ``` ### Interceptors Interceptors let you observe or modify an entire call. Common use cases include logging, error handling, and metrics collection. ```ts const client = createRouterClient(router, { interceptors: [ async ({ next, path, context }) => { console.time(path.join('.')) try { const output = await next() return output } catch (err) { console.error(`${path.join('.')}:`, err) throw err } finally { console.timeEnd(path.join('.')) } } ] }) ``` ## `.callable` extension Import `@orpc/server/extensions/callable` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds a `.callable` method to the decorated procedure, allowing you to call it directly like a regular function while still using it as a regular procedure. ::: code-group ```ts [usage] const ping = base .input(z.object({ name: z.string(), })) .handler(async ({ input }) => `Hello ${input.name}!`) .callable({ context: async () => ({}), // <- provide initial context if needed, can be async function interceptors: [], // <- client interceptors }) const router = { ping, // <- still use it as a regular procedure } const message = await ping({ name: 'World' }) // <- or call it directly ``` ```ts [setup] import '@orpc/server/extensions/callable' import { os } from '@orpc/server' export const base = os ``` ::: ## Lifecycle TODO: add lifecycle diagram --- --- url: /docs/client/client-side.md --- # Client-Side Clients Client-side clients call procedures remotely, in a different process or on a different machine. They are useful in frontend applications, mobile apps, or any setup where the client and server run in different environments. ## Installation ::: code-group ```sh [npm] npm install @orpc/client@beta ``` ```sh [yarn] yarn add @orpc/client@beta ``` ```sh [pnpm] pnpm add @orpc/client@beta ``` ```sh [bun] bun add @orpc/client@beta ``` ```sh [deno] deno add npm:@orpc/client@beta ``` ::: ## Creating a Client To create a client, first set up a link that defines how the client communicates with the server. This can be an [RPC Link](/docs/rpc/link), an [OpenAPI Link](/docs/openapi/link), or any custom link. Then create a client for your [router](/docs/router) or [contract](/docs/contract/router) using `createORPCClient`. ```ts import { createORPCClient } from '@orpc/client' import { RouterContractClient } from '@orpc/contract' import { RouterClient } from '@orpc/server' // if you are following contract-first approach const contractClient: RouterContractClient = createORPCClient(link) // if you are following normal approach const normalClient: RouterClient = createORPCClient(link) ``` :::tip You can export `RouterClient` or `RouterContractClient` from the server to avoid importing the contract or router in the client. ::: ## Calling Procedures Once your client is set up, you can call your [procedures](/docs/procedure) as if they were local functions. ```ts twoslash import * as z from 'zod' import { os, RouterClient } from '@orpc/server' const router = { ping: os.handler(() => 'pong'), pong: os.handler(() => 'ping'), } declare const client: RouterClient // ---cut--- const pong = await client.ping() client.ping // ^| ``` ## Client Context Client context lets you pass values with each call, such as auth tokens or cache hints. ```ts interface ClientContext { token?: string } // if you are following contract-first approach const client: RouterContractClient = createORPCClient(link) // if you are following normal approach const client: RouterClient = createORPCClient(link) const output = await client.someProcedure(input, { context: { token: 'abc123', }, }) ``` ## Interceptors Interceptors let you wrap client calls. They are similar to interceptors in links, but are more typesafe because the exact input, output, and error types of each client are known. You can provide per-client interceptors with `scoped`. ```ts import { isInferableError, safe } from '@orpc/client' const client: RouterClient = createORPCClient(link, { interceptors: [ async ({ context, path, next }) => { const [error, data] = await safe(next()) if (error) { if (isInferableError(error)) { // handle typesafe errors } throw error } return data } ], scoped: { planet: { find: { interceptors: [ // <- these interceptors only apply to client.planet.find async ({ context, path, next }) => { return next() } ] } } } }) ``` ::: info You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors. ::: ## Merging Clients In oRPC, a client is just an object-like structure. To merge multiple clients, assign each client to a property on a new object: ```ts const clientA: RouterClient = createORPCClient(linkA) const clientB: RouterClient = createORPCClient(linkB) const clientC: RouterClient = createORPCClient(linkC) export const orpc = { a: clientA, b: clientB, c: clientC, } ``` ## Utilities ::: info These utilities can also be used for [server-side clients](/docs/client/server-side) and are not specific to client-side clients. ::: ### Infer Client Inputs Infers input types for each procedure in a client. ```ts import type { InferClientInputs } from '@orpc/client' type Inputs = InferClientInputs type FindPlanetInput = Inputs['planet']['find'] ``` ### Infer Client Body Inputs Infers body input types for each procedure in a client. If an endpoint's input includes `{ body: ... }`, only the `body` portion is extracted. Otherwise, the entire input type is used. ```ts import type { InferClientBodyInputs } from '@orpc/client' type BodyInputs = InferClientBodyInputs type FindPlanetBodyInput = BodyInputs['planet']['find'] ``` ### Infer Client Outputs Infers output types for each procedure in a client. ```ts import type { InferClientOutputs } from '@orpc/client' type Outputs = InferClientOutputs type FindPlanetOutput = Outputs['planet']['find'] ``` ### Infer Client Body Outputs Infers body output types for each procedure in a client. If an endpoint's output includes `{ body: ... }`, only the `body` portion is extracted. Otherwise, the entire output type is used. ```ts import type { InferClientBodyOutputs } from '@orpc/client' type BodyOutputs = InferClientBodyOutputs type FindPlanetBodyOutput = BodyOutputs['planet']['find'] ``` ### Infer Client Errors Infers the errors each procedure in a client can throw when using [type-safe error handling](/docs/error-handling#typesafe-errors). ```ts import type { InferClientErrors } from '@orpc/client' type Errors = InferClientErrors type FindPlanetError = Errors['planet']['find'] ``` ### Infer Client Error Infers all possible errors the entire client can throw. This is useful with [type-safe error handling](/docs/error-handling#typesafe-errors). ```ts import type { InferClientError } from '@orpc/client' type ClientError = InferClientError ``` ### Infer Client Context Infers the [client context](#client-context) type from a client. ```ts import type { InferClientContext } from '@orpc/client' type Context = InferClientContext ``` --- --- url: /docs/client/error-handling.md --- # Client Error Handling oRPC supports several ways to handle client-side errors. In most cases, `try/catch` is enough. If you use [Typesafe Errors](/docs/error-handling#typesafe-errors), `safe` and `createSafeClient` let you handle them with full type inference. ## Using `try/catch` For most calls, use regular `try/catch`. ```ts try { const data = await client.doSomething({ id: '123' }) } catch (error) { // handle error } ``` ## Using `safe` and `isInferableError` When working with [Typesafe Errors](/docs/error-handling#typesafe-errors), use `safe` to preserve error type inference. It behaves like `try/catch`, but returns the typesafe result instead of throwing. ```ts twoslash import { call, os } from '@orpc/server' import * as z from 'zod' // ---cut--- import { isInferableError, safe } from '@orpc/client' const exampleProcedure = os .input(z.object({ id: z.string() })) .errors({ RATE_LIMIT_EXCEEDED: { data: z.object({ retryAfter: z.number() }) } }) .handler(async ({ input, errors }) => { throw errors.RATE_LIMIT_EXCEEDED({ data: { retryAfter: 1000 } }) }) // or { error, data, inferableError } const [error, data, inferableError] = await safe( call(exampleProcedure, { id: '123' }) ) if (isInferableError(error)) { // or inferableError // handle inferable error // or inferableError.data.retryAfter console.log(error.data.retryAfter) } else if (error) { // handle unknown error } else { // handle success console.log(data) } ``` ::: info `safe` supports both tuple and object forms: * `[error, data, inferableError]` * `{ error, data, inferableError }` `inferableError` is the same value as `error` when `isInferableError(error)` returns `true`; otherwise it is `null`. ::: ## Safe Client If you use `safe` often, `createSafeClient` can reduce repetition by wrapping entire client calls with `safe`. ```ts import { createSafeClient } from '@orpc/client' const safeClient = createSafeClient(client) const [error, data] = await safeClient.doSomething({ id: '123' }) ``` --- --- url: /docs/client/event-iterator.md --- # Event Iterator in Client Consume an [Event Iterator](/docs/event-iterator) like an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator). Await the call, then iterate over events as they arrive. ## Basic Usage ```ts twoslash import { eventIterator, oc, RouterContractClient } from '@orpc/contract' import { z } from 'zod' const contract = { streaming: oc.output(eventIterator(z.object({ message: z.string() }))) } declare const client: RouterContractClient // ---cut--- const iterator = await client.streaming() for await (const event of iterator) { console.log(event.message) } ``` ## Stopping the Stream Use an `AbortSignal` or call `.return` to stop the iterator. ```ts const controller = new AbortController() const iterator = await client.streaming(undefined, { signal: controller.signal }) // Stop the stream after 1 second setTimeout(async () => { controller.abort() // Or call `await iterator.return()` if you already have the iterator instance. }, 1000) for await (const event of iterator) { console.log(event.message) } ``` ## Error Handling ::: info Unlike traditional SSE, Event Iterators do not retry automatically after an error. To add retries, use the [Retry Plugin](/docs/plugins/retry#event-source-simulation). ::: ```ts const iterator = await client.streaming() try { for await (const event of iterator) { console.log(event.message) } } catch (error) { if (error instanceof ORPCError) { // Handle the error here } } ``` ## Event Metadata Use `getEventMeta` to read [event metadata](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) for each item, such as the event ID and retry interval. ```ts import { getEventMeta } from '@orpc/client' const iterator = await client.streaming() for await (const event of iterator) { const meta = getEventMeta(event) console.log(event.message, meta?.id, meta?.retry) } ``` ## Using `consumeEventIterator` Use `consumeEventIterator` to consume an event iterator with lifecycle callbacks. It accepts either an event iterator or a promise that resolves to one. ```ts import { consumeEventIterator } from '@orpc/client' const cancel = consumeEventIterator(client.streaming(), { onEvent: (event) => { console.log(event.message) }, onError: (error) => { console.error(error) }, onSuccess: (value) => { console.log(value) }, onFinish: (state) => { console.log(state) }, }) setTimeout(async () => { // Stop the stream after 1 second await cancel() }, 1000) ``` --- --- url: /docs/client/dynamic-link.md --- # DynamicLink `DynamicLink` lets you choose a link at runtime. Use it when different requests should be routed through different links. ## Example ```ts twoslash import { os, RouterClient } from '@orpc/server' import { RPCLink } from '@orpc/client/fetch' const router = { ping: os.handler(() => 'pong'), pong: os.handler(() => 'ping'), } // ---cut--- import { createORPCClient, DynamicLink } from '@orpc/client' interface ClientContext { cache?: boolean } const cacheLink = new RPCLink({ origin: 'https://cache.example.com', }) const noCacheLink = new RPCLink({ origin: 'https://example.com', }) const link = new DynamicLink((options, path, input) => { if (options.context?.cache) { return cacheLink } return noCacheLink }) const client: RouterClient = createORPCClient(link) ``` ::: info This example uses two [RPC Link](/docs/rpc/link) instances, but `DynamicLink` works with any other link. ::: --- --- url: /docs/adapters/fetch-api.md --- ## Fetch API Adapter oRPC supports the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) for both servers and clients. ## Server Usage ::: code-group ```ts [RPC] import { RPCHandler } from '@orpc/server/fetch' import { CORSPlugin } from '@orpc/server/plugins' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { plugins: [ new CORSHandlerPlugin() ], interceptors: [ onError((error) => { console.error(error) }), ], }) export async function fetch(request: Request): Promise { const { matched, response } = await handler.handle(request, { prefix: '/rpc', context: {} // Provide initial context if needed }) if (matched) { return response } return new Response('Not found', { status: 404 }) } ``` ```ts [OpenAPI] import { OpenAPIHandler } from '@orpc/openapi/fetch' import { CORSPlugin } from '@orpc/server/plugins' import { onError } from '@orpc/server' const handler = new OpenAPIHandler(router, { plugins: [ new CORSHandlerPlugin() ], interceptors: [ onError((error) => { console.error(error) }), ], }) export async function fetch(request: Request): Promise { const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} // Provide initial context if needed }) if (matched) { return response } return new Response('Not found', { status: 404 }) } ``` ::: ::: info The actual usage of `fetch` depends on the runtime environment or library you use: ::: code-group ```ts [Bun] Bun.serve({ fetch, }) ``` ```ts [Cloudflare Workers] export default { fetch, } ``` ```ts [Deno] Deno.serve(fetch) ``` ```ts [Hono Lambda] import { handle } from 'hono/aws-lambda' export const handler = handle({ fetch }) ``` ::: ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Client Usage ::: code-group ```ts [RPC] import { RPCLink } from '@orpc/client/fetch' import { onError } from '@orpc/client' const link = new RPCLink({ origin: 'https://api.example.com', // accepts async function, defaults to current origin url: '/rpc', // accepts async function headers: { authorization: 'bearer token' }, // accept async function interceptors: [ onError((error) => { console.error(error) }), ], fetch: (request, init) => { // <- override fetch if needed return globalThis.fetch(request, { ...init, credentials: 'include', // Include cookies on cross-origin requests }) }, }) ``` ```ts [OpenAPI] import { OpenAPILink } from '@orpc/openapi/fetch' import { onError } from '@orpc/client' const link = new OpenAPILink(contract, { origin: 'https://api.example.com', // accepts async function, defaults to current origin url: '/rpc', // accepts async function headers: { authorization: 'bearer token' }, // accept async function interceptors: [ onError((error) => { console.error(error) }), ], fetch: (request, init) => { // <- override fetch if needed return globalThis.fetch(request, { ...init, credentials: 'include', // Include cookies on cross-origin requests }) }, }) ``` ::: ::: info The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients) and [OpenAPI Link](/docs/openapi/link#typesafe-clients). ::: ## Event Stream Options You can configure how [event iterators](/docs/event-iterator) are streamed to the client using the `toFetchResponse.eventStream` options when creating the handler. ```ts const handler = new OpenAPIHandler(router, { toFetchResponse: { eventStream: { initialComment: { /** * If true, an initial comment is sent immediately upon stream start to flush headers. * This allows the receiving side to establish the connection without waiting for the first event. * * @default true */ enabled: true, /** * The content of the initial comment sent upon stream start. Must not include newline characters. * * @default '' */ comment: '', }, keepAlive: { /** * If true, a ping comment is sent periodically to keep the connection alive. * * @default true */ enabled: true, /** * Interval (in milliseconds) between ping comments sent after the last event. * * @default 5000 */ interval: 5000, /** * The content of the ping comment. Must not include newline characters. * * @default '' */ comment: '', }, /** * If true, a `close` event is sent even when the iterator completes with `undefined`. * When the iterator returns a value, a `close` event is always emitted regardless of this setting. * * @default true */ emptyCloseEventEnabled: true, }, }, }) ``` ::: info You can also configure how [event iterators](/docs/event-iterator) are streamed from client to server using `toFetchBody.eventStream` options when creating the link. However, this is rarely used because streaming requests are not widely supported in browsers and may require manually overriding the `fetch` function with `duplex`. ::: --- --- url: /docs/adapters/node-http.md --- # Node HTTP Adapter oRPC supports [Node HTTP](https://nodejs.org/api/http.html), [Node HTTPS](https://nodejs.org/api/https.html), and [Node HTTP2](https://nodejs.org/api/http2.html) for servers. ## Server Usage ::: code-group ```ts [RPC] import { createServer } from 'node:http' // or 'node:https' or 'node:http2' import { RPCHandler } from '@orpc/server/node' import { CORSHandlerPlugin } from '@orpc/server/plugins' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { plugins: [ new CORSHandlerPlugin() ], interceptors: [ onError((error) => { console.error(error) }), ], }) const server = createServer(async (req, res) => { const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} // Provide initial context if needed }) if (matched) { return } res.statusCode = 404 res.end('Not found') }) server.listen(3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000')) ``` ```ts [OpenAPI] import { createServer } from 'node:http' // or 'node:https' or 'node:http2' import { OpenAPIHandler } from '@orpc/openapi/node' import { CORSHandlerPlugin } from '@orpc/server/plugins' import { onError } from '@orpc/server' const handler = new OpenAPIHandler(router, { plugins: [ new CORSHandlerPlugin() ], interceptors: [ onError((error) => { console.error(error) }), ], }) const server = createServer(async (req, res) => { const { matched } = await handler.handle(req, res, { prefix: '/api', context: {} // Provide initial context if needed }) if (matched) { return } res.statusCode = 404 res.end('Not found') }) server.listen(3000, '127.0.0.1', () => console.log('Listening on 127.0.0.1:3000')) ``` ::: ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Event Stream Options You can configure how [event iterators](/docs/event-iterator) are streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler. ```ts const handler = new OpenAPIHandler(router, { sendStandardResponse: { eventStream: { initialComment: { /** * If true, an initial comment is sent immediately upon stream start to flush headers. * This allows the receiving side to establish the connection without waiting for the first event. * * @default true */ enabled: true, /** * The content of the initial comment sent upon stream start. Must not include newline characters. * * @default '' */ comment: '', }, keepAlive: { /** * If true, a ping comment is sent periodically to keep the connection alive. * * @default true */ enabled: true, /** * Interval (in milliseconds) between ping comments sent after the last event. * * @default 5000 */ interval: 5000, /** * The content of the ping comment. Must not include newline characters. * * @default '' */ comment: '', }, /** * If true, a `close` event is sent even when the iterator completes with `undefined`. * When the iterator returns a value, a `close` event is always emitted regardless of this setting. * * @default true */ emptyCloseEventEnabled: true, }, }, }) ``` --- --- url: /docs/adapters/websocket.md --- # WebSocket Adapters oRPC supports WebSockets for low-latency, full-duplex communication between clients and servers. ## Server Adapters | Adapter | Target | | ----------- | ----------------------------------------------------------------------------------------------------------------------- | | `websocket` | [MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), [ws](https://github.com/websockets/ws) | | `crossws` | [crossws](https://github.com/h3js/crossws) | ::: code-group ```ts [ws] import { WebSocketServer } from 'ws' import { RPCHandler } from '@orpc/server/websocket' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { interceptors: [ onError((error) => { console.error(error) }), ], }) const wss = new WebSocketServer({ port: 8080 }) wss.on('connection', (ws) => { handler.upgrade(ws, { /** * Provide initial context if needed. The context can be an async function * that receives the per-call request as its first argument, and is **not** * related to the initial WebSocket upgrade request. */ context: request => ({}), }) }) ``` ```ts [crossws] import { createServer } from 'node:http' import { experimental_RPCHandler as RPCHandler } from '@orpc/server/crossws' import { onError } from '@orpc/server' // any crossws adapter is supported import crossws from 'crossws/adapters/node' const handler = new RPCHandler(router, { interceptors: [ onError((error) => { console.error(error) }), ], }) const ws = crossws({ hooks: { message: (peer, message) => { handler.message(peer, message, { /** * Provide initial context if needed. The context can be an async function * that receives the per-call request as its first argument, and is **not** * related to the initial WebSocket upgrade request. */ context: request => ({}), }) }, close: (peer) => { handler.close(peer) }, }, }) const server = createServer((req, res) => { res.end(`Hello World`) }).listen(3000) server.on('upgrade', (req, socket, head) => { if (req.headers.upgrade === 'websocket') { ws.handleUpgrade(req, socket, head) } }) ``` ```ts [Deno] import { RPCHandler } from '@orpc/server/websocket' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { interceptors: [ onError((error) => { console.error(error) }), ], }) Deno.serve((req) => { if (req.headers.get('upgrade') !== 'websocket') { return new Response(null, { status: 501 }) } const { socket, response } = Deno.upgradeWebSocket(req) handler.upgrade(socket, { /** * Provide initial context if needed. The context can be an async function * that receives the per-call request as its first argument, and is **not** * related to the initial WebSocket upgrade request. */ context: request => ({}), }) return response }) ``` ```ts [Cloudflare Websocket Hibernation] import { RPCHandler } from '@orpc/server/websocket' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { interceptors: [ onError((error) => { console.error(error) }), ], }) export class ChatRoom extends DurableObject { async fetch(): Promise { const { '0': client, '1': server } = new WebSocketPair() this.ctx.acceptWebSocket(server) return new Response(null, { status: 101, webSocket: client, }) } async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise { await handler.message(ws, message, { /** * Provide initial context if needed. The context can be an async function * that receives the per-call request as its first argument, and is **not** * related to the initial WebSocket upgrade request. */ context: request => ({}), }) } async webSocketClose(ws: WebSocket): Promise { handler.close(ws) } } ``` ::: ## Client Adapters | Adapter | Target | | ----------- | ------------------------------------------------------------------------------- | | `websocket` | [MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) | ```ts import { RPCLink } from '@orpc/client/websocket' const link = new RPCLink({ connect: info => new WebSocket('ws://localhost:3000'), /** * Whether to connect immediately on initialization, instead of waiting * for the first call. Reduces latency for the first request. * * @default false */ connectOnInit: true, /** * Optional headers to attach to each per-call request. * These can be accessed in the server context or via the Request Headers Plugin. */ headers: () => ({}) }) ``` ::: info The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients). ::: ### Auto Reconnect The client adapter has built-in support for reconnecting when the connection is lost. You can configure reconnect behavior with the `reconnect` option when creating the link. ```ts const link = new RPCLink({ reconnect: { /** * Whether to automatically reconnect when the connection is lost. * * @default false */ enabled: true, /** * Delay before a (re)connect attempt, in milliseconds. * * @default info => info.attempt === 1 ? 0 : 2_000 */ delay: info => info.attempt === 1 ? 0 : 2_000, /** * Maximum number of consecutive failed attempts before giving up. * When exceeded, `getConnectedPeer` throws instead of retrying. * Should greater than 1 * * @default Infinity */ maxAttempt: Infinity, onClose: { /** * Whether to proactively reconnect right after the socket closes, * rather than waiting for the next call to trigger reconnection. * Reduces latency for the next request. * * @default false */ enabled: false, /** * Delay before reconnecting after the socket closes, in milliseconds. * * @default 0 */ delay: 0 } } }) ``` --- --- url: /docs/adapters/message-port.md --- # Message Port Adapter oRPC supports the [Message Port](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort) for communicating between different contexts, such as iframes, web workers, and service workers. ## Basic Usage Message Ports work by establishing two endpoints that can communicate with each other: ```ts [Bridge] const channel = new MessageChannel() const serverPort = channel.port1 const clientPort = channel.port2 ``` ```ts [Server] import { RPCHandler } from '@orpc/server/message-port' import { onError } from '@orpc/server' const handler = new RPCHandler(router, { interceptors: [ onError((error) => { console.error(error) }), ], }) handler.upgrade(serverPort, { /** * Provide initial context if needed. The context can be an async function * that receives the per-call request as its first argument, and is **not** * related to the initial upgrade request. */ context: request => ({}), }) serverPort.start() ``` ```ts [Client] import { RPCLink } from '@orpc/client/message-port' import { onError } from '@orpc/client' const link = new RPCLink({ port: clientPort, interceptors: [ onError((error) => { console.error(error) }), ], /** * Optional headers to attach to each per-call request. * These can be accessed in the server context or via the Request Headers Plugin. */ headers: () => ({}) }) clientPort.start() ``` ::: info The examples above only show how to configure the link. For examples of creating a typesafe client, see [RPC Link](/docs/rpc/link#typesafe-clients). ::: ## Transfer By default, oRPC serializes request/response messages to string/binary data before sending over message port. If needed, you can define the `transfer` option to utilize full power of [MessagePort: postMessage() method](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage), such as transferring ownership of objects to the other side or support unserializable objects like `OffscreenCanvas`. ::: code-group ```ts [handler] const handler = new RPCHandler(router, { experimental_transfer: (message, port) => { const transfer = deepFindTransferableObjects(message) // implement your own logic return transfer.length ? transfer : null // only enable when needed } }) ``` ```ts [link] const link = new RPCLink({ experimental_transfer: (message) => { const transfer = deepFindTransferableObjects(message) // implement your own logic return transfer.length ? transfer : null // only enable when needed } }) ``` ::: ::: info When `transfer` returns an array, messages are sent using [the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), which doesn't support all data types. If you need to support additional data types, consider customizing your [RPC Serializer](/docs/rpc/serializer). ::: --- --- url: /docs/plugins/batch.md --- # Batch Plugin Use the **Batch Plugin** to combine multiple requests into a single batch and receive their responses together. This reduces the overhead of sending each request separately. ::: warning HTTP/2, HTTP/3, and later versions already support multiplexing, which allows multiple requests and responses to share a single connection. Because these protocols are now widely adopted, this plugin is often less useful than it once was. ::: ## Setup Set up batching on both the server and the client. The server plugin handles incoming batch requests, and the client plugin groups outgoing requests into batches. ::: code-group ```ts [server.ts] import { BatchHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new BatchHandlerPlugin(), ], }) ``` ```ts [client.ts] import { BatchLinkPlugin } from '@orpc/client/plugins' const link = new RPCLink({ url: '/rpc', plugins: [ new BatchLinkPlugin({ groups: [ { condition: () => true, context: {}, }, ], }), ], }) ``` ::: ::: warning `BatchHandlerPlugin` detects batch requests by checking for the `orpc-batch` header. If you enable CORS, add this header to your allowlist so cross-origin batch requests are not blocked. ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['orpc-batch'], }) ``` ::: ## Response Modes By default, the plugin uses `streaming` mode. Responses are sent as soon as they are ready, so one slow request does not block the rest of the batch. If your environment does not support streaming responses, such as some serverless platforms or older browsers, switch to `buffered` mode instead. In this mode, all responses are collected and sent together. ```ts const link = new RPCLink({ url: '/rpc', plugins: [ new BatchLinkPlugin({ mode: 'buffered', groups: [ { condition: () => true, context: {}, }, ], }), ], }) ``` ## Groups Only requests in the same group are batched together. Each group also defines a context, as described in [client context](/docs/rpc/link#client-context). The following example batches requests by cache policy: ```ts interface ClientContext { cache?: RequestCache } const link = new RPCLink({ method: ({ context }) => { if (context?.cache) { return 'GET' } return 'POST' }, plugins: [ new BatchLinkPlugin({ groups: [ { condition: ({ context }) => context?.cache === 'force-cache', context: { // used for the rest of the request lifecycle cache: 'force-cache', }, }, { // Fallback for all other requests. Keep this last. condition: () => true, context: {}, }, ], }), ], fetch: (url, init, { context }) => globalThis.fetch(url, { ...init, cache: context?.cache, }), }) ``` Now, calls made with `cache = 'force-cache'` use that cache setting whether they are batched or sent individually. ## Filtering Requests Use `filter` to skip batching for specific requests before group matching runs. Requests for which `filter` returns `false` continue through the link chain individually. ```ts const link = new RPCLink({ url: '/rpc', plugins: [ new BatchLinkPlugin({ filter: ({ path }) => !path.includes('upload'), groups: [ { condition: () => true, context: {}, }, ], }), ], }) ``` ## Learn More See the [BatchHandlerPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/batch-handler-plugin.ts) and the [BatchLinkPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/plugins/batch-link-plugin.ts) for implementation details. --- --- url: /docs/plugins/body-compression.md --- # Body Compression Plugin **Body Compression Plugin** compresses response bodies to reduce bandwidth usage and improve performance for clients that support compression. ## Import Depending on your adapter, import the corresponding plugin: ```ts import { BodyCompressionHandlerPlugin } from '@orpc/server/node' import { BodyCompressionHandlerPlugin } from '@orpc/server/fetch' ``` ## Setup Add the plugin to your handler: ```ts const handler = new RPCHandler(router, { plugins: [ new BodyCompressionHandlerPlugin(), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ## Learn More For implementation details, see the [fetch adapter source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/adapters/fetch/body-compression-plugin.ts) and the [node adapter source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/adapters/node/body-compression-plugin.ts). --- --- url: /docs/plugins/body-limit.md --- # Body Limit Plugin **Body Limit Plugin** helps restrict the size of the request body. ## Import Depending on your adapter, import the corresponding plugin: ```ts import { BodyLimitHandlerPlugin } from '@orpc/server/fetch' import { BodyLimitHandlerPlugin } from '@orpc/server/node' ``` ## Setup Set `maxBodySize` to the maximum number of bytes allowed: ```ts const handler = new RPCHandler(router, { plugins: [ new BodyLimitHandlerPlugin({ maxBodySize: 1024 * 1024, // 1MB }), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ## Learn More For implementation details, see the [fetch adapter source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/adapters/fetch/body-limit-plugin.ts) and the [node adapter source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/adapters/node/body-limit-plugin.ts). --- --- url: /docs/plugins/cors.md --- # CORS Handler Plugin Use `CORSHandlerPlugin` to configure [CORS Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for your API. ## Basic ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { CORSHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new CORSHandlerPlugin({ 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/handler), or a custom one. ::: ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` ::: ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/cors.ts). --- --- url: /docs/plugins/csrf-guard.md --- # CSRF Guard Plugin Use `CSRFGuardHandlerPlugin` to protect against [Cross-Site Request Forgery (CSRF) attacks](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF) by rejecting requests with unsafe fetch modes. ## How It Works The plugin inspects the [Sec-Fetch-Mode header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode) and blocks requests with a mode of `navigate`, `no-cors`, or `websocket`, which may be triggered by cross-site links, forms, or other passive browser features. ## Setup ```ts import { OpenAPIHandler } from '@orpc/openapi/fetch' import { CSRFGuardHandlerPlugin } from '@orpc/server/plugins' const handler = new OpenAPIHandler(router, { plugins: [ new CSRFGuardHandlerPlugin(), ], }) ``` ::: info HTTP-based `RPCHandler` implementations enable this plugin by default. Disable it with `csrfGuardHandlerPlugin.enabled`. ```ts const handler = new RPCHandler(router, { csrfGuardHandlerPlugin: { enabled: false, }, }) ``` ::: ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/csrf-guard.ts). --- --- url: /docs/plugins/dedupe.md --- # Dedupe Plugin **Dedupe Plugin** prevents redundant requests by deduplicating similar requests, reducing the number of requests sent to the server. ## Overview ```ts import { DedupeLinkPlugin } from '@orpc/client/plugins' const link = new RPCLink({ plugins: [ new DedupeLinkPlugin({ groups: [ { condition: () => true, context: {}, // Context used for the rest of the request lifecycle }, ], }), ], }) ``` ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. ::: ## Filter By default, the plugin deduplicates only `GET` requests. You can customize this behavior by providing a `filter` function. ```ts const link = new RPCLink({ plugins: [ new DedupeLinkPlugin({ filter: ({ request }) => request.method === 'GET', groups: [ { condition: () => true, context: {}, }, ], }), ], }) ``` ::: warning If you are using [RPC Link](/docs/rpc/link), you might need to [customize the request method](/docs/rpc/link#request-method) because it defaults to `POST`. ::: ::: tip If your application does not need to run multiple mutation requests in parallel within the same [call stack](https://developer.mozilla.org/en-US/docs/Glossary/Call_stack), you can expand the filter to deduplicate **all** request types. This can also help prevent duplicate mutation requests when users click actions too quickly. ::: ## Groups Only requests in the same group are deduplicated together. Each group also defines a `context`, as described in [client context](/docs/client/client-side#client-context). The following example deduplicates requests by cache policy: ```ts interface ClientContext { cache?: RequestCache } const link = new RPCLink({ method: ({ context }) => { if (context?.cache) { return 'GET' } return 'POST' }, plugins: [ new DedupeLinkPlugin({ groups: [ { condition: ({ context }) => context?.cache === 'force-cache', context: { // used for the rest of the request lifecycle cache: 'force-cache', }, }, { // Fallback for all other requests. Keep this last. condition: () => true, context: {}, }, ], }), ], fetch: (url, init, { context }) => globalThis.fetch(url, { ...init, cache: context?.cache, }), }) ``` Now, calls made with `cache = 'force-cache'` use that cache setting whether they are deduplicated or sent individually. ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/plugins/dedupe.ts). --- --- url: /docs/plugins/openapi-reference.md --- # OpenAPI Reference Plugin (Swagger/Scalar) This plugin serves API reference documentation powered by [Scalar](https://github.com/scalar/scalar) or [Swagger UI](https://swagger.io/tools/swagger-ui/), and exposes the OpenAPI specification as JSON. ::: info This plugin depends on the [OpenAPI Generator](/docs/openapi/specification). Review that guide before setting up the reference plugin. ::: ## Setup To use this plugin, first create an [OpenAPI Generator](/docs/openapi/specification). The plugin uses it to generate the OpenAPI specification. ```ts import { OpenAPIGenerator } from '@orpc/openapi' import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins' const generator = new OpenAPIGenerator({ converters: [ new ZodToJsonSchemaConverter(), ], }) const handler = new OpenAPIHandler(router, { plugins: [ new OpenAPIReferencePlugin({ spec: () => generator.generateSpec(router, { info: { title: 'ORPC Playground', version: '1.0.0', }, servers: [ { url: 'https://api.example.com/v1', }, ], }), }), ] }) ``` ::: info By default, the API reference UI is served from `/`, and the OpenAPI specification is served from `/spec.json`. Use `docsPath` and `specPath` to change these routes. ::: ## Provider [Scalar](https://github.com/scalar/scalar) is the default provider. To use [Swagger UI](https://swagger.io/tools/swagger-ui/) instead, set `provider` to `'swagger'`. Use `providerConfig` to pass provider-specific options. ```ts const handler = new OpenAPIHandler(router, { plugins: [ new OpenAPIReferencePlugin({ provider: 'swagger', providerConfig: { // Swagger UI specific configuration }, }), ] }) ``` ::: info You can also load custom assets for the docs UI by setting `providerScriptUrl` and `providerCssUrl`. ::: ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/openapi/src/plugins/openapi-reference.ts). --- --- url: /docs/plugins/request-headers.md --- # Request Headers Plugin Use `RequestHeadersHandlerPlugin` to expose incoming request headers as `context.reqHeaders`. ## Context Access ```ts twoslash import { os } from '@orpc/server' // ---cut--- import { getCookie } from '@orpc/server/helpers' import type { RequestHeadersHandlerPluginContext } from '@orpc/server/plugins' interface ServerContext extends RequestHeadersHandlerPluginContext {} const base = os.$context() const example = base .use(({ context, next }) => { const sessionId = getCookie(context.reqHeaders, 'session_id') return next() }) .handler(({ context }) => { const userAgent = context.reqHeaders?.get('user-agent') return { userAgent } }) ``` ::: info Why can `reqHeaders` be undefined? This allows procedures to run safely even without `RequestHeadersHandlerPlugin`, such as in direct calls. ::: ::: tip Combine with [Cookie Helpers](/docs/helpers/cookie) for streamlined cookie management. ::: ## Handler Setup ```ts import { RequestHeadersHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new RequestHeadersHandlerPlugin(), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/request-headers.ts). --- --- url: /docs/plugins/request-validation.md --- # Request Validation Plugin **Request Validation Plugin** validates requests against your contract before they are sent to the server. This is useful when your application relies on server-side validation. ## Setup ```ts import { RequestValidationLinkPlugin } from '@orpc/contract/plugins' const link = new RPCLink({ plugins: [ new RequestValidationLinkPlugin(contract), ], }) ``` ::: info If you do not have a [contract](/docs/contract/router), you can use a [unlazied router](/docs/contract/router#router-to-contract) instead. ::: ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. ::: ## Forward Validated Input By default, the plugin does not reuse validated input for the rest of the request. Some schemas transform input in ways that can cause server-side validation to fail. If your schemas do not do that, set `forwardValidatedInput` to `true`. ```ts const link = new RPCLink({ plugins: [ new RequestValidationLinkPlugin(contract, { forwardValidatedInput: true, }), ], }) ``` ## Custom Validation Errors If you have already [customized validation errors on the server](/docs/advanced/validation-errors), you can use interceptors to catch and map the validation errors thrown by this plugin so they match your server-side errors. ```ts import { ORPCError } from '@orpc/client' import { ValidationError } from '@orpc/contract' const link = new RPCLink({ plugins: [ new RequestValidationLinkPlugin(contract), ], interceptors: [ async ({ next }) => { try { return await next() } catch (error) { if ( error instanceof ORPCError && error.code === 'BAD_REQUEST' && error.cause instanceof ValidationError ) { throw new CustomInputValidationError(error.cause.issues) } throw error } } ] }) ``` ## Form Validation You can pair this plugin with [Form Data Helpers](/docs/helpers/form-data) to avoid heavier form validation libraries and keep your contract as the single source of truth on both the client and server. ```tsx import { getIssueMessage, parseFormData } from '@orpc/openapi/helpers' export function ContactForm() { const [error, setError] = useState() const handleSubmit = async (form: FormData) => { try { const output = await client.someProcedure(parseFormData(form)) console.log(output) } catch (error) { setError(error) } } return (
{getIssueMessage(error, 'user[name]')} {getIssueMessage(error, 'user[emails][]')}
) } ``` ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/contract/src/plugins/request-validation.ts). --- --- url: /docs/plugins/response-headers.md --- # Response Headers Plugin Use `ResponseHeadersHandlerPlugin` to accumulate response headers in `context.resHeaders` and merge them into the final response. ## Context Access ```ts twoslash import { os } from '@orpc/server' import { setCookie } from '@orpc/server/helpers' // ---cut--- import type { ResponseHeadersHandlerPluginContext } from '@orpc/server/plugins' interface ServerContext extends ResponseHeadersHandlerPluginContext {} const base = os.$context() const procedure = base .use(({ context, next }) => { context.resHeaders?.set('x-request-id', 'req_123') return next() }) .handler(({ context }) => { setCookie(context.resHeaders, 'session_id', 'abc123', { secure: true, maxAge: 3600 }) }) ``` ::: info Why can `resHeaders` be undefined? This allows procedures to run safely even without `ResponseHeadersHandlerPlugin`, such as in direct calls. ::: ::: tip Combine with [Cookie Helpers](/docs/helpers/cookie) for streamlined cookie management. ::: ## Handler Setup ```ts import { ResponseHeadersHandlerPlugin } from '@orpc/server/plugins' const handler = new RPCHandler(router, { plugins: [ new ResponseHeadersHandlerPlugin(), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/response-headers.ts). --- --- url: /docs/plugins/response-validation.md --- # Response Validation Plugin **Response Validation Plugin** validates server responses against your contract before your application uses them. This helps ensure the data returned by the server matches the types defined in your contract. ## Setup ```ts import { ResponseValidationLinkPlugin } from '@orpc/contract/plugins' const link = new RPCLink({ plugins: [ new ResponseValidationLinkPlugin(contract), ], }) ``` ::: info If you do not have a [contract](/docs/contract/router), you can use a [unlazied router](/docs/contract/router#router-to-contract) instead. ::: ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. ::: ## Limitations Schemas that transform values into a different type are not supported. **Why?** Consider this schema, which accepts a `number` and transforms it into a `string`: ```ts const unsupported = z.number().transform(value => value.toString()) ``` When the server validates the output, it transforms the `number` into a `string`. The client then receives that `string`, but the schema still expects a `number` as input, so validation fails. ## Typesafe Errors Compatibility This plugin reconciles ORPC errors from other interceptors and plugins, allowing you to use `ORPCError` for [typesafe errors](/docs/error-handling#orpcerror-compatibility). ## Custom Validation Errors If you have already [customized validation errors on the server](/docs/advanced/validation-errors), you can use interceptors to catch and map the validation errors thrown by this plugin so they match your server-side errors. ```ts import { ORPCError } from '@orpc/client' import { ValidationError } from '@orpc/contract' const link = new RPCLink({ plugins: [ new ResponseValidationLinkPlugin(contract), ], interceptors: [ async ({ next }) => { try { return await next() } catch (error) { if ( error instanceof ORPCError && error.code === 'INTERNAL_SERVER_ERROR' && error.cause instanceof ValidationError ) { throw new CustomOutputValidationError(error.cause.issues) } throw error } } ] }) ``` ## Advanced Usage You can also use this plugin in guides such as [Expanding Type Support for OpenAPI Link](/docs/advanced/expanding-type-support-for-openapi-link). ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/contract/src/plugins/response-validation.ts). --- --- url: /docs/plugins/retry-after.md --- # Retry After Plugin **Retry After Plugin** automatically retries requests according to the `Retry-After` response header. This is especially useful for handling rate limits and temporary server unavailability. ## Usage ```ts import { RetryAfterLinkPlugin } from '@orpc/client/plugins' const link = new RPCLink({ plugins: [ new RetryAfterLinkPlugin(), ], }) ``` ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. ::: ## Options By default, the plugin retries only requests that receive a `429` (Too Many Requests) or `503` (Service Unavailable) status code. It times out after 5 minutes and allows up to 3 retry attempts. You can customize this behavior with the following options: ```ts const link = new RPCLink({ plugins: [ new RetryAfterLinkPlugin({ condition: response => [429, 503].includes(response.status), timeout: 5 * 60 * 1000, // 5 minutes maxAttempts: 3, }), ], }) ``` ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/plugins/retry-after.ts). --- --- url: /docs/plugins/retry.md --- # Retry Plugin **Retry Plugin** automatically retries failed requests based on customizable retry strategies, improving the resilience of your application. ::: warning Before using this plugin, make sure you understand [client context](/docs/client/client-side#client-context), as retry behavior is managed through context. ::: ## Setup ```ts import { RetryLinkPlugin, RetryLinkPluginContext } from '@orpc/client/plugins' interface ClientContext extends RetryLinkPluginContext {} const link = new RPCLink({ plugins: [ new RetryLinkPlugin(), ], }) ``` ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. ::: ## Usage By default, retries are disabled. To enable retries, set the `retry` count in the request context: ```ts twoslash import { router } from './shared/planet' import { RetryLinkPluginContext } 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 The following context options control retry behavior: * **retry:** Maximum number of retry attempts before throwing an error *(default: `0`)*. * **retryDelay:** Delay between retry attempts *(default: `(o) => o.lastEventRetry ?? 2000`)*. * **shouldRetry:** Function that determines whether a retry should be attempted *(default: `true`)*. You can override the default retry behavior globally by passing `default` options when initializing the plugin: ```ts const link = new RPCLink({ plugins: [ new RetryLinkPlugin({ default: { retry: 0, retryDelay: o => o.lastEventRetry ?? 2000, shouldRetry: o => true, } }), ], }) ``` ::: ## Event Source Simulation 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) } ``` ## Learn More For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/plugins/retry.ts). --- --- url: /docs/plugins/smart-coercion.md --- # Smart Coercion Plugin Automatically converts values to match your schema types without requiring manual coercion logic. ::: warning This plugin improves developer experience, but it adds runtime overhead. For performance sensitive applications or complex schemas, manual coercion in your validation layer is usually more efficient. ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/json-schema@beta ``` ```sh [yarn] yarn add @orpc/json-schema@beta ``` ```sh [pnpm] pnpm add @orpc/json-schema@beta ``` ```sh [bun] bun add @orpc/json-schema@beta ``` ```sh [deno] deno add npm:@orpc/json-schema@beta ``` ::: ## Setup Use `SmartCoercionHandlerPlugin` in your handler to coerce incoming request data to the expected `.input` schema: ```ts import { SmartCoercionHandlerPlugin } from '@orpc/json-schema' const handler = new OpenAPIHandler(router, { plugins: [ new SmartCoercionHandlerPlugin({ converters: [ new ZodToJsonSchemaConverter(), // Add other schema converters as needed ], }) ] }) ``` Use `SmartCoercionLinkPlugin` in your link to coerce server responses to the expected `.output` or `.errors` schemas: ```ts import { SmartCoercionLinkPlugin } from '@orpc/json-schema' const link = new OpenAPILink(contract, { plugins: [ new SmartCoercionLinkPlugin(contract, { converters: [ new ZodToJsonSchemaConverter(), // Add other schema converters as needed ], }), ] }) ``` ::: info This plugin relies on [JSON Schema Converters](/docs/openapi/specification#json-schema-converters) to determine how values should be coerced. Configure the appropriate converter for each validation library you use. If a required converter is unavailable, it automatically falls back to [Standard Json Schema](https://standardschema.dev/json-schema) conversion. ::: ## How It Works The plugin coerces values safely by following these rules: 1. **Schema-driven:** Converts only when the schema defines the target type 2. **Safe only:** Converts only values with an unambiguous representation, such as `'123'` to `123` 3. **Preserve original values:** Leaves the original value unchanged when conversion would be unsafe 4. **Union-aware:** Picks the best match for union types 5. **Deep conversion:** Applies recursively inside nested objects and arrays ::: info JSON Schema does not natively represent `BigInt`, `Date`, `RegExp`, `URL`, `Set`, or `Map`. For these types, oRPC relies on `x-native-type` metadata in your schema: * `x-native-type: 'bigint'` for BigInt * `x-native-type: 'date'` for Date * `x-native-type: 'regexp'` for RegExp * `x-native-type: 'url'` for URL * `x-native-type: 'set'` for Set * `x-native-type: 'map'` for Map The built-in [Standard Json Schema](https://standardschema.dev/json-schema) converter handles these cases. Because this metadata is outside the official JSON Schema specification, custom converters may need to add the appropriate `x-native-type` values explicitly. ::: ## Conversion Rules ### String → Boolean Supports these specific string values, case-insensitively: * `'true'`, `'on'` → `true` * `'false'`, `'off'` → `false` ::: info HTML `` elements commonly submit `'on'` or `'off'`, so this conversion is especially useful for form handling. ::: ### String → Number Supports valid numeric strings: * `'123'` → `123` * `'3.14'` → `3.14` ### String/Number → BigInt Supports valid numeric strings or numbers: * `'12345678901234567890'` → `12345678901234567890n` * `12345678901234567890` → `12345678901234567890n` ### String → Date Supports ISO date and datetime strings: * `'2023-10-01'` → `new Date('2023-10-01')` * `'2020-01-01T06:15'` → `new Date('2020-01-01T06:15')` * `'2020-01-01T06:15Z'` → `new Date('2020-01-01T06:15Z')` * `'2020-01-01T06:15:00Z'` → `new Date('2020-01-01T06:15:00Z')` * `'2020-01-01T06:15:00.123Z'` → `new Date('2020-01-01T06:15:00.123Z')` ### String → RegExp Supports valid regular expression strings: * `'/^\\d+$/i'` → `new RegExp('^\\d+$', 'i')` * `'/abc/'` → `new RegExp('abc')` ### String → URL Supports valid URL strings: * `'https://example.com'` → `new URL('https://example.com')` ### Array → Set Supports arrays of **unique values**: * `['apple', 'banana']` → `new Set(['apple', 'banana'])` ### Array → Object Converts arrays into objects with numeric keys: * `['apple', 'banana']` → `{ 0: 'apple', 1: 'banana' }` ::: info This is particularly useful for [Bracket Notation](/docs/openapi/bracket-notation#limitations) when you need objects with numeric keys. ::: ### Array → Map Supports arrays of key-value pairs with **unique keys**: * `[['key1', 'value1'], ['key2', 'value2']]` → `new Map([['key1', 'value1'], ['key2', 'value2']])` ## Advanced Usage You can also use this plugin in guides such as [Expanding Type Support for OpenAPI Link](/docs/advanced/expanding-type-support-for-openapi-link). ## Learn More For implementation details, see the [SmartCoercionHandlerPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/json-schema/src/v2/smart-coercion-handler-plugin.ts) or the [SmartCoercionLinkPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/json-schema/src/v2/smart-coercion-link-plugin.ts). --- --- url: /docs/helpers/base64url.md --- # Base64Url Helpers Base64Url helpers provide functions to encode and decode base64url strings, a URL-safe variant of base64 encoding used in web tokens, data serialization, and APIs. ## Basic Usage ```ts twoslash import { decodeBase64url, encodeBase64url } from '@orpc/server/helpers' const originalText = 'Hello World' const textBytes = new TextEncoder().encode(originalText) const encodedData = encodeBase64url(textBytes) const decodedBytes = decodeBase64url(encodedData) const decodedText = new TextDecoder().decode(decodedBytes) // 'Hello World' ``` ::: info The `decodeBase64url` accepts `undefined` or `null` as encoded value and returns `undefined` for invalid inputs, enabling seamless handling of optional data. ::: --- --- url: /docs/helpers/cookie.md --- # Cookie Helpers Cookie helpers provide utilities for setting and reading HTTP cookies from fetch headers. ## Basic Usage ```ts twoslash import { deleteCookie, getCookie, setCookie } from '@orpc/server/helpers' const reqHeaders = new Headers() const resHeaders = new Headers() setCookie(resHeaders, 'sessionId', 'abc123', { secure: true, maxAge: 3600 }) deleteCookie(resHeaders, 'sessionId') const sessionId = getCookie(reqHeaders, 'sessionId') ``` ::: info Both helpers accept `undefined` as headers for seamless integration with plugins like [Request Headers](/docs/plugins/request-headers) or [Response Headers](/docs/plugins/response-headers). ::: ## Security with Signing and Encryption Combine cookies with [signing](/docs/helpers/signing) or [encryption](/docs/helpers/encryption) for enhanced security: ```ts twoslash import { getCookie, setCookie, sign, unsign } from '@orpc/server/helpers' const secret = 'your-secret-key' const reqHeaders = new Headers() const resHeaders = new Headers() setCookie(resHeaders, 'sessionId', await sign('abc123', secret), { httpOnly: true, secure: true, maxAge: 3600 }) const signedSessionId = await unsign(getCookie(reqHeaders, 'sessionId'), secret) ``` --- --- url: /docs/helpers/encryption.md --- # Encryption Helpers Encryption helpers provide functions to encrypt and decrypt sensitive data using AES-GCM with PBKDF2 key derivation. ::: warning Encryption secures data content but has performance trade-offs compared to [signing](/docs/helpers/signing). It requires more CPU resources and processing time. For edge runtimes like [Cloudflare Workers](https://developers.cloudflare.com/workers/), ensure you have sufficient CPU time budget (recommend >200ms per request) for encryption operations. ::: ## Basic Usage ```ts twoslash import { decrypt, encrypt } from '@orpc/server/helpers' const secret = 'your-encryption-key' const sensitiveData = 'user-email@example.com' const encryptedData = await encrypt(sensitiveData, secret) // 'Rq7wF8...' (base64url encoded, unreadable) const decryptedData = await decrypt(encryptedData, secret) // 'user-email@example.com' ``` ::: info The `decrypt` helper accepts `undefined` or `null` as encrypted value and returns `undefined` for invalid inputs, enabling seamless handling of optional data. ::: --- --- url: /docs/helpers/form-data.md --- # Form Data Helpers Form data helpers provide utilities for parsing HTML form data and extracting validation error messages, with full support for [bracket notation](/docs/openapi/bracket-notation) to handle complex nested structures. ## `parseFormData` Parses HTML form data using [bracket notation](/docs/openapi/bracket-notation) to deserialize complex nested objects and arrays. ```ts twoslash import { parseFormData } from '@orpc/openapi/helpers' const form = new FormData() form.append('name', 'John') form.append('user[email]', 'john@example.com') form.append('user[hobbies][]', 'reading') form.append('user[hobbies][]', 'gaming') const parsed = parseFormData(form) // Result: // { // name: 'John', // user: { // email: 'john@example.com', // hobbies: ['reading', 'gaming'] // } // } ``` ## `getIssueMessage` Extracts validation error messages from [standard schema](https://standardschema.dev/) issues using [bracket notation](/docs/openapi/bracket-notation) paths. ```ts twoslash import { getIssueMessage } from '@orpc/openapi/helpers' const error = { data: { issues: [ { path: ['user', 'email'], message: 'Invalid email format' } ] } } const emailError = getIssueMessage(error, 'user[email]') // Returns: 'Invalid email format' const tagError = getIssueMessage(error, 'user[tags][]') // Returns error message for any array item const anyError = getIssueMessage('anything', 'path') // Returns undefined if cannot find issue ``` ::: warning The `getIssueMessage` utility works with any data type but requires validation errors to follow the [standard schema issue format](https://standardschema.dev/#the-specifications). It looks for issues in the `data.issues` property. If you use custom [validation errors](/docs/advanced/validation-errors), store them elsewhere, or modify the issue format, `getIssueMessage` may not work as expected. ::: ## Usage Example ```tsx import { getIssueMessage, parseFormData } from '@orpc/openapi/helpers' export function ContactForm() { const [error, setError] = useState() const handleSubmit = (form: FormData) => { try { const data = parseFormData(form) // Process structured data } catch (error) { setError(error) } } return (
{getIssueMessage(error, 'user[name]')} {getIssueMessage(error, 'user[emails][]')}
) } ``` --- --- url: /docs/helpers/publisher.md --- # Publisher Helpers Publisher helpers provide a unified way to publish and subscribe to events across different storage backends in oRPC applications. They support both static and dynamic event names, along with optional replay of missed events for subscribers. ## Installation ::: code-group ```sh [npm] npm install @orpc/publisher@beta ``` ```sh [yarn] yarn add @orpc/publisher@beta ``` ```sh [pnpm] pnpm add @orpc/publisher@beta ``` ```sh [bun] bun add @orpc/publisher@beta ``` ```sh [deno] deno add npm:@orpc/publisher@beta ``` ::: ## Basic Usage The core concept is the `Publisher` interface, which defines a standard way to publish events and subscribe to them. You can create your own publisher or use one of the provided adapters for popular storage backends. The `publish` method accepts an event name and payload, while `subscribe` lets you listen to specific events using either callback or iterator styles. ```ts twoslash import { MemoryPublisher } from '@orpc/publisher/memory' import { os } from '@orpc/server' import * as z from 'zod' // ---cut--- const publisher = new MemoryPublisher<{ 'something-updated': { id: string } }>() const live = os .handler(async function* ({ input, signal, lastEventId }) { const iterator = publisher.subscribe('something-updated', { signal, lastEventId }) for await (const payload of iterator) { // Handle payload here or yield directly to client yield payload } }) const publish = os .input(z.object({ id: z.string() })) .handler(async ({ input }) => { await publisher.publish('something-updated', { id: input.id }) }) ``` ::: tip The publisher supports both static and dynamic event names. ```ts const publisher = new MemoryPublisher>() ``` ::: ## Adapters | Name | Replay Support | Adapter for | | ------------------ | -------------- | ---------------------------------------------------- | | `MemoryPublisher` | ✅ | In-memory storage | | `RedisPublisher` | ✅ | [Redis](https://github.com/redis/redis) | | `UpstashPublisher` | ✅ | [Upstash Redis](https://github.com/upstash/redis-js) | ::: code-group ```ts [memory] import { MemoryPublisher } from '@orpc/publisher/memory' ``` ```ts [redis] import { createClient } from 'redis' import { RedisPublisher } from '@orpc/publisher/redis' const client = createClient({ url: 'redis://localhost:6379' }) // RedisRateLimiter lazily connects to Redis when needed. // You can still call `client.connect()` manually, but it is optional. await client.connect() const publisher = new RedisPublisher(client, { subscriber: client.duplicate(), // Redis client for subscribing to pub/sub (default: client.duplicate()) prefix: 'orpc:', // Optional Redis key prefix serializer: undefined, // Optional custom serializer }) ``` ```ts [upstash] import { Redis } from '@upstash/redis' import { UpstashPublisher } from '@orpc/publisher/upstash' const redis = Redis.fromEnv() const publisher = new UpstashPublisher(redis, { prefix: 'orpc:', // Optional Redis key prefix serializer: undefined, // Optional custom serializer }) ``` ::: ## Replay Missing Events Some adapters can replay events missed while a subscriber is offline. This feature is usually disabled by default, but you can enable it when creating the publisher. When enabled, the publisher automatically manages event ids and attempts to replay events since the last event id provided by the subscriber. ```ts const publisher = new MemoryPublisher({ replay: { enabled: true, // Enable replaying missed events seconds: 60 * 5, // TTL in seconds } }) const iterator = publisher.subscribe('something-updated', { signal, lastEventId, // The publisher will attempt to replay missed events since this event id }) ``` ::: warning When replay is enabled, the publisher manages event ids automatically. This means: * Any event id provided during publishing is ignored * When subscribing, you must preserve and forward the event id when yielding custom payloads ```ts import { getEventMeta, withEventMeta } from '@orpc/server' const live = os .handler(async function* ({ input, signal, lastEventId }) { const iterator = publisher.subscribe('something-updated', { signal, lastEventId }) for await (const payload of iterator) { // Preserve event id when yielding custom payloads const id = getEventMeta(payload)?.id yield withEventMeta({ custom: 'value' }, { id }) } }) const publish = os .input(z.object({ id: z.string() })) .handler(async ({ input }) => { // The event id 'this-will-be-ignored' will be replaced by the publisher await publisher.publish( 'something-updated', withEventMeta({ id: input.id }, { id: 'this-will-be-ignored' }) ) }) ``` ::: ### Client Reconnection On the client, you can use the [Retry Plugin](/docs/plugins/retry), which automatically controls and passes `lastEventId` to the server when reconnecting. Alternatively, you can manage `lastEventId` manually: ```ts import { getEventMeta } from '@orpc/client' let lastEventId: string | undefined while (true) { try { const iterator = await client.live('input', { lastEventId }) for await (const payload of iterator) { lastEventId = getEventMeta(payload)?.id // Update lastEventId console.log(payload) } } catch { await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second before retrying } } ``` --- --- url: /docs/helpers/ratelimit.md --- # Rate Limit Helpers Rate Limit helpers provide a unified set of adapters, middleware, and handler plugins for adding rate limiting to oRPC applications. They are flexible and composable, so you can use different rate-limiting strategies and storage backends without changing your procedure code. ## Installation ::: code-group ```sh [npm] npm install @orpc/ratelimit@beta ``` ```sh [yarn] yarn add @orpc/ratelimit@beta ``` ```sh [pnpm] pnpm add @orpc/ratelimit@beta ``` ```sh [bun] bun add @orpc/ratelimit@beta ``` ```sh [deno] deno add npm:@orpc/ratelimit@beta ``` ::: ## Basic Usage The core concept is the `RateLimiter` interface, which defines a standard way to check and enforce rate limits. You can create your own custom limiter or use one of the provided adapters for popular storage backends. The `limit` method accepts a key and an optional `weight` value, which defaults to `1`, so a single request can consume multiple points. ```ts twoslash import { MemoryRateLimiter } from '@orpc/ratelimit/memory' // ---cut--- import { ORPCError } from '@orpc/server' const limiter = new MemoryRateLimiter({ maxRequests: 5, window: 60000, }) const result = await limiter.limit('user:123', { weight: 2 }) if (!result.success) { throw new ORPCError('TOO_MANY_REQUESTS', { data: { limit: result.limit, remaining: result.remaining, reset: result.reset, }, }) } ``` ## Adapters The package includes adapters for multiple storage backends and runtimes. Each adapter might require `maxRequests` and `window` to configure the limit, along with adapter specific options. | Name | Blocking Mode | Adapter for | | ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------- | | `MemoryRateLimiter` | ✅ | In-memory storage | | `RedisRateLimiter` | ✅ | [Redis](https://github.com/redis/redis) | | `UpstashRateLimiter` | ✅ | [Upstash Rate Limit](https://www.npmjs.com/package/@upstash/ratelimit) | | `CloudflareRateLimiter` | | [Cloudflare RateLimit Binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) | ::: code-group ```ts [memory] import { MemoryRateLimiter } from '@orpc/ratelimit/memory' const limiter = new MemoryRateLimiter({ maxRequests: 10, // Maximum requests allowed window: 60000, // Time window in milliseconds (60 seconds) }) ``` ```ts [redis] import { RedisRateLimiter } from '@orpc/ratelimit/redis' import { createClient } from 'redis' const client = createClient({ url: 'redis://localhost:6379' }) // RedisRateLimiter lazily connects to Redis when needed. // You can still call `client.connect()` manually, but it is optional. await client.connect() const limiter = new RedisRateLimiter(client, { prefix: 'orpc:', // Optional Redis key prefix maxRequests: 10, // Maximum requests allowed window: 60000, // Time window in milliseconds (60 seconds) }) ``` ```ts [cloudflare] import { CloudflareRateLimiter } from '@orpc/ratelimit/cloudflare' export default { async fetch(request, env) { // env.MY_RATE_LIMITER is a Cloudflare Workers Rate Limiting binding // https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/ const limiter = new CloudflareRateLimiter(env.MY_RATE_LIMITER, { prefix: 'orpc:', // Optional key prefix }) } } ``` ```ts [upstash] import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' import { UpstashRateLimiter } from '@orpc/ratelimit/upstash' const redis = Redis.fromEnv() const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, '60 s'), prefix: 'orpc:', // Optional key prefix }) const limiter = new UpstashRateLimiter(ratelimit, { waitUntil: ctx.waitUntil.bind(ctx), // Pass waitUntil for Edge runtime support }) ``` ::: ### Blocking Mode Some adapters support blocking mode, which waits until capacity becomes available instead of rejecting requests immediately. ```ts const limiter = new MemoryRateLimiter({ maxRequests: 10, window: 60000, blockingUntilReady: { enabled: true, // Disabled by default timeout: 5000, // Wait up to 5 seconds }, }) ``` ## Ratelimit Middleware The `ratelimit` helper creates middleware that enforces rate limits for [procedures](/docs/procedure). ```ts import { ratelimit, RateLimiter } from '@orpc/ratelimit' const procedure = os .$context<{ ratelimiter: RateLimiter }>() .input(z.object({ email: z.email() })) .use( ratelimit({ limiter: ({ context }) => context.ratelimiter, key: ({ context }, input) => `login:${input.email}`, weight: 1, // Optional weight for each request, default is 1 }), ) .handler(({ input }) => { return { success: true } }) const ratelimiter = new MemoryRateLimiter({ maxRequests: 10, window: 60000, }) const result = await call( procedure, { email: 'user@example.com' }, { context: { ratelimiter } } ) ``` ::: info Automatic Deduplication When the same `limiter` and `key` combination is used multiple times in a single request chain, the `ratelimit` middleware performs the rate limit check only once. This behavior follows the [Dedupe Middleware Best Practice](/docs/best-practices/dedupe-middleware). To disable deduplication, set `dedupe: false`. ::: ::: tip Conditional Limiter You can choose different limiters dynamically based on the request context: ```ts const premiumLimiter = new MemoryRateLimiter({ maxRequests: 100, window: 60000, }) const standardLimiter = new MemoryRateLimiter({ maxRequests: 10, window: 60000, }) const result = await call( procedure, { email: 'user@example.com' }, { context: { ratelimiter: isPremiumUser ? premiumLimiter : standardLimiter, }, }, ) ``` ::: ## Handler Plugin The `RateLimitHandlerPlugin` automatically adds HTTP rate limiting headers (`RateLimit-*` and `Retry-After`) to responses when used with [Ratelimit Middleware](#ratelimit-middleware). This lets clients inspect the current limit state and know when they can retry after hitting a limit. ```ts import { RateLimitHandlerPlugin } from '@orpc/ratelimit' const handler = new RPCHandler(router, { plugins: [ new RateLimitHandlerPlugin(), ], }) ``` ::: info You can combine this plugin with [Retry After Plugin](/docs/plugins/retry-after) to enable automatic client-side retries based on server rate limiting headers. ::: ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: --- --- url: /docs/helpers/signing.md --- # Signing Helpers Signing helpers provide functions to cryptographically sign and verify data using HMAC-SHA256. ::: info Signing is faster than [encryption](/docs/helpers/encryption) but users can view the original data. ::: ## Basic Usage ```ts twoslash import { getSignedValue, sign, unsign } from '@orpc/server/helpers' const secret = 'your-secret-key' const userData = 'user123' const signedValue = await sign(userData, secret) // 'user123.oneQsU0r5dvwQFHFEjjV1uOI_IR3gZfkYHij3TRauVA' // ↑ Original data is visible to users const verifiedValue = await unsign(signedValue, secret) // 'user123' // Extract value without verification const extractedValue = getSignedValue(signedValue) // 'user123' ``` ::: info The `unsign` and `getSignedValue` helpers accept `undefined` or `null` as signed value and return `undefined` for invalid inputs, enabling seamless handling of optional data. ::: --- --- url: /docs/integrations/effect.md --- # Effect Integration [Effect](https://effect.website/) integration lets you seamlessly use Effect's powerful features, such as its effect system, concurrency model, and schema library, within oRPC. ::: warning This guide assumes familiarity with [Effect](https://effect.website/). Review the official documentation if needed. ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/experimental-effect@beta effect@beta ``` ```sh [yarn] yarn add @orpc/experimental-effect@beta effect@beta ``` ```sh [pnpm] pnpm add @orpc/experimental-effect@beta effect@beta ``` ```sh [bun] bun add @orpc/experimental-effect@beta effect@beta ``` ```sh [deno] deno add npm:@orpc/experimental-effect@beta npm:effect@beta ``` ::: ## Effectful Handlers `handlerGen` allows you to write effectful handlers using generator functions. Inside the generator, you can yield Effect operations, and `handlerGen` will handle the execution and error handling for you. ```ts twoslash import { os } from '@orpc/server' // ---cut--- import { handlerGen } from '@orpc/experimental-effect' import { Effect } from 'effect' const procedure = os.handler(handlerGen(function* ({ input, context }) { // You can use Effect's features here, such as concurrency, error handling, etc. const result = yield* Effect.promise(() => Promise.resolve(5)) return result })) ``` ### `.effect` extension Import `@orpc/experimental-effect/extensions/effect` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This adds an `.effect` method to the builder so you can write effectful handlers directly. ::: code-group ```ts [usage] const procedure = base.effect(function* ({ input, context }) { // You can use Effect's features here, such as concurrency, error handling, etc. const result = yield* Effect.promise(() => Promise.resolve(5)) return result }) ``` ```ts [setup] import '@orpc/experimental-effect/extensions/effect' import { os } from '@orpc/server' export const base = os ``` ::: ### Effect Services You can provide Effect services through the oRPC context in a typesafe way with `WithEffectContext` and `~effect/context`: ```ts twoslash import { call, os } from '@orpc/server' // ---cut--- import { handlerGen, WithEffectContext } from '@orpc/experimental-effect' import { Context, Effect } from 'effect' class Random extends Context.Tag('MyRandomService')< Random, { readonly next: Effect.Effect } >() {} interface ServerContext extends WithEffectContext {} const procedure = os .$context() .handler(handlerGen(function* ({ input, context }) { const random = yield* Random const result = yield* random.next return result })) const random = await call(procedure, undefined, { context: { '~effect/context': Context.empty().pipe( Context.add(Random, { next: Effect.succeed(Math.random()), }), ) } }) ``` ::: info You can also extend the Effect context with [middleware](/docs/middleware): ```ts const procedure = os .$context() .use(({ context, next }) => { return next({ context: { '~effect/context': context['~effect/context'].pipe( Context.add(AdditionService, {}), ) } }) }) .handler(handlerGen(function* ({ input, context }) { const additionService = yield* AdditionService })) ``` ::: ### Error Handling This integration preserves the original error whenever possible. If you call `Effect.fail(error)`, the error is forwarded to [middleware](/docs/middleware) and interceptors, just like a regular thrown error. To customize this behavior, wrap the effect before execution using `~effect/wrap` in the context: ```ts import { Context, Effect } from 'effect' interface ServerContext extends WithEffectContext {} export async function fetch(request: Request) { const { response } = await handler.fetch(request, { context: { '~effect/context': Context.empty(), '~effect/wrap': (effect, opts) => effect.pipe( Effect.catchAllCause((cause) => { }) ), } }) return response ?? new Response('Not Found', { status: 404 }) } ``` ::: info For app level error handling, we recommend [middleware](/docs/middleware) or interceptors. ::: ### Typesafe Errors When you `yield* Effect.fail(new ORPCError(...))` or `return new ORPCError(...)`, oRPC treats it as a [returned ORPCError](/docs/error-handling#returning-an-orpcerror). On the client, you can handle these errors in a typesafe way: ```ts const procedure = os.handler(handlerGen(function* ({ errors }) { if (resourceNotFound) { yield* Effect.fail(new ORPCError('NOT_FOUND', { message: 'The resource you are looking for does not exist', })) // -- or - return new ORPCError('NOT_FOUND', { message: 'The resource you are looking for does not exist', }) } return 'Success' })) const [error, result] = await call(procedure) if (isInferableError(error)) { // typesafe error handling } ``` ## Effect Schema oRPC natively supports [Standard Schema](https://standardschema.dev/schema#what-schema-libraries-implement-the-spec), and [Effect Schema](https://effect.website/docs/schema/introduction/) implements that spec through [Schema.standardSchemaV1](https://effect.website/docs/schema/standard-schema/): ```ts import { Schema } from 'effect' const procedure = os .input(Schema.standardSchemaV1(Schema.Struct({ name: Schema.String }))) .handler(handlerGen(function* ({ input, context }) { return `Hello ${input.name}!` })) ``` ### `.input` and `.output` Extensions Import `@orpc/experimental-effect/extensions/input-output` from a module that always runs during initialization, such as the file where you define your base builder or create your server. This lets you define `.input` and `.output` directly with Effect Schema: ::: code-group ```ts [usage] const procedure = base .input(Schema.Struct({ name: Schema.String })) .output(Schema.Struct({ greeting: Schema.String })) .handler(handlerGen(function* ({ input, context }) { return { greeting: `Hello ${input.name}!` } })) ``` ```ts [setup] import '@orpc/experimental-effect/extensions/input-output' import { os } from '@orpc/server' export const base = os ``` ::: ::: info You can also use these extensions with the [contract builder](/docs/contract/procedure). ::: ### JSON Schema Converter This integration also provides `EffectSchemaToJsonSchemaConverter`, built on top of [Effect Schema to JSON Schema](https://effect.website/docs/schema/json-schema/). You can use it with tools such as the [OpenAPI Generator](/docs/openapi/specification#openapi-generator): ```ts import { EffectSchemaToJsonSchemaConverter } from '@orpc/experimental-effect' const generator = new OpenAPIGenerator({ converters: [new EffectSchemaToJsonSchemaConverter()], }) ``` ## OpenTelemetry Integration First, set up the [oRPC OpenTelemetry integration](/docs/integrations/opentelemetry). Then instrument your Effect to work seamlessly with OpenTelemetry by providing `TracingLive` through `~effect/wrap` in the context. This makes Effect tracing equivalent to OpenTelemetry tracing: ```ts import { Resource, Tracer } from '@effect/opentelemetry' import { Context, Effect, Layer } from 'effect' interface ServerContext extends WithEffectContext {} const TracingLive = Tracer.layerGlobal.pipe( Layer.provide(Resource.layerFromEnv()), ) export async function fetch(request: Request) { const { response } = await handler.fetch(request, { context: { '~effect/context': Context.empty(), '~effect/wrap': (effect, opts) => effect.pipe(Effect.provide(TracingLive)), } }) return response ?? new Response('Not Found', { status: 404 }) } ``` --- --- url: /docs/integrations/evlog.md --- # Evlog Integration [Evlog](https://evlog.dev/) integration for oRPC adds structured logging so you can trace requests, monitor errors, and inspect application behavior. ::: warning This guide assumes familiarity with [Evlog](https://evlog.dev/). Review the official documentation if needed. ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/evlog@beta evlog@beta ``` ```sh [yarn] yarn add @orpc/evlog@beta evlog@beta ``` ```sh [pnpm] pnpm add @orpc/evlog@beta evlog@beta ``` ```sh [bun] bun add @orpc/evlog@beta evlog@beta ``` ```sh [deno] deno add npm:@orpc/evlog@beta npm:evlog@beta ``` ::: ## Setup Use `EvlogHandlerPlugin` to instrument your handler with structured logs, request tracking, and error monitoring. ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { EvlogHandlerPlugin } from '@orpc/evlog' const handler = new RPCHandler(router, { plugins: [ new EvlogHandlerPlugin({ drain: undefined, // <- custom Evlog drain (optional) plugins: [], // <- additional Evlog plugins (optional) logAbort: true, // <- log when requests are aborted (disabled by default) }), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ## Using the Logger in Your Code This plugin supports using [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) to access the logger throughout a request and enrich the final [wide event](https://www.evlog.dev/learn/wide-events#uselogger-retrieving-the-request-logger). It is the most convenient way to use Evlog's full feature set. If your runtime does not support AsyncLocalStorage, you can still [access the logger from the context](#without-asynclocalstorage). ::: code-group ```ts [business logic] import { createLoggerStorage } from '@orpc/evlog/node' /** * Pass `storage` to the plugin configuration. * Call `useLogger` inside a procedure to access the request logger. */ export const { storage, useLogger } = createLoggerStorage() const procedure = os .handler(async () => { const logger = useLogger() // [!code highlight] logger?.set({ user: { id: 123, name: 'John Doe' } }) // [!code highlight] await logger.fork('child-procedure', () => { const logger = useLogger() // [!code highlight] }) return { success: true } }) ``` ```ts [handler setup] const handler = new RPCHandler(router, { plugins: [ new EvlogHandlerPlugin({ storage, // <- pass the storage to the plugin }), ], }) ``` ::: ### Without AsyncLocalStorage If you do not want to use AsyncLocalStorage, or your runtime does not support it, you can still read the logger from the context. ```ts import { getLogger, LoggerContext } from '@orpc/evlog' interface ServerContext extends LoggerContext {} // [!code highlight] const procedure = os .$context() .handler(({ context }) => { const logger = getLogger(context) // [!code highlight] logger?.set({ user: { id: 123, name: 'John Doe' } }) // [!code highlight] return { success: true } }) ``` --- --- url: /docs/integrations/next.md --- # Next.js Integration [Next.js](https://nextjs.org/) integration provides utilities for using oRPC in Next.js applications, including support for [server functions](https://nextjs.org/docs/app/api-reference/directives/use-server) and form actions. ## Installation ::: code-group ```sh [npm] npm install @orpc/next@beta ``` ```sh [yarn] yarn add @orpc/next@beta ``` ```sh [pnpm] pnpm add @orpc/next@beta ``` ```sh [bun] bun add @orpc/next@beta ``` ```sh [deno] deno add npm:@orpc/next@beta ``` ::: ## Server Functions Use `createServerFunction` to turn a [procedure](/docs/procedure) into a [server function](https://nextjs.org/docs/app/api-reference/directives/use-server). It accepts the same options as [server-side clients](/docs/client/server-side#router-clients), and the returned function accepts the same input as the original procedure. ```ts twoslash 'use server' import { os } from '@orpc/server' import { createServerFunction } from '@orpc/next' const procedure = os.handler(async () => 'Hello from oRPC + Next.js!') export const serverFunction = createServerFunction(procedure, { context: async () => { // <- provide initial context if needed return { user: { id: '123', name: 'Alice' } } }, interceptors: [] // <- add interceptors if needed }) ``` You can call the returned `serverFunction` from a client component. ```tsx 'use client' import { serverFunction } from './path/to/server/function' export default function Page() { const handleClick = async () => { const [error, message] = await serverFunction() if (!error) { console.log({ message }) } } return (
) } ``` Special Next.js errors such as [redirect](https://nextjs.org/docs/app/api-reference/functions/redirect) and [notFound](https://nextjs.org/docs/app/api-reference/functions/not-found) are rethrown so Next.js handles them normally. All other errors are serialized to `ORPCErrorJSON` and returned as the first element of the tuple. ### Typesafe Errors [Typesafe errors](/docs/error-handling#typesafe-errors) are supported as well. Because errors are serialized before they reach the client, use the `inferable` field to distinguish errors. ::: code-group ```tsx [client] 'use client' import { serverFunction } from './path/to/server/function' export default function Page() { const handleClick = async () => { const [error, message] = await serverFunction() if (error) { if (error.inferable) { // handle typesafe error } else { // handle unknown error } } else { // handle success case } } return (
) } ``` ```ts [server] 'use server' const procedure = os .errors({ NOT_FOUND: { message: 'The resource was not found', }, }) .handler(async ({ errors }) => { throw errors.NOT_FOUND() }) export const serverFunction = createServerFunction(procedure) ``` ::: ### `createServerFunctionable` If you reuse the same options across multiple server functions, `createServerFunctionable` creates a preconfigured helper. The helper takes a procedure and returns a value that works as both a server function and the original [procedure](/docs/procedure) on the server. ```ts import { createServerFunctionable } from '@orpc/next' const functionable = createServerFunctionable({ context: async () => { // <- provide initial context if needed return { user: { id: '123', name: 'Alice' } } }, }) // Works as both a server function and a procedure. export const functionableProcedure = functionable( os.handler(async () => 'Hello from oRPC + Next.js!') ) ``` ### `.actionable` Extension Import `@orpc/next/extensions/actionable` from a module that always runs during initialization, such as the file where you define your base builder. This adds an `.actionable` method to decorated procedures. Like `createServerFunctionable`, it returns a value that works as both a server function and a [procedure](/docs/procedure). ::: code-group ```ts [usage] export const functionableProcedure = base .handler(async () => 'Hello from oRPC + Next.js!') .actionable({ context: async () => { // <- provide initial context if needed return { user: { id: '123', name: 'Alice' } } }, }) ``` ```ts [setup] import '@orpc/next/extensions/actionable' import { os } from '@orpc/server' export const base = os ``` ::: ### Hooks This integration also includes React hooks for server functions. `useServerFunction` executes a server function and tracks its status. `useOptimisticServerFunction` does the same, with optimistic updates. Unlike direct server function calls, hook errors are deserialized into native `ORPCError` instances instead of plain JSON (`ORPCErrorJSON`) for a more natural developer experience. ::: code-group ```tsx [useServerFunction] 'use client' import { useServerFunction } from '@orpc/next/hooks' import { getIssueMessage, isInferableError, onErrorDeferred, parseFormData, } from '@orpc/next/hooks' export function MyComponent() { const { execute, data, error, status } = useServerFunction(serverFunction, { interceptors: [ onErrorDeferred((error) => { if (isInferableError(error)) { console.error(error.data) // ^ Typed error data } }), ], }) return (
execute(parseFormData(form))}> {getIssueMessage(error, 'name')} {status === 'pending' &&

Loading...

}
) } ``` ```tsx [useOptimisticServerFunction] 'use client' import { useOptimisticServerAction } from '@orpc/next/hooks' import { getIssueMessage, onSuccessDeferred, parseFormData, } from '@orpc/next' export function MyComponent() { const [todos, setTodos] = useState([]) const { execute, optimisticState } = useOptimisticServerAction(someAction, { optimisticPassthrough: todos, optimisticReducer: (currentState, newTodo) => [...currentState, newTodo], interceptors: [ onSuccessDeferred(({ data }) => { setTodos(prevTodos => [...prevTodos, data]) }), ], }) return (
    {optimisticState.map(todo => (
  • {todo.todo}
  • ))}
execute(parseFormData(form))}> {getIssueMessage(error, 'todo')}
) } ``` ::: ::: info Besides hooks, this integration also re-exports [form-data helpers](/docs/helpers/form-data) for working with `FormData`, as well as deferred interceptors for updating UI states: `onStartDeferred`, `onSuccessDeferred`, `onErrorDeferred`, and `onFinishDeferred`. ::: ::: info You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors. ::: ## Server Form Functions Use `createServerFormFunction` to turn a procedure into a form action for `
`. Unlike `createServerFunction`, the returned function accepts `FormData` instead of the procedure input. It deserializes that data using [Bracket Notation](/docs/openapi/bracket-notation), then passes the result to the procedure. ::: code-group ```tsx [client] export default function Page() { return (
) } ``` ```ts [server] 'use server' import { redirect } from 'next/navigation' const procedure = os .input(z.object({ name: z.string() })) .handler(async ({ input }) => { // do something }) export const serverFormFunction = createServerFormFunction(procedure, { interceptors: [ async ({ next }) => { await next() redirect('/thank-you') // redirect on success } ] }) ``` ::: ### `createServerFormFunctionable` If you reuse the same options across multiple form actions, `createServerFormFunctionable` creates a preconfigured helper. Like [`createServerFunctionable`](#createserverfunctionable), it takes a procedure and returns a value that works as both a server form function and the original [procedure](/docs/procedure). ```ts import { createServerFormFunctionable } from '@orpc/next' const formFunctionable = createServerFormFunctionable({ context: async () => { // <- provide initial context if needed return { user: { id: '123', name: 'Alice' } } }, }) // Works as both a server form function and a procedure. export const formFunctionableProcedure = formFunctionable( os.handler(async () => 'Hello from oRPC + Next.js!') ) ``` --- --- url: /docs/integrations/opentelemetry.md --- # OpenTelemetry Integration [OpenTelemetry](https://opentelemetry.io/) integration adds automatic instrumentation to oRPC applications, enabling distributed tracing and performance monitoring with minimal setup. ::: warning This guide assumes familiarity with [OpenTelemetry](https://opentelemetry.io/). Review the official documentation if needed. ::: ![oRPC OpenTelemetry Integration Preview](/images/opentelemetry-integration-preview.png) ::: info See the complete example in our [Bun WebSocket + OpenTelemetry Playground](/docs/playgrounds). ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/opentelemetry@beta ``` ```sh [yarn] yarn add @orpc/opentelemetry@beta ``` ```sh [pnpm] pnpm add @orpc/opentelemetry@beta ``` ```sh [bun] bun add @orpc/opentelemetry@beta ``` ```sh [deno] deno add npm:@orpc/opentelemetry@beta ``` ::: ## Setup To integrate OpenTelemetry with oRPC, use `ORPCInstrumentation`. It automatically instruments both client and server for distributed tracing. ::: code-group ```ts twoslash [server] import { NodeSDK } from '@opentelemetry/sdk-node' import { ORPCInstrumentation } from '@orpc/opentelemetry' const sdk = new NodeSDK({ instrumentations: [ new ORPCInstrumentation(), // [!code highlight] ], }) sdk.start() ``` ```ts twoslash [client] import { WebTracerProvider } from '@opentelemetry/sdk-trace-web' import { registerInstrumentations } from '@opentelemetry/instrumentation' import { ORPCInstrumentation } from '@orpc/opentelemetry' const provider = new WebTracerProvider() provider.register() registerInstrumentations({ instrumentations: [ new ORPCInstrumentation(), // [!code highlight] ], }) ``` ::: ::: info You can configure OpenTelemetry for your server, client, or both, depending on your needs. ::: ## Context Propagation By default, `ORPCInstrumentation` enables [context propagation](https://opentelemetry.io/docs/concepts/context-propagation/) between the client and server. You can disable it by setting `propagationEnabled` to `false` if you do not need it or if another instrumentation already handles it. ```ts const instrumentation = new ORPCInstrumentation({ propagationEnabled: false, }) ``` ::: warning Popular instrumentations that already handle context propagation include [@hono/otel](https://www.npmjs.com/package/@hono/otel), [@opentelemetry/instrumentation-http](https://www.npmjs.com/package/@opentelemetry/instrumentation-http), and [@opentelemetry/instrumentation-fetch](https://www.npmjs.com/package/@opentelemetry/instrumentation-fetch). ::: ## Middleware Span oRPC automatically creates spans for each [middleware](/docs/middleware) execution. You can access the active span to customize attributes, events, and other span data: ```ts import { trace } from '@opentelemetry/api' export const someMiddleware = os.middleware(async (ctx, next) => { const span = trace.getActiveSpan() span?.setAttribute('someAttribute', 'someValue') span?.addEvent('someEvent') return next() }) Object.defineProperty(someMiddleware, 'name', { value: 'someName', }) ``` ::: tip Define the `name` property on your middleware to improve span naming and make traces easier to read. ::: ## Capture Abort Signals If your application heavily uses [Event Iterator](/docs/event-iterator) or similar streaming patterns, we recommend capturing an event when the `signal` is aborted to properly track and detach unexpected long-running operations: ```ts import { trace } from '@opentelemetry/api' const handler = new RPCHandler(router, { interceptors: [ ({ request, next }) => { const span = trace.getActiveSpan() request.signal?.addEventListener('abort', () => { span?.addEvent('aborted', { reason: String(request.signal?.reason) }) }) return next() }, ], }) ``` --- --- url: /docs/integrations/pino.md --- # Pino Integration [Pino](https://getpino.io/) integration for oRPC provides structured logging capabilities, allowing you to easily track requests, monitor errors, and gain insights into your application's behavior. ::: warning This guide assumes familiarity with [Pino](https://getpino.io/). Review the official documentation if needed. ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/pino@beta pino@beta ``` ```sh [yarn] yarn add @orpc/pino@beta pino@beta ``` ```sh [pnpm] pnpm add @orpc/pino@beta pino@beta ``` ```sh [bun] bun add @orpc/pino@beta pino@beta ``` ```sh [deno] deno add npm:@orpc/pino@beta npm:pino@beta ``` ::: ## Setup To set up Pino with oRPC, use the `PinoHandlerPlugin` class. This plugin automatically instruments your handler with structured logging, request tracking, and error monitoring. ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import { PinoHandlerPlugin } from '@orpc/pino' import pino from 'pino' const logger = pino() const handler = new RPCHandler(router, { plugins: [ new PinoHandlerPlugin({ logger, // <- custom logger instance generateRequestId: ({ request }) => crypto.randomUUID(), // <- custom request id generator logLifecycle: true, // <- log information about request lifecycle (disabled by default) logAbort: true, // <- log information when requests are aborted (disabled by default) }), ], }) ``` ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: ::: tip For improved log readability during development, consider using [pino-pretty](https://github.com/pinojs/pino-pretty) to format your logs in a human-friendly way. ```bash npm run dev | npx pino-pretty ``` ::: ## Using the Logger in Your Code You can access the logger from the context object using the `getLogger` function: ```ts import { getLogger, LoggerContext } from '@orpc/pino' interface ServerContext extends LoggerContext {} // [!code highlight] const procedure = os .$context() .handler(({ context }) => { const logger = getLogger(context) // [!code highlight] logger?.info('Processing request') logger?.debug({ userId: 123 }, 'User data') return { success: true } }) ``` ## Providing Custom Logger per Request You can provide a custom logger instance for specific requests by passing it through the context. This is especially useful when integrating with [pino-http](https://github.com/pinojs/pino-http) for enhanced HTTP logging: ```ts import { LOGGER_CONTEXT_SYMBOL, LoggerContext, PinoHandlerPlugin } from '@orpc/pino' const logger = pino() const httpLogger = pinoHttp({ logger }) interface ServerContext extends LoggerContext {} // [!code highlight] const router = { ping: os.$context().handler(() => 'pong') } const handler = new RPCHandler(router, { plugins: [ new PinoHandlerPlugin({ logger }), // [!code highlight] ], }) const server = createServer(async (req, res) => { httpLogger(req, res) const { matched } = await handler.handle(req, res, { prefix: '/api', context: { [LOGGER_CONTEXT_SYMBOL]: req.log, // [!code highlight] }, }) if (!matched) { res.statusCode = 404 res.end('Not Found') } }) ``` --- --- url: /docs/integrations/tanstack-query.md --- # TanStack Query Integration [TanStack Query](https://tanstack.com/query/latest) integration provides utilities for using oRPC clients with TanStack Query. It includes helper methods for building query and mutation options, as well as query and mutation keys. ::: warning This guide assumes you are already familiar with [TanStack Query](https://tanstack.com/query/latest). If you need a refresher, review the official TanStack Query documentation before continuing. ::: ## Installation ::: code-group ```sh [npm] npm install @orpc/tanstack-query@beta ``` ```sh [yarn] yarn add @orpc/tanstack-query@beta ``` ```sh [pnpm] pnpm add @orpc/tanstack-query@beta ``` ```sh [bun] bun add @orpc/tanstack-query@beta ``` ```sh [deno] deno add npm:@orpc/tanstack-query@beta ``` ::: ## Setup Before you begin, set up either a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts twoslash import { client } from './shared/planet' // ---cut--- import { createTanstackQueryUtils } from '@orpc/tanstack-query' const orpc = createTanstackQueryUtils(client) orpc.planet.find.queryOptions({ input: { id: 123 } }) // ^| // // // // // // ``` ::: details Avoiding Query and Mutation Key Conflicts? To avoid key conflicts, pass a unique base path when creating each set of utils: ```ts const userORPC = createTanstackQueryUtils(userClient, { path: ['user'] }) const postORPC = createTanstackQueryUtils(postClient, { path: ['post'] }) ``` ::: ## Query Options Use `.queryOptions` to build query options. It works with `useQuery`, `useSuspenseQuery`, and `prefetchQuery`, and any other API that accepts query options. ```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... })) ``` ## Streamed Query Options Use `.streamedOptions` to build streamed query options for [Event Iterator](/docs/event-iterator). The resulting data is an array of events, and each new event is appended as it arrives. It works with `useQuery`, `useSuspenseQuery`, and `prefetchQuery`, and any other API that accepts query options. ```ts const query = useQuery(orpc.streamed.streamedOptions({ input: { id: 123 }, // Specify input if needed context: { cache: true }, // Provide client context if needed queryFnOptions: { // Configure streamed query behavior refetchMode: 'reset', maxChunks: 3, }, retry: true, // Infinite retry for more reliable streaming // additional options... })) ``` ::: info `refetchMode` determines how data is handled when the query is fetched again: * `'reset'` *(default)*: Clears existing data and returns the query to a pending state. * `'append'`: Adds new streamed chunks to the existing data. * `'replace'`: Buffers streamed data and replaces the cache after the stream completes. ::: ## Live Query Options Use `.liveOptions` to build live query options for [Event Iterator](/docs/event-iterator). The data always reflects the latest event, replacing the previous value whenever a new one arrives. It works with `useQuery`, `useSuspenseQuery`, and `prefetchQuery`, and any other API that accepts query options. ```ts const query = useQuery(orpc.live.liveOptions({ input: { id: 123 }, // Specify input if needed context: { cache: true }, // Provide client context if needed retry: true, // Infinite retry for more reliable streaming // additional options... })) ``` ## Infinite Query Options Use `.infiniteOptions` to build infinite query options. It works with `useInfiniteQuery`, `useSuspenseInfiniteQuery`, and `prefetchInfiniteQuery`, and any other API that accepts infinite query options. ::: info The `input` option must be a function that receives the page parameter and returns the query input. Define the `pageParam` type explicitly 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 build mutation options. It works with `useMutation` and any other API that accepts mutation options. ```ts const mutation = useMutation(orpc.planet.create.mutationOptions({ context: { cache: true }, // Provide client context if needed // additional options... })) mutation.mutate({ name: 'Earth' }) ``` ## Query and Mutation Keys oRPC provides helper methods for generating query and mutation keys: * `.key`: Generates a **partial-match** key for actions such as invalidating queries or checking mutation status. * `.queryKey`: Generates a **full-match** key for [Query Options](#query-options). * `.streamedKey`: Generates a **full-match** key for [Streamed Query Options](#streamed-query-options). * `.liveKey`: Generates a **full-match** key for [Live Query Options](#live-query-options). * `.infiniteKey`: Generates a **full-match** key for [Infinite Query Options](#infinite-query-options). * `.mutationKey`: Generates a **full-match** key for [Mutation Options](#mutation-options). ```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 } }) }) // Update the planet find query with id 123 queryClient.setQueryData(orpc.planet.find.queryKey({ input: { id: 123 } }), (old) => { return { ...old, id: 123, name: 'Earth' } }) ``` ## Calling Clients The `.call` method provides direct access to the underlying procedure client when needed. ```ts const planet = await orpc.planet.find.call({ id: 123 }) ``` ## Reactive Options In reactive libraries like Vue or Solid, TanStack Query supports passing computed values as options. The exact API varies by framework, so refer to the TanStack Query documentation for [Vue](https://tanstack.com/query/latest/docs/framework/vue/reactivity) or [Solid](https://tanstack.com/query/latest/docs/framework/solid/reference/useQuery#reactive-options). ::: code-group ```ts [Options as Function] const query = useQuery( () => orpc.planet.find.queryOptions({ input: { id: id() }, }) ) ``` ```ts [Computed Options] const query = useQuery(computed( () => orpc.planet.find.queryOptions({ input: { id: id.value }, }) )) ``` ::: ## Default Options Use `scoped` to configure default options for scoped query and mutation utilities. Each value can be either a partial options object, which is spread-merged with lower priority than per-call options, or a function that receives the per-call options and returns the merged result. ```ts const orpc = createTanstackQueryUtils(client, { scoped: { planet: { find: { queryKey: options => ({ // Override the auto-generated query key for .queryKey and .queryOptions queryKey: options.queryKey ?? ['planet', 'find', options.input] }), queryOptions: { staleTime: 60 * 1000, // 1 minute retry: 3, }, }, list: { infiniteOptions: options => ({ ...options, staleTime: 30 * 1000, // override takes priority }), }, create: { mutationOptions: { onSuccess: (output, input, _, ctx) => { ctx.client.invalidateQueries({ queryKey: orpc.planet.key() }) }, }, }, }, }, }) // These calls automatically use the default options const query = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 } })) const mutation = useMutation(orpc.planet.create.mutationOptions()) // User-provided options take precedence const customQuery = useQuery(orpc.planet.find.queryOptions({ input: { id: 123 }, staleTime: 0, // overrides the default staleTime })) ``` ::: info When you configure `queryKey`, it also affects `.queryOptions` because it is used internally to generate query keys. The same applies to live, streamed, infinite, and mutation options when you configure their keys. ::: ## Interceptors Interceptors let you wrap `queryFn` and `mutationFn` calls. Unlike [default options](#default-options), which can be overridden by per-call options, interceptors always run for every query and mutation. ```ts import { isInferableError, safe } from '@orpc/client' const orpc = createTanstackQueryUtils(client, { queryInterceptors: [], liveInterceptors: [], streamedInterceptors: [], infiniteInterceptors: [], mutationInterceptors: [ async ({ context, path, next }) => { const [error, data] = await safe(next()) if (error) { if (isInferableError(error)) { // handle typesafe errors } throw error } return data } ], scoped: { planet: { create: { mutationInterceptors: [ async ({ next, fnContext }) => { const result = await next() fnContext.client.invalidateQueries({ queryKey: orpc.planet.key() }) return result }, ], }, }, }, }) ``` ::: info You can use [`safe` and `isInferableError`](/docs/client/error-handling#using-safe-and-isinferableerror) together for typesafe error handling in interceptors. ::: ## Plugins Plugins package reusable defaults and interceptors for queries and mutations. ```ts const orpc = createTanstackQueryUtils(client, { plugins: [] }) ``` ## Client Context ::: warning oRPC excludes [client context](/docs/client/client-side#client-context) from query keys. Override the query key manually when you need to prevent unintended query deduplication. ```ts const query = useQuery(orpc.planet.find.queryOptions({ context: { cache: true }, // manually include context in the query key queryKey: [['planet', 'find'], { context: { cache: true } }], // additional options... })) ``` ::: When a client is invoked through the TanStack Query integration, an **operation context** is automatically added to the [client context](/docs/client/client-side#client-context). You can use this context to configure request behavior, such as selecting the HTTP method for [RPC Link](/docs/rpc/link#request-method). ```ts twoslash import { RPCLink } from '@orpc/client/fetch' // ---cut--- import { TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL, TanstackQueryOperationContext, } from '@orpc/tanstack-query' interface ClientContext extends TanstackQueryOperationContext { } const GET_OPERATION_TYPE = new Set(['query', 'streamed', 'live', 'infinite']) const link = new RPCLink({ method: ({ context }) => { const operationType = context[TANSTACK_QUERY_OPERATION_CONTEXT_SYMBOL]?.type if (operationType && GET_OPERATION_TYPE.has(operationType)) { return 'GET' } return 'POST' }, }) ``` ## Typesafe Error Handling Use the built-in `isInferableError` helper to handle [typesafe errors](/docs/error-handling#typesafe-errors) in queries and mutations. ```ts import { isInferableError } from '@orpc/client' const mutation = useMutation(orpc.planet.create.mutationOptions({ onError: (error) => { if (isInferableError(error)) { // Handle typesafe errors here } } })) mutation.mutate({ name: 'Earth' }) if (mutation.error && isInferableError(mutation.error)) { // Handle the typesafe errors here } ``` ## `skipToken` for Disabling Queries The [skipToken symbol](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries#typesafe-disabling-of-queries-using-skiptoken) provides a typesafe alternative to setting `enabled: false` when you want to disable a query by omitting its `input`. ```ts const query = useQuery( orpc.planet.list.queryOptions({ input: search ? { search } : skipToken, // [!code highlight] }) ) const query = useInfiniteQuery( orpc.planet.list.infiniteOptions({ input: search // [!code highlight] ? (offset: number | undefined) => ({ limit: 10, offset, search }) // [!code highlight] : skipToken, // [!code highlight] initialPageParam: undefined, getNextPageParam: lastPage => lastPage.nextPageParam, }) ) ``` ## Custom Serializers If needed, you can extend the default TanStack Query serializer to support additional types supported by oRPC. Learn more about [RPC Serializers](/docs/rpc/serializer) and [TanStack Query Server Rendering & Hydration](https://tanstack.com/query/latest/docs/framework/react/guides/ssr). ```ts import { RPCSerializer } from '@orpc/client' const serializer = new RPCSerializer({ handlers: { // put custom serializers here }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { queryKeyHashFn(queryKey) { const serialized = serializer.serialize(queryKey, { useFormDataForBlobFields: false }) return JSON.stringify(serialized) }, staleTime: 60 * 1000, // > 0 to prevent immediate refetching on mount }, dehydrate: { serializeData(data) { return serializer.serialize(data, { useFormDataForBlobFields: false }) } }, hydrate: { deserializeData(data) { return serializer.deserialize(data) } }, } }) ``` --- --- url: /docs/best-practices/dedupe-middleware.md --- # Dedupe Middleware Use [context](/docs/context) to prevent the same [middleware](/docs/middleware) from repeating expensive work. ## Problem The same middleware can run more than once during a single call. This often happens when: * a procedure [calls](/docs/client/server-side#one-off-calls) another procedure that both use the same middleware * you use `.use(authProvider).router(router)`, and some procedures in `router` already use `authProvider` :::warning Repeated middleware work can hurt performance, especially for expensive operations such as opening a database connection. ::: ## Solution Store the computed value in `context` and reuse it when the middleware runs again. For example, this middleware loads auth at most once per call: ```ts twoslash import { os } from '@orpc/server' declare function loadAuth(headers: Headers): Promise<{ id: string } | undefined> // ---cut--- const authProvider = os .$context<{ headers: Headers, auth?: { id: string } | undefined, authLoaded?: boolean | undefined }>() .middleware(async ({ context, next }) => { // reuse the loaded auth value if it was already loaded const auth = context.authLoaded ? context.auth : await loadAuth(context.headers) return next({ context: { auth, authLoaded: true } }) }) ``` You can now apply `authProvider` multiple times without loading auth again: ```ts twoslash import { call, os } from '@orpc/server' declare function loadAuth(headers: Headers): Promise<{ id: string } | undefined> const authProvider = os .$context<{ headers: Headers, auth?: { id: string } | undefined, authLoaded?: boolean | undefined }>() .middleware(async ({ context, next }) => { // reuse the loaded auth value if it was already loaded const auth = context.authLoaded ? context.auth : await loadAuth(context.headers) return next({ context: { auth, authLoaded: true } }) }) // ---cut--- const base = os.$context<{ headers: Headers }>() const foo = base.use(authProvider).handler(({ context }) => 'Hello World') const bar = base.use(authProvider).handler(({ context }) => { // Reuse the auth value that is already stored in context. return call(foo, undefined, { context }) // [!code highlight] }) // Applying authProvider again does not load auth a second time. const router = base .use(authProvider) // [!code highlight] .use(({ next }) => { // Additional middleware logic return next() }) .router({ foo, bar, }) ``` --- --- url: /docs/best-practices/monorepo-setup.md --- # Monorepo Setup A monorepo stores multiple related projects in a single repository, a common practice for managing interconnected projects like web applications and their APIs. This guide shows you how to efficiently set up a monorepo with oRPC while maintaining end-to-end type safety across all projects. ::: info Playground Explore a sample monorepo setup in our [Multiservice Monorepo Playground](https://github.com/middleapi/orpc-multiservice-monorepo-playground). ::: ## TypeScript Project References When consuming, some parts of the client may end up being typed as `any` because the client environment doesn't have access to all types that oRPC procedures depend on. The most effective solution is to use [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html). This ensures the client can resolve all types used by oRPC procedures while also improving TypeScript performance. ::: code-group ```json [client/tsconfig.json] { "compilerOptions": { // ... }, "references": [ { "path": "../server" } // [!code highlight] ] } ``` ```json [server/tsconfig.json] { "compilerOptions": { "composite": true // [!code highlight] // ... } } ``` ::: ::: tip Common `composite` option's constraint The most common issue with `composite` is missing type definitions, resulting in: `The inferred type of "X" cannot be named without a reference to "Y". This is likely not portable. A type annotation is necessary.` If you encounter this, try installing package `Y` if not already installed and adding this to your codebase where the error occurs: ```ts import type * as _A from '../../node_modules/detail_Y_path_here' ``` ::: ## Recommended Structure * `/apps`: `references` dependencies in `tsconfig.json` * `/packages`: Enable `composite` in `tsconfig.json` The key principle is separating the server component (with `composite` enabled) into a dedicated package containing only necessary files. This approach simplifies dealing with the `composite` option's constraints. ::: tip Avoid **alias imports** inside server components when possible. Instead, use **linked workspace packages** (e.g., [PNPM Workspace protocol](https://pnpm.io/workspaces#workspace-protocol-workspace)). ::: ::: code-group ```txt [Contract First] apps/ ├─ api/ // Import `core-contract` and implement it ├─ web/ // Import `core-contract` and set up @orpc/client here ├─ app/ packages/ ├─ core-contract/ // Define contract with @orpc/contract ├─ .../ ``` ```txt [Service First] apps/ ├─ api/ // Import `core-service` and run it in your environment ├─ web/ // Import `core-service` and set up @orpc/client here ├─ app/ packages/ ├─ core-service/ // Define procedures with @orpc/server ├─ .../ ``` ```txt [Hybrid] apps/ ├─ api/ // Import `core-service` and set up @orpc/server here ├─ web/ // Import `core-contract` and set up @orpc/client here ├─ app/ packages/ ├─ core-contract/ // Define contract with @orpc/contract ├─ core-service/ // Import `core-contract` and implement it ├─ .../ ``` ::: ::: info This is just a suggestion. You can structure your monorepo however you like. ::: ## Learn More * [Scaling Large Projects](/docs/advanced/scaling-large-projects) * [Publish Client to NPM](/docs/advanced/publish-client-to-npm) --- --- url: /docs/best-practices/no-throw-literal.md --- # 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 [typesafe errors](/docs/client/error-handling#using-safe-and-isinferableerror). Instead, use `null | undefined | {}` (equivalent to `unknown`) for stricter error type inference. ::: ::: warning If `ThrowableError` is configured as `null | undefined | {}`, check `isSuccess` instead of relying on `error`: ```ts const { error, data, isSuccess } = await safe(client('input')) if (!isSuccess) { if (isInferableError(error)) { // handle typesafe errors } // 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/best-practices/optimizing-ssr.md --- # Optimizing Server-Side Rendering (SSR) for Fullstack Frameworks This guide shows how to optimize Server-Side Rendering (SSR) with oRPC in fullstack frameworks such as Next.js, Nuxt, and SvelteKit. The goal is to avoid unnecessary network calls while the server renders a page. ## The Problem with Standard SSR Data Fetching In many fullstack frameworks, SSR still fetches data by making an HTTP request from the server to its own API route. ![Standard SSR: Server calls its own API via HTTP.](/images/standard-ssr-diagram.svg) This works, but it adds avoidable overhead. The server has to go through the HTTP layer just to reach code that is already running in the same process. That extra hop can increase latency and waste resources. Ideally, SSR should fetch data by calling the relevant API logic directly in the same process. ![Optimized SSR: Server calls API logic directly.](/images/optimized-ssr-diagram.svg) With [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) support, you can easily create an internal link that reaches your API logic without making a real network request. For even lower overhead, you can use the [server-side client](#using-server-side-client-directly) directly. ## Conceptual approach ```ts // During SSR, use an internal link const orpc: RouterClient = createORPCClient(internalLink) // In the browser, use a normal remote link const orpc: RouterClient = createORPCClient(remoteLink) ``` 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 an SSR client without bundling server-only code into the browser. ::: info This setup is not limited to [RPC Link](/docs/rpc/link) or [Next.js](https://nextjs.org/). You can use [OpenAPI Link](/docs/openapi/link) or a custom one, and the same pattern works in SvelteKit, Nuxt, and other fullstack frameworks. ::: ::: 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({ origin: () => { if (typeof window === 'undefined') { throw new Error('This link is not allowed on the server side.') } return window.location.origin }, }) /** * Fall back to a browser client when no SSR client is registered. */ export const client: RouterClient = globalThis.$client ?? createORPCClient(link) ``` ```ts [lib/orpc.server.ts] import 'server-only' import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' import type { RouterClient } from '@orpc/server' import { headers } from 'next/headers' const internalLink = new RPCLink({ origin: 'http://localhost', fetch: async (url, init) => { const request = new Request(url, init) // Use a fetch handler here const { response } = await handler.handle(request, { context: { // provide initial context if needed headers: await headers(), }, }) return response ?? new Response('Not Found', { status: 404 }) }, }) globalThis.$client = createORPCClient(internalLink) ``` ::: Import `lib/orpc.server.ts` before other server code so the SSR client is registered early. In Next.js, add it to both `instrumentation.ts` and `app/layout.tsx`: ::: code-group ```ts [instrumentation.ts] export async function register() { // Conditionally import if facing runtime compatibility issues // if (process.env.NEXT_RUNTIME === "nodejs") { await import('./lib/orpc.server') // } } ``` ```ts [app/layout.tsx] import '../lib/orpc.server' // for pre-rendering // Rest of the code ``` ::: With this setup, importing `client` from `lib/orpc.ts` uses the internal-link client during SSR and the remote client in the browser. ## Using Server-Side Client Directly Alternatively, you can use the [server-side client](/docs/client/server-side) directly for SSR. This approach is more efficient and straightforward, as it eliminates serialization and deserialization overhead entirely. ::: info Both a [fetch-based internal link](#implementation) and the [server-side client](/docs/client/server-side) are valid strategies for optimizing SSR. The fetch-based approach offers greater flexibility and plugin compatibility, while the server-side client is more efficient and easier to set up. Choose whichever best fits your needs. ::: ```ts import 'server-only' import { createRouterClient } from '@orpc/server' import { headers } from 'next/headers' globalThis.$client = createRouterClient(router, { /** * Provide initial context if needed. * * Because this client instance is shared across all requests, * only include context that's safe to reuse globally. * For per-request context, use middleware context or pass a function as the initial context. */ context: async () => ({ headers: await headers(), // provide headers if initial context required }), }) ``` ## Using the client The `client` needs no special handling. Use it like any other oRPC client. ```tsx export default async function PlanetListPage() { const planets = await client.planet.list({ limit: 10 }) return (
{planets.map(planet => (
{planet.name}
))}
) } ``` ::: info These examples use Next.js, but the same pattern also works in SvelteKit, Nuxt, and other fullstack frameworks. ::: --- --- url: /docs/advanced/exceeds-the-maximum-length-problem.md --- # Exceeds the Maximum Length Problem TypeScript may report this error when you export a large or complex [router](/docs/router). This is a known TypeScript limitation, not an oRPC bug. TypeScript enforces it to maintain reasonable editor and type-checking performance for large types. ```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 } ``` ## When It Happens You usually see this error when all of the following are true: 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` If you don't need this feature, disable this option in your `tsconfig.json`: ```diff [tsconfig.json] { "compilerOptions": { -- "declaration": true, ++ "declaration": false } } ``` ### 2. Add Explicit Output Types Add `.output` or an explicit handler return type to your procedures. This lets TypeScript use the declared output shape instead of fully expanding the handler implementation, which often improves both type-checking and editor performance. :::tip Use the [type](/docs/procedure#type-utility) utility if you just want to specify the output type without validating the output. ::: ### 3. Split the Router into Smaller Exports If you need `"declaration": true`, avoid exporting a single massive router object from the server. Instead, export smaller router segments and combine them on the client side, where `"declaration": false`: ```ts export const userRouter = { /** ... */ } export const planetRouter = { /** ... */ } export const publicRouter = { /** ... */ } ``` Then define the client type from those smaller exports: ```ts interface Router { user: typeof userRouter planet: typeof planetRouter public: typeof publicRouter } export const client: RouterClient = createORPCClient(link) ``` --- --- url: /docs/advanced/expanding-type-support-for-openapi-link.md --- # Expanding Type Support for OpenAPI Link Because of [OpenAPI Serializer limitations](/docs/openapi/serializer#limitations), values like `Date` and `bigint` are received by the client in JSON-friendly form. You can convert them back to native types on the client with either [Response Validation Plugin](/docs/plugins/response-validation) or [Smart Coercion Plugin](/docs/plugins/smart-coercion), but only under the conditions described below. ## Choose a Plugin * Use [Response Validation Plugin](/docs/plugins/response-validation) when you want manual control over coercion logic and can define explicit coercion rules in your schemas. * Use [Smart Coercion Plugin](/docs/plugins/smart-coercion) when you want automatic coercion based on schema instead of defining coercion logic yourself. ::: warning These plugins can only restore types that the [OpenAPI Serializer](/docs/openapi/serializer) can represent. If you need additional types, extend the serializer first. Nested `Blob` and `File` values are still limited by [Bracket Notation](/docs/openapi/bracket-notation#limitations). ::: ## Use Response Validation Plugin Use [Response Validation Plugin](/docs/plugins/response-validation) when you want to manually control how values are converted back to native types. The coercion rules live in your contract schemas, so the behavior stays explicit and predictable. ```ts const contract = oc.output(z.object({ date: z.coerce.date(), bigint: z.coerce.bigint(), })) const procedure = implement(contract).handler(() => ({ date: new Date(), bigint: 123n, })) ``` The server still returns JSON-friendly data: ```ts const rawOutput = { date: '2025-09-01T07:24:39.000Z', bigint: '123', } ``` With `ResponseValidationLinkPlugin`, the client validates that response and applies your schema coercion before your code uses it. ```ts const output = { date: new Date('2025-09-01T07:24:39.000Z'), bigint: 123n, } ``` ### Setup Add the plugin to your link, then remove the `JsonifiedClient` wrapper from the client type. ```ts import type { RouterContractClient } from '@orpc/contract' import { ResponseValidationLinkPlugin } from '@orpc/contract/plugins' const link = new OpenAPILink(contract, { plugins: [ new ResponseValidationLinkPlugin(contract), // [!code ++] ], }) const client: JsonifiedClient> = createORPCClient(link) // [!code --] const client: RouterContractClient = createORPCClient(link) // [!code ++] ``` ## Use Smart Coercion Plugin Use [Smart Coercion Plugin](/docs/plugins/smart-coercion) when you want the client to coerce values automatically from schema instead of adding coercion logic to each schema manually. ```ts import type { RouterContractClient } from '@orpc/contract' import { SmartCoercionLinkPlugin } from '@orpc/json-schema' const link = new OpenAPILink(contract, { plugins: [ new SmartCoercionLinkPlugin(contract), // [!code ++] ], }) const client: JsonifiedClient> = createORPCClient(link) // [!code --] const client: RouterContractClient = createORPCClient(link) // [!code ++] ``` --- --- url: /docs/advanced/publish-client-to-npm.md --- # Publish Client to NPM Publishing your oRPC client to NPM allows users to easily consume your APIs as a software development kit (SDK). ::: info Before you start, we recommend watching some [publish typescript library to npm tutorials](https://www.youtube.com/results?search_query=publish+typescript+library+to+npm) to get familiar with the process. ::: ## Prerequisites You must have a project already set up with oRPC. [Contract First](/docs/contract/router) is the preferred approach. If you haven't set one up yet, you can clone an [oRPC playground](/docs/playgrounds) and start from there. ::: info In this guide, we'll use [pnpm](https://pnpm.io/) as the package manager and [tsdown](https://tsdown.dev/) for bundling the package. You can use other package managers and bundlers, but the commands may differ. ::: ## Export & Scripts First, create a `src/index.ts` file to set up and export your client. ```ts [src/index.ts] import { createORPCClient } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' import type { RouterContractClient } from '@orpc/contract' export function createMyApi(apiKey: string): RouterContractClient { const link = new RPCLink({ origin: 'https://example.com', url: '/rpc', headers: { 'x-api-key': apiKey, } }) return createORPCClient(link) } ``` ::: info This example uses [RPC Link](/docs/rpc/link) combined with [Contract First](/docs/contract/router) to create a client. This is just an example, you can use any other link or client setup that you prefer. ::: Next, configure your `package.json` with the necessary fields for publishing to NPM. ```json [package.json] { "name": "", // [!code highlight] "type": "module", "version": "0.0.0", // [!code highlight] "publishConfig": { "access": "public" // [!code highlight] }, "exports": { ".": { "types": "./dist/index.d.ts", // [!code highlight] "import": "./dist/index.js", // [!code highlight] "default": "./dist/index.js" // [!code highlight] } }, "files": [ "dist" // [!code highlight] ], "scripts": { "build": "tsdown --dts src/index.ts", // [!code highlight] "release": "pnpm publish" // [!code highlight] }, "dependencies": { "@orpc/client": "...", // [!code highlight] "@orpc/contract": "..." // [!code highlight] // ... other dependencies that `src/index.ts` depends on }, "devDependencies": { "tsdown": "latest", "typescript": "latest" } } ``` ## Build & Publish After completing the necessary setup, commit your changes and run the following commands to build and publish your client to NPM: ```bash pnpm login # if you haven't logged in yet pnpm run build pnpm run release ``` ## Install & Use Once your client is published to NPM, you can install it in your project and use it like this: ```bash pnpm add "" ``` ```ts [example.ts] import { createMyApi } from '' const myApi = createMyApi('your-api-key') const output = await myApi.someMethod('input') ``` ::: info This client includes all oRPC client features, so you can use it with any supported integrations like [Tanstack Query](/docs/integrations/tanstack-query). ::: --- --- url: /docs/advanced/scaling-large-projects.md --- # Scaling Large Projects A single root [client](/docs/client/client-side) is a great way to get started. As your project grows, though, it can lead to type performance issues and tangled dependencies. Splitting the client into smaller service-level clients can help for a while, but very large codebases can still outgrow that approach. This guide shows an alternative pattern for large projects: import and use individual [procedure contracts](/docs/contract/procedure) directly. ## Requirements This pattern depends on one consistency rule: every [procedure contract](/docs/contract/procedure) must define `meta.path`, and that path must exactly match the procedure's location in the root contract. ```ts import { meta, oc } from '@orpc/contract' export const procedure = oc .meta(meta.path(['real', 'path', 'to', 'procedure'])) .input(z.object({ name: z.string() })) .output(z.object({ message: z.string() })) ``` If you use `['real', 'path', 'to', 'procedure']` as the path, the procedure must be mounted at `real.path.to.procedure` in the root contract. This is required for the pattern to work correctly: ```ts import { procedure } from './path/to/procedure' const router = { real: { path: { to: { procedure, }, }, } } ``` ## Contract Caller This pattern does not require a single root client. Instead, you configure a caller that communicates with the server. `createContractCaller` accepts an [RPC Link](/docs/rpc/link), an [OpenAPI Link](/docs/openapi/link), or a custom link. It also accepts options similar to [`createORPCClient`](/docs/client/client-side), but with less typesafe because the full contract is not known up front: ```ts import { createContractCaller } from '@orpc/contract' export const call = createContractCaller(link, { /** options */}) ``` ::: warning If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedCaller` from `@orpc/openapi` instead of `createContractCaller`. ::: You can then call a procedure by importing its contract directly in the client: ```ts import { procedure } from './path/to/procedure' const output = await call(procedure, input, {/** options */}) ``` ### `contractRef` Some integrations still need a root contract. For example, [OpenAPI Link](/docs/openapi/link) and some plugins depend on one. In those cases, `contractRef` can help: ```ts import { RouterContract } from '@orpc/contract' const contractRef: RouterContract = {} const link = new OpenAPILink(contractRef, { plugins: [ new PluginRequireContract(contractRef) ] }) export const call = createContractCaller(link, { contractRef }) ``` The idea behind `contractRef` is simple: every time `call` is used, the caller automatically registers the called procedure contract into `contractRef` at the path defined by `meta.path`. ::: info Some features may not support `contractRef` well. In those cases, import the root contract instead and cast it with `as any` when needed. ::: ## TanStack Query Integration [TanStack Query Integration](/docs/integrations/tanstack-query) also supports this pattern. First, create a factory that accepts a [contract caller](#contract-caller) and options similar to the [TanStack Query interceptor options](/docs/integrations/tanstack-query#interceptors), but with less type safety because the full contract is not known up front: ```ts import { createContractUtilsFactory } from '@orpc/tanstack-query' export const createUtils = createContractUtilsFactory(call, { /** options */}) ``` ::: warning If you are using [OpenAPI Link](/docs/openapi/link), or any link that requires the client to be wrapped in `JsonifiedClient`, use `createContractJsonifiedUtilsFactory` from `@orpc/tanstack-query` instead of `createContractUtilsFactory`. ::: You can then create utilities for each procedure contract: ```ts import { procedure } from './path/to/procedure' const utils = createUtils(procedure) const query = useQuery(utils.queryOptions({/** options */})) ``` --- --- url: /docs/advanced/testing-and-mocking.md --- # Testing and Mocking Testing and mocking are essential for building reliable applications. In this section, we'll explore how to test your procedures and routers effectively, as well as how to create mock implementations for testing purposes. ## Testing For fast, focused tests, use [Server-Side Clients](/docs/client/server-side) or call your procedures directly with `call`. This lets you verify validation, middleware, and handler logic without going through HTTP. ```ts import { call } from '@orpc/server' it('lists planets', async () => { await expect( call(router.planet.list, { page: 1, size: 10 }) ).resolves.toEqual([ { id: '1', name: 'Earth' }, { id: '2', name: 'Mars' }, ]) }) ``` ::: info For a production-like test setup, create [fetch-based internal clients](/docs/best-practices/optimizing-ssr#implementation). ::: ## Mocking Use the [Implementer](/docs/contract/implementation) to create test-specific versions of a [procedure](/docs/procedure) or [router](/docs/router). This is useful when one part of your system depends on another procedure, but your test should not execute the real implementation. ```ts twoslash import { router } from './shared/planet' // ---cut--- import { implement } from '@orpc/server' const fakeListPlanet = implement(router.planet.list).handler(() => []) ``` Use `fakeListPlanet` anywhere your test would normally use the real `listPlanet` procedure. ::: info `implement` is also useful for building mock servers in frontend tests. ::: ::: warning `implement` does not support [lazy routers](/docs/router#lazy-router) directly. If you need to mock one, first [unlazy the router](/docs/contract/router#router-to-contract). ::: --- --- url: /docs/advanced/validation-errors.md --- # Validation Errors oRPC includes built-in validation errors that work well for most cases. Customize them when you need a different message or error shape. ## Customizing You can catch validation errors with [interceptors](/docs/rpc/handler#interceptors), [client interceptors](/docs/rpc/handler#client-interceptors), or [middleware](/docs/middleware) applied before `.input` or `.output`. ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' import { router } from './shared/planet' // ---cut--- import * as z from 'zod' import { ORPCError, ValidationError } from '@orpc/server' const handler = new RPCHandler(router, { interceptors: [ async ({ next }) => { try { return await next() } catch (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 z.ZodError(error.cause.issues as z.core.$ZodIssue[]) throw new ORPCError('INPUT_VALIDATION_FAILED', { message: z.prettifyError(zodError), data: z.flattenError(zodError), cause: error, }) } if ( error instanceof ORPCError && error.code === 'INTERNAL_SERVER_ERROR' && error.cause instanceof ValidationError ) { // do not expose validation details for output validation errors throw new ORPCError('OUTPUT_VALIDATION_FAILED', { cause: error, }) } throw error } }, ], }) ``` ## Typesafe Validation Errors As explained in the [error handling guide](/docs/error-handling#orpcerror-compatibility), if you throw an `ORPCError` whose `code` and `data` match an error defined with `.errors`, oRPC treats it the same as `errors.[code]`. This does not work in [interceptors](/docs/rpc/handler#interceptors). Use [client interceptors](/docs/rpc/handler#client-interceptors) or [middleware](/docs/middleware) applied before `.input` or `.output` instead. ```ts twoslash import { RPCHandler } from '@orpc/server/fetch' // ---cut--- import { ORPCError, os, ValidationError } from '@orpc/server' import * as z 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.uuid() })) .handler(() => { /** do something */ }) const handler = new RPCHandler({ example }, { clientInterceptors: [ async ({ next }) => { try { return await next() } catch (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 z.ZodError(error.cause.issues as z.core.$ZodIssue[]) throw new ORPCError('INPUT_VALIDATION_FAILED', { message: z.prettifyError(zodError), data: z.flattenError(zodError), cause: error, }) } throw error } }, ], }) ``` --- --- url: /docs/migrations/from-trpc.md --- # Migrating from tRPC This guide shows how to migrate an existing tRPC app to oRPC. Because oRPC is heavily inspired by tRPC, most concepts map directly, so the migration should feel familiar. ::: info If you want to add oRPC features to an existing tRPC app without a full migration, see [tRPC Integration](/docs/openapi/integrations/trpc). ::: ## Core Concepts Comparison | Concept | tRPC | oRPC | | --------------------- | ---------------------------- | ------------------- | | **Router** | `t.router()` | plain object | | **Procedure** | `t.procedure` | `os` | | **Context** | `t.context()` | `os.$context()` | | **Create Middleware** | `t.middleware()` | `os.middleware()` | | **Use Middleware** | `t.procedure.use()` | `os.use()` | | **Input Validation** | `t.procedure.input(schema)` | `os.input(schema)` | | **Output Validation** | `t.procedure.output(schema)` | `os.output(schema)` | | **Error Handling** | `TRPCError` | `ORPCError` | | **Serializer** | `superjson` | built-in | ::: info See [oRPC vs tRPC Comparison](/docs/comparison) for a broader comparison. ::: ## Step-by-Step Migration ### 1. Installation Remove the tRPC packages and install the oRPC replacements: ::: code-group ```sh [npm] npm uninstall @trpc/server @trpc/client @trpc/tanstack-react-query npm install @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta ``` ```sh [yarn] yarn remove @trpc/server @trpc/client @trpc/tanstack-react-query yarn add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta ``` ```sh [pnpm] pnpm remove @trpc/server @trpc/client @trpc/tanstack-react-query pnpm add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta ``` ```sh [bun] bun remove @trpc/server @trpc/client @trpc/tanstack-react-query bun add @orpc/server@beta @orpc/client@beta @orpc/tanstack-query@beta ``` ```sh [deno] deno remove npm:@trpc/server npm:@trpc/client npm:@trpc/tanstack-react-query deno add npm:@orpc/server@beta npm:@orpc/client@beta npm:@orpc/tanstack-query@beta ``` ::: ### 2. Initialize Initialization is optional in oRPC. You can use `os` directly, but creating shared base procedures makes context and middleware easier to reuse. ::: code-group ```ts [orpc/base.ts] import { ORPCError, os } from '@orpc/server' export async function createORPCContext(opts: { headers: Headers }) { const session = await auth() return { headers: opts.headers, session, } } const o = os.$context>>() const timingMiddleware = o.middleware(async ({ next, path }) => { const start = Date.now() try { return await next() } finally { console.log(`[oRPC] ${path} took ${Date.now() - start}ms to execute`) } }) export const publicProcedure = o.use(timingMiddleware) export const protectedProcedure = publicProcedure.use(({ context, next }) => { if (!context.session?.user) { throw new ORPCError('UNAUTHORIZED') } return next({ context: { session: { ...context.session, user: context.session.user } } }) }) ``` ```ts [trpc/base.ts] import { initTRPC, TRPCError } from '@trpc/server' import superjson from 'superjson' export async function createTRPCContext(opts: { headers: Headers }) { const session = await auth() return { headers: opts.headers, session, } } const t = initTRPC.context().create({ transformer: superjson, }) export const createTRPCRouter = t.router const timingMiddleware = t.middleware(async ({ next, path }) => { const start = Date.now() const result = await next() const end = Date.now() console.log(`[tRPC] ${path} took ${end - start}ms to execute`) return result }) export const publicProcedure = t.procedure.use(timingMiddleware) export const protectedProcedure = t.procedure .use(timingMiddleware) .use(({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }) } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }) }) ``` ::: ::: info Learn more about oRPC [Context](/docs/context) and [Middleware](/docs/middleware). ::: ### 3. Procedures oRPC does not split procedures into `.query`, `.mutation`, and `.subscription`. Use `.handler` for all procedure types. ::: code-group ```ts [orpc/routers/planet.ts] export const planetRouter = { list: publicProcedure .input(z.object({ cursor: z.number().int().default(0) })) .handler(({ input }) => { // Logic here return { planets: [ { name: 'Earth', distanceFromSun: 149.6, } ], nextCursor: input.cursor + 1, } }), create: protectedProcedure .input(z.object({ name: z.string().min(1), distanceFromSun: z.number().positive() })) .handler(async ({ context, input }) => { // Logic here }), } ``` ```ts [trpc/routers/planet.ts] export const planetRouter = createTRPCRouter({ list: publicProcedure .input(z.object({ cursor: z.number().int().default(0) })) .query(({ input }) => { // Logic here return { planets: [ { name: 'Earth', distanceFromSun: 149.6, } ], nextCursor: input.cursor + 1, } }), create: protectedProcedure .input(z.object({ name: z.string().min(1), distanceFromSun: z.number().positive() })) .mutation(async ({ ctx, input }) => { // Logic here }), }) ``` ::: ::: info Learn more about oRPC [Procedures](/docs/procedure). ::: ### 4. App Router The overall router structure stays similar. In oRPC, you do not wrap routers in a `.router` call. A plain object is enough. ::: code-group ```ts [orpc/routers/index.ts] import { planetRouter } from './planet' export const appRouter = { planet: planetRouter, } ``` ```ts [trpc/routers/index.ts] import { planetRouter } from './planet' export const appRouter = createTRPCRouter({ planet: planetRouter, }) ``` ::: ::: info Learn more about oRPC [Router](/docs/router). ::: ### 5. Error Handling Error handling is similar, but `ORPCError` takes the error code as its first argument. ::: code-group ```ts [orpc] throw new ORPCError('BAD_REQUEST', { message: 'Invalid input', data: 'some data', cause: validationError }) ``` ```ts [trpc] throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid input', data: 'some data', cause: validationError }) ``` ::: ::: info Learn more about oRPC [Error Handling](/docs/error-handling). ::: ### 6. Server Setup This example uses [Next.js](https://nextjs.org/). If you use another framework, see [oRPC HTTP Adapters](/docs/adapters/fetch-api). ::: code-group ```ts [app/api/orpc/[[...rest]]/route.ts] import { RPCHandler } from '@orpc/server/fetch' const handler = new RPCHandler(appRouter, { interceptors: [ async ({ next, path }) => { try { return await next() } catch (error) { console.error(`❌ oRPC failed on ${path.join('.')}: `, error) throw error } } ] }) async function handleRequest(request: Request) { const { response } = await handler.handle(request, { prefix: '/api/orpc', context: await createORPCContext({ headers: request.headers }) }) return response ?? new Response('Not found', { status: 404 }) } export const GET = handleRequest export const POST = handleRequest ``` ```ts [app/api/trpc/[trpc]/route.ts] import { fetchRequestHandler } from '@trpc/server/adapters/fetch' function handler(req: Request) { return fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: () => createTRPCContext({ headers: req.headers }), onError: ({ path, error }) => { console.error( `❌ tRPC failed on ${path ?? ''}: ${error.message}` ) } }) } export { handler as GET, handler as POST } ``` ::: ### 7. Client Setup Create a transport link, then use it to build a typed client. ::: code-group ```ts [orpc/client.ts] import { createORPCClient, onError } from '@orpc/client' import { RPCLink } from '@orpc/client/fetch' import { RouterClient } from '@orpc/server' const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc', interceptors: [ onError((error) => { console.error(error) }) ], }) export const client: RouterClient = createORPCClient(link) // ---------------- Usage ---------------- const { planets } = await client.planet.list({ cursor: 0 }) ``` ```ts [trpc/client.ts] import { createTRPCProxyClient, httpLink } from '@trpc/client' export const client = createTRPCProxyClient({ links: [ httpLink({ url: 'http://localhost:3000/api/trpc' }) ] }) // ---------------- Usage ---------------- const { planets } = await client.planet.list.query({ cursor: 0 }) ``` ::: ::: info Learn more about oRPC [Client-Side Clients](/docs/client/client-side), [Batch Plugin](/docs/plugins/batch), and [Dedupe Plugin](/docs/plugins/dedupe). ::: ### 8. TanStack Query (React) Integration The TanStack Query integration feels similar to tRPC, but it is lighter. You can use the generated `orpc` utilities directly without a React provider or custom hooks. ::: code-group ```ts [orpc/tanstack-query.ts] import { createTanstackQueryUtils } from '@orpc/tanstack-query' export const orpc = createTanstackQueryUtils(client) // ---------------- Usage in React Components ---------------- const query = useQuery(orpc.planet.list.queryOptions({ input: { cursor: 0 }, })) const infinite = useInfiniteQuery(orpc.planet.list.infiniteOptions({ input: (page: number) => ({ cursor: page }), initialPageParam: 0, getNextPageParam: lastPage => lastPage.nextCursor, })) const mutation = useMutation(orpc.planet.create.mutationOptions()) ``` ```ts [trpc/tanstack-query.ts] import { createTRPCContext } from '@trpc/tanstack-react-query' export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext() // ---------------- Usage in React Components ---------------- const trpc = useTRPC() const query = useQuery(trpc.planet.list.queryOptions({ cursor: 0 })) const infinite = useInfiniteQuery(trpc.planet.list.infiniteQueryOptions( {}, { initialCursor: 0, getNextPageParam: lastPage => lastPage.nextCursor, } )) const mutation = useMutation(trpc.planet.create.mutationOptions()) ``` ::: ::: info Learn more about oRPC [TanStack Query Integration](/docs/integrations/tanstack-query). ::: --- --- url: /learn-and-contribute/mini-orpc/overview.md description: >- A brief introduction to Mini oRPC, a simplified version of oRPC designed for learning purposes. --- # Overview of Mini oRPC Mini oRPC is a simplified implementation of oRPC that includes essential features to help you understand the core concepts. It's designed to be straightforward and easy to follow, making it an ideal starting point for learning about oRPC. ::: info The complete Mini oRPC implementation is available in our GitHub repository: [Mini oRPC Repository](https://github.com/middleapi/mini-orpc) ::: ## Prerequisites Before you begin, ensure you have a solid understanding of [TypeScript](https://www.typescriptlang.org/). Please review the following resources: * [TypeScript Generics Handbook](https://www.typescriptlang.org/docs/handbook/2/generics.html) * [How Theo Deals with Unsafe Packages](https://www.youtube.com/watch?v=JfZPz6PWGtA) You can also practice TypeScript at [typehero.dev](https://typehero.dev/). --- --- url: /learn-and-contribute/mini-orpc/procedure-builder.md description: >- Learn how Mini oRPC's procedure builder provides an excellent developer experience for defining type-safe procedures. --- # Procedure Builder in Mini oRPC The procedure builder is Mini oRPC's core component that enables you to define type-safe procedures with an intuitive, fluent API. ::: info The complete Mini oRPC implementation is available in our GitHub repository: [Mini oRPC Repository](https://github.com/middleapi/mini-orpc) ::: ## Implementation Here is the complete procedure builder system implementation over the basic [procedure](/docs/procedure), [middleware](/docs/middleware), and [context](/docs/context) systems in Mini oRPC: ::: code-group ```ts [server/src/builder.ts] import type { IntersectPick } from '@orpc/shared' import type { Middleware } from './middleware' import type { ProcedureDef, ProcedureHandler } from './procedure' import type { AnySchema, Context, InferSchemaInput, InferSchemaOutput, Schema, } from './types' import { Procedure } from './procedure' export interface BuilderDef< TInitialContext extends Context, TCurrentContext extends Context, TInputSchema extends AnySchema, TOutputSchema extends AnySchema, > extends Omit< ProcedureDef, 'handler' > {} export class Builder< TInitialContext extends Context, TCurrentContext extends Context, TInputSchema extends AnySchema, TOutputSchema extends AnySchema, > { /** * Holds the builder configuration. */ '~orpc': BuilderDef< TInitialContext, TCurrentContext, TInputSchema, TOutputSchema > constructor( def: BuilderDef< TInitialContext, TCurrentContext, TInputSchema, TOutputSchema > ) { this['~orpc'] = def } /** * Sets the initial context type. */ $context(): Builder< U & Record, U, TInputSchema, TOutputSchema > { // `& Record` prevents "has no properties in common" TypeScript errors return new Builder({ ...this['~orpc'], middlewares: [], }) } /** * Creates a middleware function. */ middleware>( middleware: Middleware ): Middleware { // Ensures UOutContext doesn't conflict with current context return middleware } /** * Applies middleware to transform context or enhance the pipeline. */ use>( middleware: Middleware ): Builder< TInitialContext, Omit & UOutContext, TInputSchema, TOutputSchema > { // UOutContext merges with and overrides current context properties return new Builder({ ...this['~orpc'], middlewares: [...this['~orpc'].middlewares, middleware], }) } /** * Sets the input validation schema. */ input( schema: USchema ): Builder { return new Builder({ ...this['~orpc'], inputSchema: schema, }) } /** * Sets the output validation schema. */ output( schema: USchema ): Builder { return new Builder({ ...this['~orpc'], outputSchema: schema, }) } /** * Defines the procedure handler and creates the final procedure. */ handler>( handler: ProcedureHandler< TCurrentContext, InferSchemaOutput, UFuncOutput > ): Procedure< TInitialContext, TCurrentContext, TInputSchema, TOutputSchema extends { initial?: true } ? Schema : TOutputSchema > { // If no output schema is defined, infer it from handler return type return new Procedure({ ...this['~orpc'], handler, }) as any } } export const os = new Builder< Record, Record, Schema, Schema & { initial?: true } >({ middlewares: [], }) ``` ```ts [server/src/procedure.ts] import type { AnyMiddleware } from './middleware' import type { AnySchema, Context } from './types' export interface ProcedureHandlerOptions< TCurrentContext extends Context, TInput, > { context: TCurrentContext input: TInput path: readonly string[] procedure: AnyProcedure signal?: AbortSignal } export interface ProcedureHandler< TCurrentContext extends Context, TInput, THandlerOutput, > { ( opt: ProcedureHandlerOptions ): Promise } export interface ProcedureDef< TInitialContext extends Context, TCurrentContext extends Context, TInputSchema extends AnySchema, TOutputSchema extends AnySchema, > { /** * This property must be optional, because it only available in the type system. * * Why `(type: TInitialContext) => unknown` instead of `TInitialContext`? * You can read detail about this topic [here](https://www.typescriptlang.org/docs/handbook/2/generics.html#variance-annotations) */ __initialContext?: (type: TInitialContext) => unknown middlewares: readonly AnyMiddleware[] inputSchema?: TInputSchema outputSchema?: TOutputSchema handler: ProcedureHandler } export class Procedure< TInitialContext extends Context, TCurrentContext extends Context, TInputSchema extends AnySchema, TOutputSchema extends AnySchema, > { '~orpc': ProcedureDef< TInitialContext, TCurrentContext, TInputSchema, TOutputSchema > constructor( def: ProcedureDef< TInitialContext, TCurrentContext, TInputSchema, TOutputSchema > ) { this['~orpc'] = def } } export type AnyProcedure = Procedure /** * TypeScript only enforces type constraints at compile time. * Checking only `item instanceof Procedure` would fail for objects * that have the same structure as `Procedure` but aren't actual * instances of the `Procedure` class. */ export function isProcedure(item: unknown): item is AnyProcedure { if (item instanceof Procedure) { return true } return ( (typeof item === 'object' || typeof item === 'function') && item !== null && '~orpc' in item && typeof item['~orpc'] === 'object' && item['~orpc'] !== null && 'middlewares' in item['~orpc'] && 'handler' in item['~orpc'] ) } ``` ```ts [server/src/middleware.ts] import type { MaybeOptionalOptions, Promisable } from '@orpc/shared' import type { AnyProcedure } from './procedure' import type { Context } from './types' export type MiddlewareResult = Promisable<{ output: any context: TOutContext }> /** * By conditional checking `Record extends TOutContext` * users can avoid declaring `context` when TOutContext can be empty. * */ export type MiddlewareNextFnOptions = Record< never, never > extends TOutContext ? { context?: TOutContext } : { context: TOutContext } export interface MiddlewareNextFn { >( ...rest: MaybeOptionalOptions> ): MiddlewareResult } export interface MiddlewareOptions { context: TInContext path: readonly string[] procedure: AnyProcedure signal?: AbortSignal next: MiddlewareNextFn } export interface Middleware< TInContext extends Context, TOutContext extends Context, > { ( options: MiddlewareOptions ): Promisable> } export type AnyMiddleware = Middleware ``` ::: ## Router System The router is another essential component of oRPC that organizes procedures into logical groups and handles routing based on procedure paths. It provides a hierarchical structure for your API endpoints. ::: code-group ```ts [server/src/router.ts] import type { Procedure } from './procedure' import type { Context } from './types' /** * Router can be either a single procedure or a nested object of routers. * This recursive structure allows for unlimited nesting depth. */ export type Router = | Procedure | { [k: string]: Router } export type AnyRouter = Router /** * Utility type that extracts the initial context types * from all procedures within a router. */ export type InferRouterInitialContexts = T extends Procedure ? UInitialContext : { [K in keyof T]: T[K] extends AnyRouter ? InferRouterInitialContexts : never; } ``` ::: ## Usage This implementation covers 60-70% of oRPC procedure building. Here are practical examples: ```ts // Define reusable authentication middleware const authMiddleware = os .$context<{ user?: { id: string, name: string } }>() .middleware(async ({ context, next }) => { if (!context.user) { throw new Error('Unauthorized') } return next({ context: { user: context.user } }) }) // Public procedure with input validation 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 }) => { // Fetch planets with pagination return [{ id: 1, name: 'Earth' }] }) // Protected procedure with context and middleware export const createPlanet = os .$context<{ user?: { id: string, name: string } }>() .use(authMiddleware) .input(PlanetSchema.omit({ id: true })) .handler(async ({ input, context }) => { // Create new planet (user is guaranteed to exist via middleware) return { id: 2, name: input.name } }) export const router = { listPlanet, createPlanet, } ``` --- --- url: /learn-and-contribute/mini-orpc/server-side-client.md description: >- Learn how to turn a procedure into a callable function in Mini oRPC, enabling server-side client functionality. --- # Server-side Client in Mini oRPC The server-side client in Mini oRPC transforms procedures into callable functions, enabling direct server-side invocation. This is the foundation of Mini oRPC client system - all other client functionality builds upon it. ::: info The complete Mini oRPC implementation is available in our GitHub repository: [Mini oRPC Repository](https://github.com/middleapi/mini-orpc) ::: ## Implementation Here is the complete implementation of the [server-side client](/docs/client/server-side) functionality in Mini oRPC: ::: code-group ```ts [server/src/procedure-client.ts] import type { Client } from '@mini-orpc/client' import type { MaybeOptionalOptions } from '@orpc/shared' import type { AnyProcedure, Procedure, ProcedureHandlerOptions, } from './procedure' import type { AnySchema, Context, InferSchemaInput, InferSchemaOutput, } from './types' import { ORPCError } from '@mini-orpc/client' import { resolveMaybeOptionalOptions } from '@orpc/shared' import { ValidationError } from './error' export type ProcedureClient< TInputSchema extends AnySchema, TOutputSchema extends AnySchema, > = Client, InferSchemaOutput> /** * context can be optional if `Record extends TInitialContext` */ export type CreateProcedureClientOptions = { path?: readonly string[] } & (Record extends TInitialContext ? { context?: TInitialContext } : { context: TInitialContext }) /** * Turn a procedure into a callable function */ export function createProcedureClient< TInitialContext extends Context, TInputSchema extends AnySchema, TOutputSchema extends AnySchema, >( procedure: Procedure, ...rest: MaybeOptionalOptions> ): ProcedureClient { const options = resolveMaybeOptionalOptions(rest) return (...[input, callerOptions]) => { return executeProcedureInternal(procedure, { context: options.context ?? {}, input, path: options.path ?? [], procedure, signal: callerOptions?.signal, }) } } async function validateInput( procedure: AnyProcedure, input: unknown ): Promise { const schema = procedure['~orpc'].inputSchema if (!schema) { return input } const result = await schema['~standard'].validate(input) if (result.issues) { throw new ORPCError('BAD_REQUEST', { message: 'Input validation failed', data: { issues: result.issues, }, cause: new ValidationError({ message: 'Input validation failed', issues: result.issues, }), }) } return result.value } async function validateOutput( procedure: AnyProcedure, output: unknown ): Promise { const schema = procedure['~orpc'].outputSchema if (!schema) { return output } const result = await schema['~standard'].validate(output) if (result.issues) { throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Output validation failed', cause: new ValidationError({ message: 'Output validation failed', issues: result.issues, }), }) } return result.value } function executeProcedureInternal( procedure: AnyProcedure, options: ProcedureHandlerOptions ): Promise { const middlewares = procedure['~orpc'].middlewares const inputValidationIndex = 0 const outputValidationIndex = 0 const next = async ( index: number, context: Context, input: unknown ): Promise => { let currentInput = input if (index === inputValidationIndex) { currentInput = await validateInput(procedure, currentInput) } const mid = middlewares[index] const output = mid ? ( await mid({ ...options, context, next: async (...[nextOptions]) => { const nextContext: Context = nextOptions?.context ?? {} return { output: await next( index + 1, { ...context, ...nextContext }, currentInput ), context: nextContext, } }, }) ).output : await procedure['~orpc'].handler({ ...options, context, input: currentInput, }) if (index === outputValidationIndex) { return await validateOutput(procedure, output) } return output } return next(0, options.context, options.input) } ``` ```ts [client/src/error.ts] import type { MaybeOptionalOptions } from '@orpc/shared' import { isObject, resolveMaybeOptionalOptions } from '@orpc/shared' export type ORPCErrorOptions = ErrorOptions & { status?: number message?: string } & (undefined extends TData ? { data?: TData } : { data: TData }) export class ORPCError extends Error { readonly code: TCode readonly status: number readonly data: TData constructor( code: TCode, ...rest: MaybeOptionalOptions> ) { const options = resolveMaybeOptionalOptions(rest) if (options?.status && !isORPCErrorStatus(options.status)) { throw new Error('[ORPCError] Invalid error status code.') } super(options.message, options) this.code = code this.status = options.status ?? 500 // Default to 500 if not provided this.data = options.data as TData // data only optional when TData is undefinable so can safely cast here } toJSON(): ORPCErrorJSON { return { code: this.code, status: this.status, message: this.message, data: this.data, } } } export type ORPCErrorJSON = Pick< ORPCError, 'code' | 'status' | 'message' | 'data' > export function isORPCErrorStatus(status: number): boolean { return status < 200 || status >= 400 } export function isORPCErrorJson( json: unknown ): json is ORPCErrorJSON { if (!isObject(json)) { return false } const validKeys = ['code', 'status', 'message', 'data'] if (Object.keys(json).some(k => !validKeys.includes(k))) { return false } return ( 'code' in json && typeof json.code === 'string' && 'status' in json && typeof json.status === 'number' && isORPCErrorStatus(json.status) && 'message' in json && typeof json.message === 'string' ) } ``` ```ts [client/src/types.ts] export interface ClientOptions { signal?: AbortSignal } export type ClientRest = undefined extends TInput ? [input?: TInput, options?: ClientOptions] : [input: TInput, options?: ClientOptions] export interface Client { (...rest: ClientRest): Promise } export type NestedClient = Client | { [k: string]: NestedClient } ``` ::: ## Router Client Creating a client for each procedure individually can be tedious. Here is how to create a router client that handles multiple procedures: ::: code-group ```ts [server/src/router-client.ts] import type { MaybeOptionalOptions } from '@orpc/shared' import type { Procedure } from './procedure' import type { CreateProcedureClientOptions, ProcedureClient } from './procedure-client' import type { AnyRouter, InferRouterInitialContexts } from './router' import { get, resolveMaybeOptionalOptions, toArray } from '@orpc/shared' import { isProcedure } from './procedure' import { createProcedureClient } from './procedure-client' export type RouterClient = TRouter extends Procedure< any, any, infer UInputSchema, infer UOutputSchema > ? ProcedureClient : { [K in keyof TRouter]: TRouter[K] extends AnyRouter ? RouterClient : never; } /** * Turn a router into a chainable procedure client. */ export function createRouterClient( router: T, ...rest: MaybeOptionalOptions< CreateProcedureClientOptions> > ): RouterClient { const options = resolveMaybeOptionalOptions(rest) if (isProcedure(router)) { const caller = createProcedureClient(router, options) return caller as RouterClient } const recursive = new Proxy(router, { get(target, key) { if (typeof key !== 'string') { return Reflect.get(target, key) } const next = get(router, [key]) as AnyRouter | undefined if (!next) { return Reflect.get(target, key) } return createRouterClient(next, { ...options, path: [...toArray(options.path), key], }) }, }) return recursive as unknown as RouterClient } ``` ::: ## Usage Transform any procedure or router into a callable client for server-side use: ```ts // Create a client for a single procedure const procedureClient = createProcedureClient(myProcedure, { context: { userId: '123' }, }) const result = await procedureClient({ input: 'example' }) // Create a client for an entire router const routerClient = createRouterClient(myRouter, { context: { userId: '123' }, }) const result = await routerClient.someProcedure({ input: 'example' }) ``` --- --- url: /learn-and-contribute/mini-orpc/client-side-client.md description: >- Learn how to implement remote procedure calls (RPC) on the client side in Mini oRPC. --- # Client-side Client in Mini oRPC In Mini oRPC, the client-side client initiates remote procedure calls to the server. Both client and server must follow shared conventions to communicate effectively. While we could use the [RPC Protocol](/docs/advanced/rpc-protocol), we'll implement simpler conventions for clarity. ::: info The complete Mini oRPC implementation is available in our GitHub repository: [Mini oRPC Repository](https://github.com/middleapi/mini-orpc) ::: ## Implementation Here's the complete implementation of the [client-side client](/docs/client/client-side) functionality in Mini oRPC: ::: code-group ```ts [server/src/fetch/handler.ts] import { ORPCError } from '@mini-orpc/client' import { get, parseEmptyableJSON } from '@orpc/shared' import { isProcedure } from '../procedure' import { createProcedureClient } from '../procedure-client' import type { Router } from '../router' import type { Context } from '../types' export interface JSONHandlerHandleOptions { prefix?: `/${string}` context: T } export type JSONHandlerHandleResult = | { matched: true, response: Response } | { matched: false, response?: undefined } export class RPCHandler { private readonly router: Router constructor(router: Router) { this.router = router } async handle( request: Request, options: JSONHandlerHandleOptions ): Promise { const prefix = options.prefix const url = new URL(request.url) if ( prefix && !url.pathname.startsWith(`${prefix}/`) && url.pathname !== prefix ) { return { matched: false, response: undefined } } const pathname = prefix ? url.pathname.replace(prefix, '') : url.pathname const path = pathname .replace(/^\/|\/$/g, '') .split('/') .map(decodeURIComponent) const procedure = get(this.router, path) if (!isProcedure(procedure)) { return { matched: false, response: undefined } } const client = createProcedureClient(procedure, { context: options.context, path, }) try { /** * The request body may be empty, which is interpreted as `undefined` input. * Only JSON data is supported for input transfer. * For more complex data types, consider using a library like [SuperJSON](https://github.com/flightcontrolhq/superjson). * Note: oRPC uses its own optimized serialization for internal transfers. */ const input = parseEmptyableJSON(await request.text()) const output = await client(input, { signal: request.signal, }) const response = Response.json(output) return { matched: true, response, } } catch (e) { const error = e instanceof ORPCError ? e : new ORPCError('INTERNAL_ERROR', { message: 'An error occurred while processing the request.', cause: e, }) const response = new Response(JSON.stringify(error.toJSON()), { status: error.status, headers: { 'Content-Type': 'application/json', }, }) return { matched: true, response, } } } } ``` ```ts [client/src/fetch/link.ts] import { parseEmptyableJSON } from '@orpc/shared' import { isORPCErrorJson, isORPCErrorStatus, ORPCError } from '../error' import type { ClientOptions } from '../types' export interface JSONLinkOptions { url: string | URL } export class RPCLink { private readonly url: string | URL constructor(options: JSONLinkOptions) { this.url = options.url } async call( path: readonly string[], input: any, options: ClientOptions ): Promise { const url = new URL(this.url) url.pathname = `${url.pathname.replace(/\/$/, '')}/${path.join('/')}` const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(input), signal: options.signal, }) /** * The request body may be empty, which is interpreted as `undefined` output/error. * Only JSON data is supported for output/error transfer. * For more complex data types, consider using a library like [SuperJSON](https://github.com/flightcontrolhq/superjson). * Note: oRPC uses its own optimized serialization for internal transfers. */ const body = await parseEmptyableJSON(await response.text()) if (isORPCErrorStatus(response.status) && isORPCErrorJson(body)) { throw new ORPCError(body.code, body) } if (!response.ok) { throw new Error( `[ORPC] Request failed with status ${response.status}: ${response.statusText}`, { cause: response } ) } return body } } ``` ::: ## Type-Safe Wrapper We can create a type-safe wrapper for easier client-side usage: ::: code-group ```ts [client/src/client.ts] import type { RPCLink } from './fetch' import type { Client, ClientOptions, NestedClient } from './types' export interface createORPCClientOptions { /** * Base path for all procedures. Useful when calling only a subset of procedures. */ path?: readonly string[] } /** * Create an oRPC client from a link. */ export function createORPCClient( link: RPCLink, options: createORPCClientOptions = {} ): T { const path = options.path ?? [] const procedureClient: Client = async ( ...[input, clientOptions = {} as ClientOptions] ) => { return await link.call(path, input, clientOptions) } const recursive = new Proxy(procedureClient, { get(target, key) { if (typeof key !== 'string') { return Reflect.get(target, key) } return createORPCClient(link, { ...options, path: [...path, key], }) }, }) return recursive as any } ``` ::: ## Usage Simply set up a client and enjoy a server-side-like experience: ```ts const link = new RPCLink({ url: `${window.location.origin}/rpc`, }) export const orpc: RouterClient = createORPCClient(link) const result = await orpc.someProcedure({ input: 'example' }) ``` --- --- url: /learn-and-contribute/mini-orpc/beyond-the-basics.md description: Explore advanced features you can implement in Mini oRPC. --- # Beyond the Basics of Mini oRPC This section explores advanced features and techniques you can implement to enhance Mini oRPC's capabilities. ## Getting Started The complete Mini oRPC implementation is available in the [Mini oRPC Repository](https://github.com/middleapi/mini-orpc), with a [playground](https://github.com/middleapi/mini-orpc/tree/main/playground) for testing. Once you implement a new feature, submit a pull request to the repository for review. ## Feature Suggestions Below are recommended features you can implement in Mini oRPC: ::: info You can implement these features in any order. Pick the ones you find interesting. You can import code from existing oRPC packages to make development easier. ::: * \[ ] [Middleware Typed Input](/docs/middleware#middleware-input) Support ([reference](https://github.com/middleapi/orpc/blob/main/packages/server/src/middleware.ts)) * \[ ] Builder Variants ([reference](https://github.com/middleapi/orpc/blob/main/packages/server/src/builder-variants.ts)) * \[ ] Prevent redefinition of `.input` and `.output` methods * \[ ] [Type-Safe Error](/docs/error-handling#type%E2%80%90safe-error-handling) Support ([reference](https://github.com/middleapi/orpc/blob/main/packages/server/src/procedure-client.ts#L113-L120)) * \[ ] [RPC Protocol](/docs/advanced/rpc-protocol) Implementation ([reference](https://github.com/middleapi/orpc/blob/main/packages/client/src/adapters/standard/rpc-serializer.ts)) * \[ ] Support native types like `Date`, `Map`, `Set`, etc. * \[ ] Support `File`/`Blob` types * \[ ] Support [Event Iterator](/docs/event-iterator) types * \[ ] Multi-runtime support * \[ ] Standard Server Concept ([reference](https://github.com/middleapi/orpc/tree/main/packages/standard-server)) * \[ ] Fetch Adapter ([reference](https://github.com/middleapi/orpc/tree/main/packages/standard-server-fetch)) * \[ ] Node HTTP Adapter ([reference](https://github.com/middleapi/orpc/tree/main/packages/standard-server-node)) * \[ ] Peer Adapter (WebSocket, MessagePort, etc.) ([reference](https://github.com/middleapi/orpc/tree/main/packages/standard-server-peer)) * \[ ] [Contract First](/docs/contract/procedure) Support * \[ ] Contract Builder ([reference](https://github.com/middleapi/orpc/blob/main/packages/contract/src/builder.ts)) * \[ ] Contract Implementer ([reference](https://github.com/middleapi/orpc/blob/main/packages/server/src/implementer.ts)) * \[ ] [OpenAPI](/docs/openapi/getting-started) Support * \[ ] OpenAPI Handler ([reference](https://github.com/middleapi/orpc/blob/main/packages/openapi/src/adapters/standard/openapi-handler.ts)) * \[ ] OpenAPI Generator ([reference](https://github.com/middleapi/orpc/blob/main/packages/openapi/src/openapi-generator.ts)) * \[ ] OpenAPI Link ([reference](https://github.com/middleapi/orpc/blob/main/packages/openapi-client/src/adapters/fetch/openapi-link.ts)) * \[ ] [Tanstack Query](/docs/integrations/tanstack-query) Integration ([reference](https://github.com/middleapi/orpc/tree/main/packages/tanstack-query)) --- --- url: /shared/common-error-status-map-table.md --- | Error Code | HTTP Status Code | | ---------------------- | ---------------: | | BAD\_REQUEST | 400 | | UNAUTHORIZED | 401 | | PAYMENT\_REQUIRED | 402 | | FORBIDDEN | 403 | | NOT\_FOUND | 404 | | METHOD\_NOT\_SUPPORTED | 405 | | NOT\_ACCEPTABLE | 406 | | TIMEOUT | 408 | | CONFLICT | 409 | | GONE | 410 | | PRECONDITION\_FAILED | 412 | | PAYLOAD\_TOO\_LARGE | 413 | | UNSUPPORTED\_MEDIA\_TYPE | 415 | | UNPROCESSABLE\_CONTENT | 422 | | PRECONDITION\_REQUIRED | 428 | | TOO\_MANY\_REQUESTS | 429 | | CLIENT\_CLOSED\_REQUEST | 499 | | INTERNAL\_SERVER\_ERROR | 500 | | NOT\_IMPLEMENTED | 501 | | BAD\_GATEWAY | 502 | | SERVICE\_UNAVAILABLE | 503 | | GATEWAY\_TIMEOUT | 504 | --- --- url: /shared/common-plugin-handler-compatibility.md --- ::: info The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. ::: --- --- url: /shared/common-plugin-link-compatibility.md --- ::: info The `link` can be any supported oRPC link, such as [RPCLink](/docs/rpc/link), [OpenAPILink](/docs/openapi/link), or a custom one. ::: --- --- url: /shared/router-keys-compatibility-warning.md --- ::: warning For compatibility, do not use these router keys: `then`, `bind`, `valueOf`, `toString`, `toJSON`. ::: --- --- url: /shared/standard-server-cors-warning.md --- ::: warning To better support `Blob`, `File`, and `ReadableStream` at the root level in cross-origin scenarios, extend your [CORS allowlist](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) to allow clients to send and receive the `Content-Disposition` and `Standard-Server` headers. Learn more in the [Standard Server documentation](https://github.com/middleapi/standardserver#resolving-body). If you use the [CORS Plugin](/docs/plugins/cors), include them in `allowHeaders` and `exposeHeaders`: ```ts const cors = new CORSHandlerPlugin({ allowHeaders: ['Content-Disposition', 'Standard-Server'], exposeHeaders: ['Content-Disposition', 'Standard-Server'], }) ``` :::