Loading...
Loading...
oRPC (v1.12+) typesafe API layer for TypeScript. Covers contract-first development, procedure definition, middleware, routers, context, error handling, Hono integration, TanStack Query, event iterators, plugins, file uploads, WebSocket adapter, and best practices. Use when building or modifying API procedures, data fetching hooks, server handlers, or real-time features. Triggers on tasks involving oRPC, typesafe APIs, RPC procedures, or server route handlers.
npx skill4agent add stephen-golban/orpc-skill orpc-fullstack| Concept | Import | Purpose |
|---|---|---|
| Contract | | Define API shape without handlers |
| Procedure | | Function with validation + middleware + DI |
| Router | Plain object | Compose procedures into a tree |
| Middleware | | Intercept, inject context, guard access |
| Handler | | Serve procedures over HTTP/WS |
| Client | | Type-safe client from contract/router |
| TanStack Query | | React hooks for queries/mutations |
src/
index.ts # Hono app + handler setup
middlewares/
auth-middleware.ts # Session validation -> injects user
modules/
contract.ts # Root barrel: all contracts
router.ts # Root barrel: all routers
health/
health.contract.ts
health.router.ts
user/
user.contract.ts
user.router.tsexport default { health, user }// Contract — define shape
import { oc } from "@orpc/contract";
import { z } from "zod/v4";
const userContract = oc
.route({ tags: ["user"] })
.errors({ UNAUTHORIZED: {} });
const searchUser = userContract
.route({ method: "POST", path: "/user/search" })
.input(z.object({ query: z.string() }))
.output(z.array(userSchema));
export default { searchUser };// Router — implement contract
import { implement } from "@orpc/server";
import contract from "./user.contract";
const router = implement(contract).$context<{ headers: Headers }>();
const searchUser = router.searchUser
.use(authMiddleware)
.handler(async ({ input, context }) => { /* ... */ });
export default { searchUser };import { os } from "@orpc/server";
const example = os
.use(aMiddleware) // Middleware
.input(z.object({ name: z.string() })) // Validate input
.output(z.object({ id: z.number() })) // Validate output (recommended)
.handler(async ({ input, context }) => { // Handler
return { id: 1 };
});.handler.outputconst protectedProcedure = os.$context<Ctx>().use(authMiddleware)export const authMiddleware = os
.$context<{ headers: Headers }>()
.middleware(async ({ context, next }) => {
const session = await auth.api.getSession({ headers: context.headers });
if (!session) throw new ORPCError("UNAUTHORIZED");
return next({ context: { ...context, user: session.user } });
});export const membershipGuard = os
.$context<{ user: User }>()
.middleware(async ({ context, next }, input: { uuid: string }) => {
// Check membership using input.uuid + context.user.id
if (!member) throw new ORPCError("FORBIDDEN");
return next();
});.use(auth).use(guard).handler(...)onStartonSuccessonErroronFinishdedupeMiddleware// Server — throw errors
throw new ORPCError("NOT_FOUND");
throw new ORPCError("BAD_REQUEST", { message: "Invalid input" });
// Contract-defined typed errors
const contract = oc.errors({
RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },
});
// Handler uses typed factory
const proc = implement(contract).handler(async ({ errors }) => {
throw errors.RATE_LIMITED({ data: { retryAfter: 60 } });
});
// Client — handle errors
const [error, data] = await safe(client.doSomething({ id: "123" }));
if (isDefinedError(error)) { /* typed from contract */ }import { OpenAPIHandler } from "@orpc/openapi/fetch";
import { Hono } from "hono";
const handler = new OpenAPIHandler(router, { /* plugins, interceptors */ });
const app = new Hono()
.basePath("/api")
.use("/rpc/*", async (c, next) => {
const { matched, response } = await handler.handle(c.req.raw, {
prefix: "/api/rpc",
context: { headers: c.req.raw.headers },
});
if (matched) return c.newResponse(response.body, response);
await next();
});import { createTanstackQueryUtils } from "@orpc/tanstack-query";
const orpc = createTanstackQueryUtils(client);
// Queries
useQuery(orpc.user.search.queryOptions({ input: { query } }));
// Mutations
useMutation(orpc.vehicle.add.mutationOptions());
// Infinite queries
useInfiniteQuery(orpc.feed.list.infiniteOptions({
input: (pageParam) => ({ cursor: pageParam, limit: 20 }),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}));
// Keys for invalidation
orpc.vehicle.key() // All vehicle queries
queryClient.invalidateQueries({ queryKey: orpc.vehicle.key() });// Server — async generator
const live = os
.output(eventIterator(z.object({ message: z.string() })))
.handler(async function* ({ signal }) {
for await (const payload of publisher.subscribe("topic", { signal })) {
yield payload;
}
});
// Client — consume
for await (const event of await client.live()) {
console.log(event.message);
}EventPublisherimport { RPCLink } from "@orpc/client/fetch";
import { createORPCClient } from "@orpc/client";
const link = new RPCLink({
url: "http://localhost:3000/api/rpc",
headers: () => ({ Authorization: `Bearer ${getToken()}` }),
});
export const client = createORPCClient(link);import { RPCLink } from "@orpc/client/websocket"