Skip to main content
1

Install packages

Install the server and client packages. oRPC works with any schema validator — these examples use Zod.
npm install @orpc/server @orpc/client zod
2

Define your router

Create your procedures and group them into a router. The os builder lets you chain .input(), .output(), and .handler() to define each operation.
router.ts
import type { IncomingHttpHeaders } from 'node:http'
import { ORPCError, 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 listPlanet = os
  .input(
    z.object({
      limit: z.number().int().min(1).max(100).optional(),
      cursor: z.number().int().min(0).default(0),
    }),
  )
  .handler(async ({ input }) => {
    // your list code here
    return [{ id: 1, name: 'name' }]
  })

export const findPlanet = os
  .input(PlanetSchema.pick({ id: true }))
  .handler(async ({ input }) => {
    // your find code here
    return { id: 1, name: 'name' }
  })

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

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

    throw new ORPCError('UNAUTHORIZED')
  })
  .input(PlanetSchema.omit({ id: true }))
  .handler(async ({ input, context }) => {
    // your create code here
    return { id: 1, name: 'name' }
  })

export const router = {
  planet: {
    list: listPlanet,
    find: findPlanet,
    create: createPlanet,
  },
}
A router is a plain JavaScript object — no magic, no decorators. You can nest routers as deeply as you need.
3

Create your server

Pass your router to an RPCHandler and attach it to a Node.js HTTP server (or any supported runtime).
server.ts
import { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'
import { CORSPlugin } from '@orpc/server/plugins'

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

const server = createServer(async (req, res) => {
  const result = await handler.handle(req, res, {
    context: { headers: req.headers }
  })

  if (!result.matched) {
    res.statusCode = 404
    res.end('No procedure matched')
  }
})

server.listen(
  3000,
  '127.0.0.1',
  () => console.log('Listening on 127.0.0.1:3000')
)
oRPC supports Node.js, Cloudflare Workers, Deno, Bun, and any Fetch-compatible runtime. See the adapters section for all options.
4

Create your client

The client is fully typed from your router — no code generation required.
client.ts
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  url: 'http://127.0.0.1:3000',
  headers: { Authorization: 'Bearer token' },
})

export const orpc: RouterClient<typeof router> = createORPCClient(link)
5

Call your API

Call procedures as if they were local async functions. TypeScript will infer the input and output types automatically.
import { orpc } from './client'

const planets = await orpc.planet.list({ limit: 10 })
//    ^? { id: number; name: string; description?: string }[]
6

Generate an OpenAPI spec (optional)

oRPC can produce a fully compliant OpenAPI specification from your router with no extra annotations.
import { OpenAPIGenerator } from '@orpc/openapi'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'

const generator = new OpenAPIGenerator({
  schemaConverters: [new ZodToJsonSchemaConverter()]
})

const spec = await generator.generate(router, {
  info: {
    title: 'Planet API',
    version: '1.0.0'
  }
})

console.log(spec)

Next steps

Core concepts

Learn about procedures, routers, middleware, and context.

Server adapters

Deploy to Node.js, Cloudflare, Bun, Deno, and more.

Client setup

Connect from any frontend with full type safety.

OpenAPI

Generate OpenAPI specs and serve interactive API docs.

Build docs developers (and LLMs) love