ORPC is a friendly library to help us build an end-to-end, type-safe API. Let me explain.
Generally, an API server exposes endpoints regardless of the backend language. The backend team—or whoever maintains it—may write documentation explaining how frontend developers can use it, including methods, endpoints, payloads, request bodies, headers, and so on.
And your frontend code may look something like this:
try {
const res = await fetch('https://api.example.com/planets', {
method: 'GET',
})
if (!res.ok) {
throw new Error(/* error */)
// or maybe :
const text = await res.text()
throw new Error(text)
}
const json = await res.json()
const parsed = z.object({ name: z.string() }).array().parse(json)
return parsed // <-- typed response
} catch (error) {
if (error instanceof Error) {
if (error.message === 'INVALID_BODY') {
// ....
}
if (error.message === 'INVALID_HEADER') {
// ....
}
}
// ... handle unexpected errors
}With oRPC, we can simplify this code to:
const { error, data } = await safe(
apiClient.auth.listPlanets({
name: 'Earth',
workspaceId: '1',
}),
)
if (error) {
if (isDefinedError(error)) {
// error.code is typed
switch (error.code) {
case 'RETRY_AFTER':
console.log(`Retry after ${error.data.seconds} seconds`)
break
default:
break
}
} else {
// ... handle unexpected errors
}
} else {
return data
}we don't need to guess! everything is typed. From query, params, body, etc... And thats what I meant for end-to-end, to more custom validations.
Generally, we will need three components:
npm install @orpc/client@beta
npm install @orpc/contract@beta
npm install @orpc/openapi@beta
npm install @orpc/server@betaimport { openapi } from '@orpc/openapi'
import { os } from '@orpc/server'
import { z } from 'zod'
import { OpenAPIHandler } from '@orpc/openapi/fetch'
const inputSchema = z.object({
page: z.number(),
size: z.number(),
})
const outputSchema = z
.object({
id: z.string(),
name: z.string(),
createdAt: z.coerce.date<Date>(), // <--- the `coerce` here is important if you are using openapi modules
})
.array()
export const router = {
planet: {
list: os
.meta(openapi({ method: 'GET', path: '/planets' }))
.input(inputSchema)
.output(outputSchema)
.handler(async ({ input }) => {
const { page, size } = input
const data = await db
.select()
.from(planets)
.offset(page * size)
.limit(size)
return data
}),
},
}
export const handler = new OpenAPIHandler(router)The simplest version of handler look something like this. And the code is pretty self explanatory.
Now we need to connect the handler to the server.
import { handler } from './orpc'
Bun.serve({
async fetch(request: Request) {
const { matched, response } = await handler.handle(request, {
context: {},
})
if (matched) {
return response
}
return new Response('Not found', { status: 404 })
},
})import { createORPCClient } from '@orpc/client'
import { ResponseValidationLinkPlugin } from '@orpc/contract/plugins'
import { OpenAPILink } from '@orpc/openapi/fetch'
import { type RouterClient } from '@orpc/server'
const link = new OpenAPILink(router, {
origin: 'http://localhost:3000',
interceptors: [
async ({ next, path }) => {
console.time(path.join('.'))
try {
return await next()
} finally {
console.timeEnd(path.join('.'))
}
},
],
fetch: (request, init) => {
return globalThis.fetch(request, {
...init,
})
},
plugins: [new ResponseValidationLinkPlugin(router)],
})
export const apiClient: RouterClient<typeof router> = createORPCClient(link)Remember to use ResponseValidationLinkPlugin with OpenAPI. Without it, dates are returned as strings at runtime. The plugin validates the response and coerces those strings back into Date objects.
So far, oRPC has become my go-to library for building full-stack applications. It supports runtimes beyond Bun, including Node.js and Cloudflare Workers. It also integrates with TanStack Query, so we don’t need to build query options ourselves. Give it a try if you haven’t already.
If you enjoy TanStack Start’s server functions, tRPC, Elysia Treaty, or Hono RPC, you’ll likely enjoy oRPC as well.