In this article
April 28, 2025
April 28, 2025

oRPC: OpenAPI Remote Procedure Call for Type-Safe APIs

oRPC (OpenAPI Remote Procedure Call) combines the familiarity of RPC with the industry-standard OpenAPI spec so that every request/response is fully typed from client to server. 

Every seasoned engineer knows the real break-points in a system aren’t inside the algorithms—they’re in the seams where services talk. Schema drift sneaks in during a late-night hot-fix, a version bump lands without regeneration, and suddenly the JSON your mobile team committed to memory is two fields off. You patch, you document, somebody forgets—repeat.

oRPC fixes this loop by collapsing definition, implementation, and documentation into a single, type-safe contract that travels unchanged from server to edge to browser.

Think RPC ergonomics, OpenAPI guarantees, and zero code-gen. If keeping micro-services honest feels like half your sprint, read on.

Why another API toolkit?

End-to-end type-safety

Types flow from server procedure → wire → client with no manual sync or code generation, eliminating an entire class of integration bugs.

Contract-first: define types once, share everywhere

Contract-first means you pin down the shape of your API before you write any handlers. It’s perfect when:

  • Front- & back-end work in parallel – the contract is the handshake.
  • External consumers need a spec early – mock servers & SDKs can be generated immediately.
  • Governance matters – contracts are versioned artifacts you can diff and audit.

With oRPC you use the oc builder to describe only inputs and outputs—no verbs, no DB calls—keeping the file portable and testable.

Here's a simplified example of defining a contract for a blog platform:

// contract.ts
import { oc } from '@orpc/contract'
import { z } from 'zod'

const PostSchema = z.object({
  id: z.number().int(),
  title: z.string(),
  content: z.string(),
})

export const blogContract = {
  getPost: oc
    .input(z.object({ id: z.number().int() }))
    .output(PostSchema),

  listPosts: oc
    .input(z.object({ limit: z.number().int().min(1).max(50).optional() }))
    .output(z.array(PostSchema)),

  createPost: oc
    .input(PostSchema.omit({ id: true }))
    .output(PostSchema),
} as const

Note: No routing or logic—just pure schema describing the shape of the API.

Once the contract is written, implementation becomes a strict, type-checked affair. oRPC enforces that your handlers match the contract exactly.

Start by converting your contract into an implementer instance:

import { implement } from '@orpc/server'
import { blogContract } from './contract'

const os = implement(blogContract) // replaces the usual os from '@orpc/server'

Now you can bind handlers to your contract-defined procedures:

export const listPosts = os.listPosts.handler(({ input }) => {
  return db.listPosts(input.limit ?? 10)
})

export const getPost = os.getPost.handler(({ input }) => {
  return db.getPost(input.id)
})

export const createPost = os.createPost.handler(({ input, context }) => {
  return db.createPost({ ...input, authorId: context.user?.id })
})

Finally, compose your contract-bound handlers into a fully typed router:

export const router = os.router({
  getPost,
  listPosts,
  createPost,
})

This router becomes your API entry point. oRPC checks that the shape matches your contract, ensuring zero drift.

Type-safe errors built-in

oRPC offers two approaches to error handling: standard error throwing and fully typed error definitions using .errors().

Type-safe approach

The .errors() method enables clients to infer error types from your API contract:

import { implement, ORPCError } from '@orpc/server'
import { z } from 'zod'

const os = implement({
  divide: oc
    .errors({
      DIVIDE_BY_ZERO: {
        message: 'Cannot divide by zero',
      },
    })
    .input(z.object({ a: z.number(), b: z.number() }))
    .output(z.number())
})

export const divide = os.divide.handler(({ input, errors }) => {
  if (input.b === 0) throw errors.DIVIDE_BY_ZERO()
  return input.a / input.b
})

Client-side usage

Clients can handle specific errors using inferred codes:

try {
  await orpc.divide({ a: 10, b: 0 })
} catch (e) {
  if (e.code === 'DIVIDE_BY_ZERO') {
    toast.error('Cannot divide by zero')
  }
}

Note on ORPCError

You can still throw new ORPCError() directly, even alongside .errors() definitions. As long as the error code and structure match, oRPC will reconcile them correctly.

⚠️ Avoid placing sensitive data in ORPCError.data as it will be exposed to clients.

Auth & protected routes

import type { IncomingHttpHeaders } from 'node:http'
import { implement, ORPCError } from '@orpc/server'
import { z } from 'zod'
import { parseJWT } from './auth'

const os = implement({
  createPlanet: oc
    .$context<{ headers: IncomingHttpHeaders }>()
    .input(z.object({ name: z.string() }))
    .output(z.object({ id: z.number(), name: z.string() }))
})

export const createPlanet = os.createPlanet
  .use(({ context, next }) => {
    const token = context.headers.authorization?.split(' ')[1]
    const user = token && parseJWT(token)
    if (!user) throw new ORPCError('UNAUTHORIZED')
    return next({ context: { user } })
  })
  .handler(async ({ input, context }) => {
    return { id: 1, name: input.name, ownerId: context.user.id }
  })

export const router = os.router({
  createPlanet,
})

When to reach for oRPC

  • Micro-frontends & BFFs – multiple UIs share a single contract.
  • Server Actions on the Edge.actionable() turns any procedure into a React/Next 15 server action.
  • Edge-first workloads – deploy unchanged handlers to Workers, Deno Deploy, or Bun.
  • Teams that care about OpenAPI – oRPC emits a full spec on demand.

Further reading

Final thoughts

oRPC lets you write a function and get a fully typed, OpenAPI-compliant endpoint—no code-gen, no drift, just one source of truth.

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.