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 zodDefine 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
| Feature | oRPC | tRPC |
|---|---|---|
| Type Safety | ✅ | ✅ |
| OpenAPI Support | ✅ Built-in | ⚠️ Third-party |
| Framework Support | React, Vue, Solid, Svelte | Mainly React |
| Native Types | Date, File, Blob, etc. | Limited |
| Edge Runtime | ✅ | ⚠️ Limited |
| Setup Complexity | Simple | More 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
- Official Docs
- Works great with API gateways like Zuplo