Skip to main content

os — the global builder

os is the root Builder instance. All procedure and router definitions start here.
import { os } from '@orpc/server'

Builder methods

.$context<TContext>()

Sets the expected initial context type for all procedures. Call this first when you want type-safe context.
const authed = os.$context<{ userId: string }>()

const getProfile = authed
  .handler(async ({ context }) => {
    return { id: context.userId }
  })

.$config(config)

Overrides builder-level configuration.
os.$config({
  initialInputValidationIndex: 0,
  initialOutputValidationIndex: 0,
  dedupeLeadingMiddlewares: true,
})

.$meta<TMeta>(initialMeta)

Sets the initial metadata type for procedures.

.$route(initialRoute)

Sets the initial route definition (useful as a router-level default).

.$input<TSchema>(initialSchema)

Sets the initial input schema. Procedures inherit this schema.

.use(middleware)

Adds middleware to the pipeline. Middleware runs for every procedure defined after this call.
const auth = os.use(async ({ context, next }) => {
  const user = await authenticate(context.headers)
  return next({ context: { user } })
})

.errors(errorMap)

Registers type-safe errors. Error constructors are available in procedure handlers via errors.*.
const withErrors = os.errors({
  NOT_FOUND: { status: 404, message: 'Not found' },
  UNAUTHORIZED: { status: 401 },
})

.meta(meta)

Sets or merges metadata onto subsequent procedures.

.route(route)

Sets or merges route options (method, path, tags, etc.) onto subsequent procedures.
os.route({ method: 'GET', path: '/planets', tags: ['planets'] })

.input(schema)

Defines the input validation schema for a procedure.
import * as z from 'zod'

const proc = os.input(z.object({ name: z.string() }))

.output(schema)

Defines the output validation schema for a procedure.

.handler(fn)

Defines the procedure handler. Returns a DecoratedProcedure.
const listPlanets = os
  .input(z.object({ limit: z.number().optional() }))
  .handler(async ({ input, context, errors }) => {
    return [{ id: 1, name: 'Earth' }]
  })

.prefix(path)

Prefixes all procedures in a router with the given path. Only affects procedures with explicit .route({ path: ... }).
const apiRouter = os.prefix('/api').router({ ... })

.tag(...tags)

Adds OpenAPI tags to all procedures in the subsequent router.

.router(router)

Applies all accumulated options (middleware, errors, prefix, tags) to a router.
export const router = os
  .use(authMiddleware)
  .errors({ UNAUTHORIZED: {} })
  .router({
    planet: { list: listPlanets, create: createPlanet },
  })

.lazy(loader)

Creates a lazy-loaded router. The loader is called only when the route is first matched.
export const router = os.lazy(() => import('./planet-router'))

.middleware(fn)

Creates a DecoratedMiddleware (useful for reuse and composition).
const logMiddleware = os.middleware(async ({ next, path }) => {
  console.log('Calling', path)
  return next()
})

ORPCError

Thrown inside procedure handlers or middleware to signal known errors.
import { ORPCError } from '@orpc/server'

throw new ORPCError('NOT_FOUND', {
  message: 'Planet not found',
  data: { id: 42 },
})

// Or use a shorthand from the errors constructor map:
throw errors.NOT_FOUND({ message: 'Custom message' })

Properties

PropertyTypeDescription
codestringThe error code (e.g. 'NOT_FOUND')
statusnumberHTTP status code
messagestringHuman-readable message
dataTDataOptional typed payload
definedbooleantrue if registered in an error map
causeunknownOriginal cause (from ErrorOptions)

Common error codes

BAD_REQUEST (400), UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), CONFLICT (409), UNPROCESSABLE_CONTENT (422), INTERNAL_SERVER_ERROR (500)

Types

RouterClient<TRouter, TClientContext>

Infers a fully type-safe callable client type from a router. Nested procedures become nested async functions.
import type { RouterClient } from '@orpc/server'

const client: RouterClient<typeof router> = createRouterClient(router, {
  context: { headers: req.headers },
})

const planets = await client.planet.list({ limit: 10 })

InferRouterInputs<TRouter>

Infers the input types of all procedures in a router.
import type { InferRouterInputs } from '@orpc/server'

type Inputs = InferRouterInputs<typeof router>
// => { planet: { list: { limit?: number }, create: { name: string } } }

InferRouterOutputs<TRouter>

Infers the output types of all procedures in a router.

createRouterClient()

Creates a server-side client for calling procedures directly (without HTTP).
import { createRouterClient } from '@orpc/server'

const client = createRouterClient(router, {
  context: { userId: '123' },
})

const result = await client.planet.list({ limit: 5 })

lazy()

Wraps a dynamic import in a lazy router for improved cold start times.
import { lazy } from '@orpc/server'

export const router = {
  planet: lazy(() => import('./planet-router')),
}

EventPublisher

Publishes events to multiple async iterator subscribers. Used for SSE/streaming.
import { EventPublisher } from '@orpc/server'

// Generic type maps event names to their payload shapes
const publisher = new EventPublisher<{
  updated: { id: number; name: string }
  deleted: { id: number }
}>()

// Publish an event (synchronous, returns void)
publisher.publish('updated', { id: 1, name: 'Earth' })

// Subscribe via async iterator (for use in a procedure handler)
async function* streamUpdates(signal: AbortSignal) {
  yield* publisher.subscribe('updated', { signal })
}

// Subscribe via callback
const unsubscribe = publisher.subscribe('updated', (payload) => {
  console.log('Planet updated:', payload)
})
unsubscribe() // remove listener

eventIterator()

Used in output schemas to declare that a procedure returns an event iterator (async iterable).
import { eventIterator } from '@orpc/server'
import * as z from 'zod'

const stream = os
  .output(eventIterator(z.object({ type: z.string() })))
  .handler(async function* ({ input }) {
    yield { type: 'start' }
    yield { type: 'done' }
  })

isDefinedError() and safe()

import { isDefinedError, safe } from '@orpc/server'

// Check if an error is a known (registered) ORPCError
if (isDefinedError(error)) {
  console.log(error.code, error.data)
}

// Wrap a procedure call to get [error, data] tuple instead of throwing
const [err, result] = await safe(client.planet.find({ id: 1 }))

Lifecycle hooks

Hooks for observing procedure execution:
import { onStart, onSuccess, onError, onFinish } from '@orpc/server'

const telemetryMiddleware = os.middleware(
  onFinish((result, { path }) => {
    console.log('Finished', path, result.success)
  }),
)
HookCalled when
onStart(fn)Before the handler runs
onSuccess(fn)Handler completes successfully
onError(fn)Handler throws an error
onFinish(fn)Always, after success or error

RPCHandler adapters

Handlers for different runtimes. All support plugins.
import { RPCHandler } from '@orpc/server/fetch'

const handler = new RPCHandler(router, { plugins: [] })

export default {
  fetch(request: Request) {
    return handler.handle(request, { context: {} })
  },
}

Plugins

CORSPlugin

import { CORSPlugin } from '@orpc/server/plugins'

new RPCHandler(router, {
  plugins: [
    new CORSPlugin({
      origin: ['https://app.example.com'],
      allowHeaders: ['Authorization', 'Content-Type'],
    }),
  ],
})

BatchHandlerPlugin

Enables batching multiple procedure calls into a single HTTP request.
import { BatchHandlerPlugin } from '@orpc/server/plugins'

new RPCHandler(router, {
  plugins: [new BatchHandlerPlugin()],
})

SimpleCsrfProtectionHandlerPlugin

Validates a custom header to prevent CSRF attacks.
import { SimpleCsrfProtectionHandlerPlugin } from '@orpc/server/plugins'

new RPCHandler(router, {
  plugins: [new SimpleCsrfProtectionHandlerPlugin()],
})

ResponseHeadersPlugin

Allows procedures to set response headers via context.
import { ResponseHeadersPlugin } from '@orpc/server/plugins'

new RPCHandler(router, {
  plugins: [new ResponseHeadersPlugin()],
})

Build docs developers (and LLMs) love