Back

Building Type-Safe APIs with oRPC

Learn how oRPC combines type safety with OpenAPI standards for effortless API development

Why oRPC?

The creator built oRPC after struggling with tRPC's lack of OpenAPI support and finding existing solutions like trpc-openapi didn't work with Edge runtimes. It solves real problems:

  • Full type safety - From client to server, including errors
  • OpenAPI first-class - No third-party adapters needed
  • Framework agnostic - Works with React, Vue, Solid, Svelte
  • Native types - Handles Date, File, Blob, BigInt out of the box
  • Simpler setup - Less boilerplate than alternatives

Quick Start

Install the packages:

npm install @orpc/server @orpc/client zod

Define Your API

Create procedures with full type safety:

import { os, ORPCError } from "@orpc/server";
import * as z from "zod";

// Define your schema
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

// Create a procedure
export const getUser = os.input(z.object({ id: z.number() })).handler(async ({ input }) => {
  // Your logic here
  const user = await db.users.findById(input.id);

  if (!user) {
    throw new ORPCError("NOT_FOUND", "User not found");
  }

  return user;
});

Add Middleware

Add auth or context easily:

export const createUser = os
  .$context<{ headers: Headers }>()
  .use(({ context, next }) => {
    const token = context.headers.get("authorization");
    const user = validateToken(token);

    if (!user) {
      throw new ORPCError("UNAUTHORIZED");
    }

    return next({ context: { user } });
  })
  .input(UserSchema.omit({ id: true }))
  .handler(async ({ input, context }) => {
    // context.user is typed and available
    return db.users.create(input);
  });

Create a Router

Bundle your procedures:

import { or } from "@orpc/server";

export const router = or.router({
  users: {
    get: getUser,
    create: createUser,
    list: listUsers,
  },
  posts: {
    get: getPost,
    create: createPost,
  },
});

export type Router = typeof router;

Setup the Server

Works with Node.js, Bun, Deno, Cloudflare Workers:

import { createServer } from "node:http";
import { RPCHandler } from "@orpc/server/node";

const handler = new RPCHandler(router);

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("Not found");
  }
});

server.listen(3000);

Setup the Client

Type-safe calls from anywhere:

import type { RouterClient } from "@orpc/server";
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";

const link = new RPCLink({
  url: "http://localhost:3000",
  headers: { Authorization: "Bearer token" },
});

export const client: RouterClient<typeof router> = createORPCClient(link);

Use It

Fully typed, autocomplete everywhere:

// TypeScript knows the shape!
const user = await client.users.get({ id: 1 });

// Create with validation
const newUser = await client.users.create({
  name: "John",
  email: "john@example.com",
});

Generate OpenAPI Spec

Turn your API into OpenAPI docs:

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: "My API",
    version: "1.0.0",
  },
});

// Use with Swagger UI, Postman, etc.

Framework Integration

Works with your favorite tools:

React with TanStack Query:

import { useQuery } from '@tanstack/react-query';

function UserProfile({ id }: { id: number }) {
  const { data } = useQuery({
    queryKey: ['user', id],
    queryFn: () => client.users.get({ id }),
  });

  return <div>{data?.name}</div>;
}

Next.js Server Actions:

"use server";

export async function createPost(data: FormData) {
  return client.posts.create({
    title: data.get("title"),
    content: data.get("content"),
  });
}

Error Handling

Typed errors make debugging easy:

try {
  await client.users.get({ id: 999 });
} catch (error) {
  if (error instanceof ORPCError) {
    if (error.code === "NOT_FOUND") {
      // Handle not found
    }
  }
}

Schema Validation

Works with multiple validators:

  • Zod - Most popular
  • Valibot - Lightweight alternative
  • ArkType - Performance focused
  • Any standard schema library

oRPC vs tRPC

FeatureoRPCtRPC
Type Safety
OpenAPI Support✅ Built-in⚠️ Third-party
Framework SupportReact, Vue, Solid, SvelteMainly React
Native TypesDate, File, Blob, etc.Limited
Edge Runtime⚠️ Limited
Setup ComplexitySimpleMore config

When to Use oRPC

Choose oRPC if you need:

  • OpenAPI documentation for external APIs
  • Multi-framework support (Vue, Solid, Svelte)
  • Edge runtime compatibility
  • Simpler setup with less boilerplate
  • File upload handling out of the box

Stick with tRPC if you:

  • Have an established tRPC codebase
  • Only use React Query
  • Don't need OpenAPI specs

Tips

  • Start with simple procedures, add middleware later
  • Use Zod for schema validation - it's the most mature
  • Generate OpenAPI specs for documentation
  • Leverage typed errors for better error handling
  • Keep procedures focused and composable

Resources

Building Type-Safe APIs with oRPC | Loggrfun