Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Read the full documentation at [orpc.dev](https://orpc.dev), starting with the [
| [@orpc/server](https://www.npmjs.com/package/@orpc/server) | Build APIs or implement contracts. | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler) |
| [@orpc/client](https://www.npmjs.com/package/@orpc/client) | Consume APIs with end-to-end type safety. | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling) |
| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi) | Add OpenAPI compatibility to APIs. | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar) |
| [@orpc/openrpc](https://www.npmjs.com/package/@orpc/openrpc) | Serve JSON-RPC 2.0 and generate OpenRPC documents. | [OpenRPC Handler](https://orpc.dev/docs/openrpc/handler), [OpenRPC Link](https://orpc.dev/docs/openrpc/link), [Specification](https://orpc.dev/docs/openrpc/specification) |

**Schema validation**

Expand Down
2 changes: 1 addition & 1 deletion apps/content/docs/adapters/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar:
hidden: true
---

An adapter connects a handler to where your code runs. The router stays the same, so switching hosts only changes the few lines that mount the handler. HTTP adapters serve both [RPC](/docs/rpc/adapters) and [OpenAPI](/docs/openapi/adapters); WebSocket and Message Port adapters serve RPC only.
An adapter connects a handler to where your code runs. The router stays the same, so switching hosts only changes the few lines that mount the handler. HTTP adapters serve [RPC](/docs/rpc/adapters), [OpenAPI](/docs/openapi/adapters), and [OpenRPC](/docs/openrpc/adapters); WebSocket adapters serve RPC and OpenRPC, and Message Port adapters serve RPC only.

## Core

Expand Down
1 change: 1 addition & 0 deletions apps/content/docs/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ For questions the reference does not answer, [oRPC on DeepWiki](https://deepwiki
| [@orpc/server](https://npmx.dev/package-docs/@orpc/server) | Build APIs or implement contracts. | [Procedure](/docs/procedure), [Router](/docs/router), [Middleware](/docs/middleware), [Context](/docs/context), [RPC Handler](/docs/rpc/handler) |
| [@orpc/client](https://npmx.dev/package-docs/@orpc/client) | Consume APIs with end-to-end type safety. | [Client-Side Clients](/docs/client/client-side), [Server-Side Clients](/docs/client/server-side), [RPC Link](/docs/rpc/link), [Error Handling](/docs/client/error-handling) |
| [@orpc/openapi](https://npmx.dev/package-docs/@orpc/openapi) | Add OpenAPI compatibility to APIs. | [OpenAPI Handler](/docs/openapi/handler), [OpenAPI Link](/docs/openapi/link), [Specification](/docs/openapi/specification), [Scalar](/docs/openapi/scalar) |
| [@orpc/openrpc](https://npmx.dev/package-docs/@orpc/openrpc) | Serve JSON-RPC 2.0 and generate OpenRPC documents. | [OpenRPC Handler](/docs/openrpc/handler), [OpenRPC Link](/docs/openrpc/link), [Specification](/docs/openrpc/specification) |

## Schema Validation

Expand Down
3 changes: 2 additions & 1 deletion apps/content/docs/comparison.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ oRPC types errors per procedure or globally: throw a plain `ORPCError`, [declare
| Implementation-first approach | ✅ | ✅ | ✅ |
| [Contract-first approach](/docs/contract-first) | ✅ | 🛑 | 🟡 |
| [OpenAPI spec generation](/docs/openapi/specification) | ✅ | 🟡 | 🟡 |
| [JSON-RPC 2.0 & OpenRPC](/docs/openrpc/handler) | ✅ | 🛑 | 🛑 |
| [Standard Schema (Zod, Valibot, ArkType, ...)](/docs/integrations/standard-schema) | ✅ | ✅ | 🟡 |
| [Native types (Date, URL, Set, Map, ...)](/docs/rpc/serializer#supported-data-types) | ✅ | 🟡 | 🛑 |
| [Custom serializers](/docs/rpc/serializer#custom-serializers) | ✅ | ✅ | 🛑 |
| [Bracket notation](/docs/openapi/bracket-notation) | ✅ | 🛑 | 🛑 |
| [Lazy router (code splitting, faster cold starts)](/docs/router#lazy-router) | ✅ | ✅ | 🛑 |

oRPC serves the same procedures over its [RPC protocol](/docs/rpc/protocol) and RESTful [OpenAPI endpoints](/docs/openapi/handler), and generates the spec with a first-party package. tRPC's `@trpc/openapi` is alpha and covers only tRPC-style endpoints, and Hono relies on third-party middleware like `@hono/zod-openapi`. Native types like `Date` or `Map` work out of the box in oRPC; tRPC needs a transformer, and Hono has no serialization layer: what you pass to `c.json()` is what you get.
oRPC serves the same procedures over its [RPC protocol](/docs/rpc/protocol), RESTful [OpenAPI endpoints](/docs/openapi/handler), and [JSON-RPC 2.0](/docs/openrpc/handler) with an [OpenRPC document](/docs/openrpc/specification), and generates the OpenAPI spec with a first-party package. tRPC's `@trpc/openapi` is alpha and covers only tRPC-style endpoints, and Hono relies on third-party middleware like `@hono/zod-openapi`. Native types like `Date` or `Map` work out of the box in oRPC; tRPC needs a transformer, and Hono has no serialization layer: what you pass to `c.json()` is what you get.

### Integrations & Ecosystem

Expand Down
1 change: 1 addition & 0 deletions apps/content/docs/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default defineMeta({
'metadata',
'rpc',
'openapi',
'openrpc',
'contract',
'client',
'adapters',
Expand Down
31 changes: 31 additions & 0 deletions apps/content/docs/openrpc/adapters.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: "OpenRPC Adapters"
description: "The HTTP and WebSocket adapters that run OpenRPC Handler and OpenRPC Link for JSON-RPC 2.0, and where to import each one."
---

OpenRPC runs over HTTP and WebSocket. Pick the adapter for your transport, then import [OpenRPC Handler](/docs/openrpc/handler) or [OpenRPC Link](/docs/openrpc/link) from it. For message ports, use [RPC](/docs/rpc/adapters).

## Server Adapters

| Adapter | Import `OpenRPCHandler` from |
| --------------------------------------- | ---------------------------- |
| [Fetch API](/docs/adapters/fetch-api) | `@orpc/openrpc/fetch` |
| [Node HTTP](/docs/adapters/node-http) | `@orpc/openrpc/node` |
| [Fastify](/docs/adapters/fastify) | `@orpc/openrpc/fastify` |
| [AWS Lambda](/docs/adapters/aws-lambda) | `@orpc/openrpc/aws-lambda` |
| [WebSocket](/docs/adapters/websocket) | `@orpc/openrpc/websocket` |

The HTTP adapters answer `POST` requests to the `prefix` path itself, such as `/rpc`, so mount the handler on a route that matches it. In Fastify, use `app.all('/rpc', ...)`: `/rpc/*` does not match `/rpc`, and a `POST`-only route would keep [CORS](/docs/plugins/cors) preflight requests from reaching the handler. The WebSocket adapter answers each message on its socket, also from crossws hooks, see [OpenRPC Handler over WebSocket](/docs/openrpc/handler#websocket).

## Client Adapters

| Adapter | Import `OpenRPCLink` from | Calls |
| ------------------------------------- | ------------------------- | ------------------------ |
| [Fetch API](/docs/adapters/fetch-api) | `@orpc/openrpc/fetch` | Any HTTP server adapter |
| [WebSocket](/docs/adapters/websocket) | `@orpc/openrpc/websocket` | WebSocket server adapter |

Both also call any other JSON-RPC 2.0 server over the same transport.

## Frameworks and Platforms

Every framework and platform adapter built on the Fetch API or Node HTTP also serves OpenRPC, such as [Next.js](/docs/adapters/next), [Hono](/docs/adapters/hono), [Express.js](/docs/adapters/express), and [Cloudflare Workers](/docs/adapters/cloudflare-workers). Where an example uses `RPCHandler`, swap in `OpenRPCHandler` from the matching import above. See [Adapters](/docs/adapters) for the full list.
149 changes: 149 additions & 0 deletions apps/content/docs/openrpc/handler.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: "OpenRPC Handler"
description: "Use OpenRPCHandler to serve oRPC procedures as a JSON-RPC 2.0 API, for OpenRPC Link or any other JSON-RPC client."
---

## Overview

`OpenRPCHandler` serves your [router](/docs/router) as a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) API over HTTP `POST`, for [OpenRPC Link](/docs/openrpc/link) or any other JSON-RPC client. Pair it with the [OpenRPC Generator](/docs/openrpc/specification) to describe the API in an OpenRPC document.

```package-install
npm install @orpc/openrpc@beta
```

```ts
import { OpenRPCHandler } from '@orpc/openrpc/fetch' // or '@orpc/openrpc/node', '@orpc/openrpc/fastify', '@orpc/openrpc/aws-lambda'

const handler = new OpenRPCHandler(router)

export async function fetch(request: Request) {
const { matched, response } = await handler.handle(request, {
prefix: '/rpc',
context: {}, // Provide initial context if needed
})

if (matched) {
return response
}

return new Response('Not Found', { status: 404 })
}
```

The handler answers `POST` requests to the `prefix` path itself, here `/rpc`, so mount it on a route that matches that path in your [adapter](/docs/openrpc/adapters). Other requests are not matched, so other handlers or plugins can serve them. Requests must use `Content-Type: application/json` (or `application/json-rpc`), otherwise they are rejected with `415`, which also protects the endpoint from CSRF.

```bash
curl -X POST http://localhost:3000/rpc \
-H 'content-type: application/json' \
-d '{"jsonrpc": "2.0", "method": "planet.find", "params": {"id": 1}, "id": 1}'
```

:::warning
JSON-RPC over HTTP is request/response only: procedures with an [event iterator](/docs/async-iterator-object) input or output are not exposed, and inputs or outputs containing files or streams cannot be sent. Server-sent notifications are not supported.
:::

## Methods and Params

Each procedure is a method named after its path joined by `.`, such as `planet.find`. Names starting with `rpc.` are reserved by JSON-RPC.

| `params` | Procedure input |
| ---------------------- | ---------------------------------------------------------------- |
| `{ "id": 1 }` | `{ id: 1 }` (by-name) |
| `[x]` | `x` (by-position), for inputs that are not objects |
| absent, `[]`, or `{}` | `undefined` or `{}`, whichever the input schema accepts |
| `[x, y]` | rejected with `-32602`, since a procedure takes a single input |

Results and error data are serialized like [OpenAPI responses](/docs/openapi/serializer), so a `Date` becomes an ISO string, and an `undefined` result is sent as `null`. Use the [Smart Coercion Plugin](/docs/plugins/smart-coercion) to restore native types from params.

## Error Codes

An `ORPCError` is answered with a JSON-RPC error whose `data` carries the oRPC error, so [OpenRPC Link](/docs/openrpc/link) restores it, including [typesafe errors](/docs/error-handling):

```json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32004,
"message": "Planet not found",
"data": { "code": "NOT_FOUND", "defined": true, "data": { "id": 2 } }
}
}
```

Error codes come from `COMMON_JSON_RPC_ERROR_CODE_MAP`: `BAD_REQUEST`, including input validation errors, is `-32602` "Invalid params", other `4xx` errors are `-32000 - (status - 400)`, such as `-32004` for `NOT_FOUND`, `5xx` errors are `-32603` "Internal error", and codes missing from the map are `-32000`. Use `errorCodeMap` to change it, and pass the same map to the [OpenRPC Generator](/docs/openrpc/specification):

```ts
import { COMMON_JSON_RPC_ERROR_CODE_MAP } from '@orpc/openrpc'

const handler = new OpenRPCHandler(router, {
errorCodeMap: { ...COMMON_JSON_RPC_ERROR_CODE_MAP, OUT_OF_STOCK: 1001 },
})
```

Requests that are not valid JSON-RPC are answered with the protocol errors `-32700` "Parse error", `-32600` "Invalid Request", or `-32601` "Method not found". JSON-RPC errors use HTTP status `200`.

## Batch Requests and Notifications

A JSON array of requests is a batch: its calls run concurrently and the responses keep the request order. A request without `id` is a notification: it runs to completion, but is never answered, and a request with only notifications is answered with `204`. Batches are limited to `maxBatchSize` requests (default `100`), larger ones are rejected with `413`.

```ts
const handler = new OpenRPCHandler(router, {
maxBatchSize: 20,
})
```

## WebSocket

`OpenRPCHandler` from `@orpc/openrpc/websocket` serves the same API over a WebSocket connection, for the same WebSocket implementations as the [WebSocket Adapters](/docs/adapters/websocket). Each message is a request or a batch, answered with a message on the same socket, and notifications are not answered:

```ts
import { WebSocketServer } from 'ws'
import { OpenRPCHandler } from '@orpc/openrpc/websocket'

const handler = new OpenRPCHandler(router)

const wss = new WebSocketServer({ port: 8080 })

wss.on('connection', (ws) => {
handler.upgrade(ws, {
context: {}, // Provide initial context if needed, resolved for each message
})
})
```

Responses to concurrent calls can arrive in any order, matched to their requests by `id`. When the socket closes, the `signal` of its in-flight calls is aborted. Use `.message(ws, data)` and `.close(ws)` instead of `upgrade` when your server hands you the messages, such as with Bun, Cloudflare Hibernation WebSocket, or [crossws](https://github.com/h3js/crossws):

```ts
import crossws from 'crossws/adapters/node' // any crossws adapter is supported

const ws = crossws({
hooks: {
message: async (peer, message) => {
await handler.message(peer, message.text())
},
close: (peer) => {
handler.close(peer)
},
},
})
```

## Interceptors

Each JSON-RPC call runs as its own sub-request: [interceptors](/docs/rpc/handler#interceptors) and client interceptors, including those of plugins, run once per call, and each call gets its own tracing span. Use `getJsonRpcCall` to read the call:

```ts
import { getJsonRpcCall } from '@orpc/openrpc/standard'

const handler = new OpenRPCHandler(router, {
interceptors: [
async ({ next, request }) => {
const call = getJsonRpcCall(request) // { method: 'planet.find', id: 1 }
return next()
},
],
})
```

Routing interceptors run once per HTTP request (or WebSocket message), around all the calls of a batch, and can answer the whole request, such as to require authentication. Over WebSocket, such an answer is sent as an error for each call of the message. `rpc.discover`, answered by the [discovery option](/docs/openrpc/specification#service-discovery), only runs inside routing interceptors. Plugins built on them apply to the HTTP request itself, such as [CORS](/docs/plugins/cors), [Request Limit](/docs/plugins/request-limit), and [Response Headers](/docs/plugins/response-headers), and logging plugins such as [Pino](/docs/integrations/pino) log once per HTTP request. The response headers of the calls in a batch are merged. The [Batch Plugin](/docs/plugins/batch) is not needed, since JSON-RPC batches requests natively.
15 changes: 15 additions & 0 deletions apps/content/docs/openrpc/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: "OpenRPC"
description: "Serve oRPC procedures as a JSON-RPC 2.0 API for any JSON-RPC client, call it with a typesafe link, and describe it in an OpenRPC document."
seo:
title: "OpenRPC: JSON-RPC 2.0 Handler, Link, and Specification"
sidebar:
hidden: true
---

OpenRPC serves the same router as a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) API, for clients and tools of the JSON-RPC ecosystem. For oRPC's own protocol, see [RPC](/docs/rpc), and for a REST API, see [OpenAPI](/docs/openapi).

- [OpenRPC Handler](/docs/openrpc/handler): serve procedures as JSON-RPC methods, with batches and notifications.
- [OpenRPC Link](/docs/openrpc/link): call a JSON-RPC server from a typesafe client.
- [OpenRPC Adapters](/docs/openrpc/adapters): the HTTP and WebSocket adapters that run them.
- [OpenRPC Specification](/docs/openrpc/specification): generate an OpenRPC document and serve it with service discovery.
54 changes: 54 additions & 0 deletions apps/content/docs/openrpc/link.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: "OpenRPC Link"
description: "Use OpenRPCLink to call OpenRPCHandler, or any other JSON-RPC 2.0 server, from a typesafe oRPC client that restores typed errors."
---

## Overview

`OpenRPCLink` sends each call as a JSON-RPC 2.0 request over HTTP `POST`, whose `method` is the procedure path joined by `.`. It needs no contract, so it can call [OpenRPC Handler](/docs/openrpc/handler) or any other JSON-RPC server.

```ts
import type { JsonifiedClient } from '@orpc/openapi'
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { OpenRPCLink } from '@orpc/openrpc/fetch'

const link = new OpenRPCLink({
url: '/rpc',
origin: 'http://localhost:3000',
headers: () => ({ authorization: 'Bearer token' }),
})

const client: JsonifiedClient<RouterClient<typeof router>> = createORPCClient(link)

const planet = await client.planet.find({ id: 1 }) // { "jsonrpc": "2.0", "id": "1", "method": "planet.find", "params": { "id": 1 } }
```

An object input is sent as by-name `params`, any other input as `[input]`, and an `undefined` input without `params`.

:::info
Results are plain JSON, so use `JsonifiedClient` from `@orpc/openapi` to type them, such as a `Date` as a string. An `undefined` output arrives as `null`, although `JsonifiedClient` still types it as `undefined`, so check it with `== null`. With a [contract](/docs/contract-first), the [Smart Coercion Plugin](/docs/plugins/smart-coercion) can restore native types instead.
:::

## Error Handling

Errors from oRPC servers are restored as the original `ORPCError`, so [typesafe errors](/docs/client/error-handling) work as usual. Errors from other servers become an `ORPCError` with the code that matches their JSON-RPC error code (`INTERNAL_SERVER_ERROR` when none does), their message, and their `data`.

Since JSON-RPC errors arrive with HTTP status `200`, the error's `cause` gets the HTTP status of its code, so the [Retry Plugin](/docs/plugins/retry) retries server errors but not client errors. A response that is not a JSON-RPC response for the request throws a `MalformedResponseError`.

## WebSocket

`OpenRPCLink` from `@orpc/openrpc/websocket` sends the calls as messages on a WebSocket connection, such as to an [OpenRPC Handler over WebSocket](/docs/openrpc/handler#websocket), and matches the responses by `id`:

```ts
import { createORPCClient } from '@orpc/client'
import { OpenRPCLink } from '@orpc/openrpc/websocket'

const link = new OpenRPCLink({
connect: () => new WebSocket('ws://localhost:8080'),
})

const client: JsonifiedClient<RouterClient<typeof router>> = createORPCClient(link)
```

`connect` is called on the first call. When the socket closes, its pending calls fail, and the next call connects again. Aborting a call stops waiting for its response, but JSON-RPC cannot cancel it on the server.
11 changes: 11 additions & 0 deletions apps/content/docs/openrpc/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMeta } from 'blume'

export default defineMeta({
title: 'OpenRPC',
pages: [
'handler',
'link',
'adapters',
'specification',
],
})
Loading
Loading