Typesafe APIs Made Simple with oRPC
This article is written by unnoq, creator of ORPC. All opinions expressed are their own.
Building APIs often involves navigating a complex landscape of tools for routing, validation, documentation, and type safety. What if you could simplify this while enhancing the developer experience?
Enter oRPC, a TypeScript library designed to make building end-to-end typesafe APIs effortless, automatically adhering to OpenAPI standards and boosting developer productivity. It aims to provide the best developer experience ever for the world of APIs.
oRPC just announced its v1 release! π For the full story and details behind the v1 launch, check out the official announcement here.
The Idea behind oRPC#
oRPC's philosophy is powerful simplicity. The goal is to let you define API endpoints almost as easily as writing standard functions, while automatically gaining significant benefits:
- End-to-end type safety (including inputs, outputs, and errors)
- Server Action compatibility
- Full OpenAPI specification generation
- Optional Contract-first workflow support
- Standard function call compatibility
Hereβs a taste of the fluent API design:
const getting = os
.use(dbProvider)
.use(requiredAuth)
.use(rateLimit)
.use(analytics)
.use(sentryMonitoring)
.use(retry({ times: 3 }))
.route({ method: "GET", path: "/getting/{id}" })
.input(z.object({ id: z.string() }))
.use(canGetting, (input) => input.id)
.errors({
SOME_TYPE_SAFE_ERROR: {
data: z.object({ something: z.string() }),
},
})
.handler(async ({ input, errors, context }) => {
// do something
})
.actionable() // server action compatible
.callable(); // regular function compatible
oRPC highlights#
- π End-to-End Type Safety: Ensure type-safe inputs, outputs, and errors from client to server.
- π First-Class OpenAPI: Built-in support that fully adheres to the OpenAPI standard.
- π Contract-First Development: Optionally define your API contract before implementation.
- βοΈ Framework Integrations: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte), Pinia Colada, and more.
- π Server Actions: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms.
- π Standard Schema Support: Works out of the box with Zod, Valibot, ArkType, and other schema validators.
- ποΈ Native Types: Supports native types like Date, File, Blob, BigInt, URL, and more.
- β±οΈ Lazy Router: Enhance cold start times with our lazy routing feature.
- π‘ SSE & Streaming: Enjoy full type-safe support for SSE and streaming.
- π Multi-Runtime Support: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond.
- π Extendability: Easily extend functionality with plugins, middleware, and interceptors.
- π‘οΈ Reliability: Well-tested, TypeScript-based, production-ready, and MIT licensed.
Why oRPC? (Performance & DX)#
Compared to popular alternatives like tRPC, oRPC was designed with first-class OpenAPI support from the ground up, addressing a common need, especially for developers working with tools like API Gateways (like Zuplo!).
Simple benchmark comparisons with tRPC also show promising results for oRPC (see full report here):
- Faster type checking ( ~1.6x )
- Faster runtime request handling ( ~2.8x )
- Lower max CPU usage ( ~1.26x less )
- Lower max RAM usage ( ~2.6x less )
- Smaller bundle size ( ~2x smaller )
Warning Benchmark results can vary significantly based on environment, project complexity, and specific usage patterns. Treat these numbers as a reference point, not an absolute guarantee.
oRPC aims to provide a highly performant and developer-friendly alternative for building robust, typesafe APIs.
Quick Start#
Getting started with oRPC is straightforward.
1. Install#
npm install @orpc/server@latest @orpc/client@latest zod # Use zod or your preferred schema validator
2. Define your first router#
Create procedures (functions) and group them in a router object.
import type { IncomingHttpHeaders } from "node:http";
import { ORPCError, os } from "@orpc/server";
import { 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,
},
};
3. Create your server#
oRPC offers adapters for various runtimes (Node.js, Cloudflare Workers, Deno, Bun, etc.). Hereβs a Node.js example:
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"),
);
4. Create TypeScript Client#
Set up a client to consume your API type-safely.
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. Consume your API#
Now you can call your API procedures with full autocompletion and type safety.
import { isDefinedError, safe } from "@orpc/client";
const planet = await orpc.planet.find({ id: 1 });
// ^^^^^ Everything is type-safe
const { data, error } = await safe(orpc.planet.create({ name: "name" }));
if (error) {
if (isDefinedError(error)) {
// Handle known and type-safe errors here
}
// Handle unknown errors here
} else {
// Handle success here
}
Tanstack Query Integration#
oRPC provides lightweight helpers for seamless integration with TanStack Query (React, Vue, Solid, Svelte). Refer to the official documentation for detailed setup guides.
import { isDefinedError } from "@orpc/client";
import {
useInfiniteQuery,
useMutation,
useQueryClient,
} from "@tanstack/react-query";
const query = useInfiniteQuery(
orpc.planet.list.infiniteOptions({
input: (cursor) => ({ cursor }),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
}),
);
const queryClient = useQueryClient();
const mutation = useMutation(
orpc.planet.update.mutationOptions({
onError(error) {
if (isDefinedError(error)) {
const id = error.data.id;
// ^ type-safe errors
}
},
onSuccess() {
queryClient.invalidateQueries({
queryKey: orpc.planet.key(),
});
},
}),
);
Explore Further#
We have many examples and playgrounds available, covering integrations from vanilla JS to Next.js, SvelteKit, Vue, and more:
- Playgrounds: https://orpc.unnoq.com/docs/playgrounds
- Full Documentation: https://orpc.unnoq.com/
Special Thanks to Zuplo#
Developing open-source tools takes significant effort. We'd like to extend a special thank you to Zuplo - the Serverless API Gateway, designed for developers. Their sponsorship and support during the early stages of oRPC were invaluable and greatly appreciated. Thank you, Zuplo team, for believing in the project!