Loading...
Loading...
Build, serve, and call end-to-end typesafe APIs with oRPC v2. Use for any task in a project that depends on `@orpc/*` packages, even a one-procedure change: defining procedures with the os builder (.input/.output/.handler, any Standard Schema validator), assembling routers, middleware and context, typesafe errors with ORPCError, serving via RPCHandler on any runtime adapter, calling from server-side clients (call, createRouterClient) or client-side clients (createORPCClient with RPCLink), integrating TanStack Query, or streaming over SSE. Pretrained oRPC knowledge describes v1 and is wrong for v2, so load this skill even when the change looks trivial. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For REST/OpenAPI exposure, prefer the orpc-openapi skill; for contract-first design, the orpc-contract skill; for tRPC or oRPC v1 migrations, the orpc-migrate skill.
npx skill4agent add middleapi/orpc orpcnpm ls @orpc/server@orpc/*orpc-migratebetanpm install @orpc/server@beta @orpc/client@betanpm view @orpc/server dist-tagslatest.meta(openapi(...))RPCLinkurlorigin@orpc/serverosRPCHandlercallcreateRouterClientimplement@orpc/clientcreateORPCClientRPCLinksafecreateSafeClientisInferableError@orpc/contractorpc-contract@orpc/openapiOpenAPIHandlerOpenAPILink@orpc/tanstack-query@orpc/nestos.handler.input.outputdataimport { os } from '@orpc/server'
import * as z from 'zod'
export const listPlanets = os
.handler(async () => [{ id: 1, name: 'Earth' }]) // no .input: takes no arguments
export const findPlanet = os
.input(z.object({ id: z.number() }))
.handler(async ({ input }) => ({ id: input.id, name: 'Earth' })).handlerconst example = os
.$context<{ headers: Headers }>() // initial context this procedure requires
.errors({ NOT_FOUND: {} }) // typed errors
.use(requireAuth) // middleware
.input(z.object({ id: z.number() }))
.output(z.object({ id: z.number(), name: z.string() })) // optional; also speeds up type checking
.handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))const authed = os.use(requireAuth)authedthenbindvalueOftoStringtoJSONexport const router = {
planet: { list: listPlanets, find: findPlanet },
admin: os.use(requireAuth).router({ deletePlanet }), // apply shared middleware to a subtree
planetLazy: os.lazy(() => import('./planet')), // code-split; module's default export is a router
}InferRouterInputsInferRouterOutputs@orpc/server.use.$contextnext({ context })import { ORPCError, os } from '@orpc/server'
const base = os.$context<{ headers: Headers }>()
const requireAuth = base.middleware(async ({ context, next }) => {
const user = await parseUser(context.headers)
if (!user) {
throw new ORPCError('UNAUTHORIZED')
}
return next({ context: { user } }) // handler now sees context.user, typed non-null
}).use.inputos.middleware(async ({ next }, id: number) => ...).use(mw.adaptInput(input => input.id)).usecallconst authProvider = os
.$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()
.middleware(async ({ context, next }) => {
const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)
return next({ context: { auth, authLoaded: true } })
})ORPCErrorcodemessagedatamessagedataError.errorsconst find = os
.errors({
NOT_FOUND: { message: 'Planet not found' }, // default message
RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },
})
.handler(async ({ input, errors }) => {
throw errors.NOT_FOUND()
})throw new ORPCError('NOT_FOUND')ORPCErrortry/catchRPCHandlerimport { onError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { CORSHandlerPlugin } from '@orpc/server/plugins'
const handler = new RPCHandler(router, {
plugins: [new CORSHandlerPlugin()],
interceptors: [onError(error => console.error(error))],
})
export async function fetch(request: Request): Promise<Response> {
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: { headers: request.headers }, // provide the router's initial context here
})
return matched ? response : new Response('Not found', { status: 404 })
}
// Bun.serve({ fetch }) / Deno.serve(fetch) / export default { fetch } on Workersimport { createServer } from 'node:http'
import { RPCHandler } from '@orpc/server/node'
const handler = new RPCHandler(router)
const server = createServer(async (req, res) => {
const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })
if (matched)
return
res.statusCode = 404
res.end('Not found')
})
server.listen(3000)RPCHandlerPOSTPUTPATCHDELETEGETallowMethodsinterceptorsroutingInterceptorsclientInterceptorspluginsfiltererrorStatusMapimport { call, createRouterClient } from '@orpc/server'
const planet = await call(findPlanet, { id: 1 }, { context: { headers } })
const client = createRouterClient(router, { context: { headers } }) // context can be a function
const planets = await client.planet.list()RPCLinkimport type { RouterClient } from '@orpc/server'
import type { router } from '../server/router'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
const link = new RPCLink({
origin: 'http://127.0.0.1:3000',
url: '/rpc', // must match the server's prefix
headers: () => ({ authorization: `Bearer ${token}` }), // options accept functions
})
export const orpc: RouterClient<typeof router> = createORPCClient(link)
const planet = await orpc.planet.find({ id: 1 })try/catchsafeimport { createSafeClient, isInferableError, safe } from '@orpc/client'
const [error, data] = await safe(orpc.planet.find({ id: 1 }))
if (isInferableError(error)) {
console.log(error.code, error.data) // typed from the procedure's .errors
}
else if (error) {
// unknown error
}
const safeClient = createSafeClient(orpc) // every call returns [error, data].handlerasyncIteratorObjectlastEventIdOpenAPIHandlerorpc-openapi@orpc/contractimplementorpc-contractcreateTanstackQueryUtilscallimplement(router.planet.list).handler(() => [])orpc-migrate.mdhttps://orpc.dev/docs/procedureroutermiddlewarecontexterror-handlingmetadatabinary-dataasync-iterator-objectrpc/*openapi/*contract/*orpc-contractclient/*DynamicLinkadapters/*plugins/*helpers/*integrations/*recipes/*migrations/from-v1migrations/from-trpcorpc-migrate