orpc-fullstack
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseoRPC — Typesafe APIs Made Simple
oRPC — 让类型安全API变得简单
oRPC combines RPC with OpenAPI for end-to-end type-safe APIs (v1.12+). It supports Zod, Valibot, Arktype, or any Standard Schema library.
oRPC将RPC与OpenAPI相结合,实现端到端的类型安全API(v1.12+)。它支持Zod、Valibot、Arktype或任何标准Schema库。
When to Apply
适用场景
Reference these guidelines when:
- Creating or modifying API procedures (contracts, routers, handlers)
- Setting up middleware (auth, authorization guards, logging)
- Integrating oRPC with Hono or other server frameworks
- Building data fetching hooks with TanStack Query
- Implementing real-time features with event iterators / SSE
- Configuring client links (fetch, WebSocket, batch, retry)
- Handling errors (server-side ORPCError, client-side typed errors)
在以下场景中可参考这些指南:
- 创建或修改API过程(契约、路由、处理程序)
- 设置中间件(认证、授权守卫、日志)
- 将oRPC与Hono或其他服务器框架集成
- 使用TanStack Query构建数据获取钩子
- 利用事件迭代器/SSE实现实时功能
- 配置客户端链接(fetch、WebSocket、批量处理、重试)
- 错误处理(服务端ORPCError、客户端类型化错误)
Core Concepts
核心概念
| 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 |
| 概念 | 导入方式 | 用途 |
|---|---|---|
| Contract | | 定义API结构,无需处理程序 |
| Procedure | | 包含验证、中间件和依赖注入的函数 |
| Router | 普通对象 | 将过程组合为树形结构 |
| Middleware | | 拦截请求、注入上下文、守卫访问权限 |
| Handler | | 通过HTTP/WS提供过程服务 |
| Client | | 基于契约/路由的类型安全客户端 |
| TanStack Query | | 用于查询/突变的React钩子 |
Project Structure
项目结构
Organize by domain modules with paired contract + router files:
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.tsRoot barrels compose modules: .
export default { health, user }按领域模块组织,搭配契约和路由文件:
src/
index.ts # Hono应用 + 处理程序设置
middlewares/
auth-middleware.ts # 会话验证 -> 注入用户信息
modules/
contract.ts # 根入口:所有契约
router.ts # 根入口:所有路由
health/
health.contract.ts
health.router.ts
user/
user.contract.ts
user.router.ts根入口组合各模块:。
export default { health, user }Contract-First Development
契约优先开发
Contracts define API shape. Routers implement them with TypeScript enforcement.
ts
// 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 };ts
// 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 };契约定义API结构,路由通过TypeScript约束来实现它们。
ts
// 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 };ts
// 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 };Procedures
过程(Procedures)
ts
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 };
});- is the only required step
.handler - Specifying improves TypeScript inference speed
.output - Create reusable bases:
const protectedProcedure = os.$context<Ctx>().use(authMiddleware)
ts
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 - 指定可提升TypeScript推断速度
.output - 创建可复用基础:
const protectedProcedure = os.$context<Ctx>().use(authMiddleware)
Middleware
中间件
Auth middleware injects user into context:
ts
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 } });
});Input-aware middleware for authorization guards:
ts
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();
});Stack middleware left-to-right: .
.use(auth).use(guard).handler(...)Built-ins: , , , , .
onStartonSuccessonErroronFinishdedupeMiddleware认证中间件将用户信息注入上下文:
ts
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 } });
});感知输入的中间件用于授权守卫:
ts
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(...)内置中间件:, , , , 。
onStartonSuccessonErroronFinishdedupeMiddlewareError Handling
错误处理
ts
// 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 */ }ts
// 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 */ }Hono Integration
Hono集成
ts
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();
});ts
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();
});TanStack Query
TanStack Query
ts
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() });ts
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() });Event Iterator (SSE / Streaming)
事件迭代器(SSE / 流处理)
ts
// 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);
}Use for typed pub/sub between handlers.
EventPublisherts
// 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);
}使用实现处理程序之间的类型化发布/订阅。
EventPublisherClient Setup
客户端设置
ts
import { 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);WebSocket: .
import { RPCLink } from "@orpc/client/websocket"ts
import { 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);WebSocket:。
import { RPCLink } from "@orpc/client/websocket"Detailed Reference
详细参考
For comprehensive examples, advanced patterns, and full API coverage see:
- Full Reference — Complete documentation with detailed code examples covering contracts, routers, middleware, server handlers, client setup, TanStack Query, event iterators, plugins, file uploads, WebSocket, AI SDK integration, and metadata.
如需完整示例、高级模式和全面API覆盖,请查看:
- 完整参考 — 包含契约、路由、中间件、服务器处理程序、客户端设置、TanStack Query、事件迭代器、插件、文件上传、WebSocket、AI SDK集成和元数据的详细代码示例的完整文档。