Skip to main content
A procedure is the fundamental building block in oRPC. It represents one callable API operation: a function that accepts validated input and returns validated output. You build procedures using the os builder from @orpc/server.

Basic procedure

The simplest procedure has no input schema — it just defines a handler:
import { os } from '@orpc/server'

const hello = os.handler(async () => {
  return 'Hello, world!'
})

Defining input and output

Chain .input() and .output() to add Zod (or any Standard Schema-compatible) validators:
import { os } from '@orpc/server'
import * as z from 'zod'

const PlanetSchema = z.object({
  id: z.number().int().min(1),
  name: z.string(),
  description: z.string().optional(),
})

export const findPlanet = os
  .input(PlanetSchema.pick({ id: true }))
  .output(PlanetSchema)
  .handler(async ({ input }) => {
    // input is typed as { id: number }
    return { id: input.id, name: 'Earth' }
    // return type is validated against PlanetSchema
  })
The handler receives an options object with the following properties:
PropertyDescription
inputValidated and parsed input, typed from your input schema
contextRequest-scoped data passed from middleware and the server handler
errorsType-safe error constructors defined with .errors()
pathThe procedure’s path in the router hierarchy
procedureReference to the current procedure
signalAbortSignal from the request
lastEventIdLast event ID for SSE reconnections

Input-only procedure

You can omit .output() — oRPC will infer the return type from the handler:
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' }]
  })

The builder chain

The os builder supports a fluent chain. You can call the steps in this order:
os
  .$context<TContext>()   // set the initial context type
  .errors({ ... })        // declare typed errors
  .use(middleware)        // attach middleware
  .input(schema)          // define input validation
  .output(schema)         // define output validation
  .handler(fn)            // implement the procedure
.input() and .output() are optional. If you omit them, the procedure accepts any input and returns the inferred handler output type.

Adding middleware to a procedure

You can attach middleware directly to a procedure with .use(). Middleware runs before the handler and can modify context or short-circuit the call:
import { ORPCError, os } from '@orpc/server'
import type { IncomingHttpHeaders } from 'node:http'

export const createPlanet = os
  .$context<{ headers: IncomingHttpHeaders }>()
  .use(({ context, next }) => {
    const user = parseJWT(context.headers.authorization?.split(' ')[1])

    if (user) {
      return next({ context: { user } })
    }

    throw new ORPCError('UNAUTHORIZED')
  })
  .input(PlanetSchema.omit({ id: true }))
  .handler(async ({ input, context }) => {
    // context.user is available and typed here
    return { id: 1, name: input.name }
  })

Reusing procedures

Because procedures are plain objects, you can store them in variables, import them across files, and compose them into routers freely. See Routers for how to group procedures together.

Server actions

A DecoratedProcedure can be made callable as a React Server Action with .actionable():
export const createPlanet = os
  .input(PlanetSchema.omit({ id: true }))
  .handler(async ({ input }) => {
    return { id: 1, name: input.name }
  })
  .actionable()
Learn more about server action support in the Server Actions guide.

Build docs developers (and LLMs) love