Skip to content

Commit b084274

Browse files
authored
feat: add oRPC agent skills and publish them for discovery (#1884)
Adds four agent skills under a repo-root `skills/` folder that teach coding agents (Claude Code, Codex, Cursor) to work with oRPC v2, and publishes them for discovery: the docs build now emits `/.well-known/agent-skills/` (index plus per-skill `SKILL.md`, per the Agent Skills Discovery RFC) via `ai.skills` in the Blume config, and the homepage hero now advertises `npx skills add middleapi/orpc`. ## Skills - `orpc`: the hub. Procedures, routers, middleware/context, typed errors, `RPCHandler`, server- and client-side clients, plus a doc map and retrieval instructions. - `orpc-contract`: contract-first. `oc`, `implement`, contract-typed clients over both links, shipping a minified contract, publishing a typed SDK to npm, Hey API generation. - `orpc-openapi`: REST exposure. Route metadata, input/output mapping, `OpenAPIHandler`/`OpenAPILink`, Smart Coercion, spec generation, Scalar. - `orpc-migrate`: tRPC to oRPC (incremental via `@orpc/trpc` or full rewrite) and v1 to v2. Descriptions partition the space and cross-route in both directions, so agents load the right one. ## Accuracy Every API claim was sourced from the live v2 docs (exact per-page markdown), then adversarially re-verified against them; pretrained-model knowledge of oRPC is v1-shaped, so each skill opens by telling the agent to prefer retrieval and trust fetched pages over the skill itself. Beta-era facts self-correct instead of rotting: install guidance checks `npm view @orpc/server dist-tags` to know when the beta has ended. Docs links use `v2.orpc.dev` because `orpc.dev` still serves v1; at v2 launch a sweep of `v2.orpc.dev` and the beta caveats is expected alongside the existing docs-wide `@beta` sweep. ## Notes - `skills/**` joins the docs/playgrounds eslint exemption block: its code fences are user-facing samples, so internal-only rules like `ban/ban` (`JSON.stringify`) don't apply. - Frontmatter passes Blume's publish validation (name/description rules); zero em dashes; all links verified 200; `pnpm docs:validate` and lint are clean.
1 parent b6a88ad commit b084274

7 files changed

Lines changed: 673 additions & 1 deletion

File tree

apps/content/blume.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export default defineConfig({
140140
mcp: {
141141
enabled: true,
142142
},
143+
skills: '../../skills',
143144
},
144145
deployment: {
145146
output: 'server',

apps/content/pages/_home/Hero.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { githubStars } from './stats'
99
// unreachable, and the button simply loses its count.
1010
const stars = await githubStars()
1111
12-
const install = 'npm install @orpc/server@beta @orpc/client@beta'
12+
const install = 'npx skills add middleapi/orpc'
1313
---
1414

1515
<section class="relative isolate overflow-hidden border-b border-border">

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export default antfu({
8484
'apps/content/examples/**',
8585
'playgrounds/**',
8686
'packages/*/playground/**',
87+
'skills/**',
8788
],
8889
rules: {
8990
'no-restricted-imports': 'off',

skills/orpc-contract/SKILL.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
name: orpc-contract
3+
description: "Design oRPC v2 APIs contract-first, defining the API shape with oc from `@orpc/contract`, implementing it with implement from `@orpc/server`, and consuming the contract from typesafe clients. Use when a project depends on `@orpc/contract`, when defining a contract with oc, implementing a contract with implement, sharing an API contract between server and client packages, generating a contract from an existing OpenAPI spec, or publishing a typed API client to npm. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For core builder, serving, and client work without a contract, use the orpc skill; for REST/OpenAPI exposure, spec generation, and OpenAPILink details, use the orpc-openapi skill."
4+
license: MIT
5+
---
6+
7+
# oRPC Contract-First
8+
9+
Contract-first oRPC splits an API into two artifacts: a contract (schemas, errors, metadata, no business logic) defined with `oc` from `@orpc/contract`, and an implementation built from it with `implement` from `@orpc/server`. Server and client both depend on the contract, never on each other, so the API shape can live in its own package, be reviewed on its own, and ship to consumers as a typed SDK. Prefer it when server and client are separate packages or teams, when the shape starts from an existing OpenAPI spec, or when publishing a client to npm. Stay with the `os`-first flow when one codebase holds both sides and the client can import the router type directly; converting later is cheap because a plain router already works as a router contract (resolve lazy routers with `unlazyRouter` first).
10+
11+
This skill targets oRPC v2. The `orpc` skill carries the v2 install and version check plus core builder, middleware, serving, and client concepts. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its docs page first (see [Full documentation](#full-documentation)).
12+
13+
## Define the contract with oc
14+
15+
Every chain is optional and each call returns a new instance, so share base contracts freely. A contract has no `.handler`. Zod, Valibot, ArkType, and any other Standard Schema library work.
16+
17+
```ts
18+
import { oc } from '@orpc/contract'
19+
import * as z from 'zod'
20+
21+
export const contract = {
22+
planet: {
23+
list: oc
24+
.output(z.array(z.object({ id: z.number(), name: z.string() }))),
25+
find: oc
26+
.errors({ NOT_FOUND: {} })
27+
.input(z.object({ id: z.number() }))
28+
.output(z.object({ id: z.number(), name: z.string() })),
29+
},
30+
}
31+
```
32+
33+
- Always define `.output`: without it clients infer the output as `unknown`.
34+
- A router contract is a plain object mapping keys to procedure contracts or nested objects. Avoid the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.
35+
- Attach shared metadata to a whole subtree with `oc.meta(someMeta).router({...})`.
36+
- `.errors({ NOT_FOUND: {} })` declares typesafe errors; implementations throw them via `errors.NOT_FOUND()` and clients infer their shapes.
37+
- Repeated `.input`/`.output` calls stack schemas instead of replacing them: object input schemas compose into one flat value, output schemas pipe. Use this to extend a base contract without repeating fields.
38+
- Schema-library-free contracts use the `type` utility from `@orpc/contract`: `oc.input(type<{ value: number }>())`, optionally with a mapping function as `type<Input, Output>(fn)`.
39+
- REST routes attach via `.meta(openapi({ method: 'GET', path: '/planets/{id}' }))` from `@orpc/openapi`, exactly as on `os`; routing rules, `prefix`, and spec generation belong to the `orpc-openapi` skill.
40+
41+
Infer types with `InferRouterContractInputs`, `InferRouterContractOutputs`, and `InferRouterContractErrors` from `@orpc/contract`.
42+
43+
## Implement with implement
44+
45+
`implement` turns the contract into an implementer that mirrors its shape and type-checks every handler; `.router` also enforces the contract at runtime.
46+
47+
```ts
48+
import { implement } from '@orpc/server'
49+
50+
const implementer = implement(contract).$context<{ db: DB }>()
51+
52+
const listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())
53+
54+
const findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {
55+
const planet = await context.db.find(input.id)
56+
if (!planet)
57+
throw errors.NOT_FOUND()
58+
return planet
59+
})
60+
61+
export const router = implementer.router({
62+
planet: { list: listPlanets, find: findPlanet },
63+
})
64+
```
65+
66+
- `.$context` declares the initial context the procedures require, as on `os`.
67+
- Apply middleware per procedure with `.use(mw)` before `.handler`. That runs after input validation (the contract already registered `.input`); to wrap validation, apply it at router level: `implementer.use(mw)` for every procedure, or `implementer.planet.use(mw).list` for a subtree. Router-level plus procedure-level `.use` can run the same middleware twice; use the dedupe pattern from the `orpc` skill.
68+
- `implementer.middleware(fn)` creates middleware that infers the contract's typesafe errors. When not every procedure defines a code, guard with the `in` operator: `if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS()`. Any type-compatible middleware also works.
69+
- The result is a normal router: serve it with `RPCHandler` (`orpc` skill) or `OpenAPIHandler` (`orpc-openapi` skill), call it in-process with `call` or `createRouterClient` from `@orpc/server`.
70+
71+
## Consume the contract from clients
72+
73+
`RPCLink` needs only the contract type; `OpenAPILink` takes the contract as a runtime value to read each procedure's route. Get the client types exactly right:
74+
75+
```ts
76+
import type { RouterContractClient } from '@orpc/contract'
77+
import type { JsonifiedClient } from '@orpc/openapi'
78+
import { createORPCClient } from '@orpc/client'
79+
import { RPCLink } from '@orpc/client/fetch'
80+
import { OpenAPILink } from '@orpc/openapi/fetch'
81+
82+
// RPC protocol (server side is RPCHandler)
83+
const rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })
84+
const client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)
85+
86+
// OpenAPI protocol (OpenAPIHandler or any spec-compliant server)
87+
const openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })
88+
const apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)
89+
```
90+
91+
- Router-first equivalents use `RouterClient<typeof router>` from `@orpc/server` in the same positions.
92+
- `JsonifiedClient` is required over `OpenAPILink` because OpenAPI serialization is one-way (a `Date` returns as a string); dropping it via Smart Coercion, plus `OpenAPILink` options and CORS caveats, are in the `orpc-openapi` skill.
93+
- Per-call client context is the second type parameter, `RouterContractClient<typeof contract, ClientContext>`, then `client.planet.find(input, { context: { token } })`; link options like `headers` accept functions of that context.
94+
- Export `RouterContractClient<typeof contract>` as a type from the server package so clients never import the contract module itself (still needed as a runtime value for `OpenAPILink`; ship the minified JSON below).
95+
- In very large codebases, skip the root client: pin each procedure with `.meta(meta.path([...]))` and build per-procedure clients with `createContractClientFactory` from `@orpc/contract` (`createContractJsonifiedClientFactory` from `@orpc/openapi` when the link needs `JsonifiedClient`); fetch `advanced/scaling-large-projects` before adopting it.
96+
97+
## Ship the contract
98+
99+
When the contract is derived from a router, importing it on the client is heavy and may expose internals. Minify and export JSON instead:
100+
101+
```ts
102+
import fs from 'node:fs'
103+
import { minifyRouterContract } from '@orpc/contract'
104+
import { unlazyRouter } from '@orpc/server'
105+
106+
const minified = minifyRouterContract(await unlazyRouter(router))
107+
fs.writeFileSync('./contract.json', JSON.stringify(minified))
108+
```
109+
110+
`minifyRouterContract` keeps only client-needed metadata. On the client, import the JSON and cast, since schemas do not survive serialization: `new OpenAPILink(contract as typeof router, ...)`.
111+
112+
To publish a typed SDK to npm, export a factory that pairs the contract with a link:
113+
114+
```ts
115+
import type { RouterContractClient } from '@orpc/contract'
116+
import { createORPCClient } from '@orpc/client'
117+
import { RPCLink } from '@orpc/client/fetch'
118+
119+
export function createMyApi(apiKey: string): RouterContractClient<typeof contract> {
120+
const link = new RPCLink({
121+
origin: 'https://example.com',
122+
url: '/rpc',
123+
headers: { 'x-api-key': apiKey },
124+
})
125+
return createORPCClient(link)
126+
}
127+
```
128+
129+
Bundle with `tsdown --dts src/index.ts`, point `exports` at `dist` types plus import entries, list `@orpc/client` and `@orpc/contract` as dependencies, and publish. Consumers get a fully typed client that works with every oRPC client integration (TanStack Query included). Fetch `advanced/publish-client-to-npm` for the complete `package.json`.
130+
131+
## Generate the contract from an existing OpenAPI spec
132+
133+
Use Hey API's `orpc` plugin instead of hand-writing the contract. Install `@hey-api/openapi-ts@next` as a dev dependency (oRPC v2 output requires the `next` tag until the next stable Hey API release) and create `openapi-ts.config.ts`:
134+
135+
```ts
136+
import { defineConfig } from '@hey-api/openapi-ts'
137+
138+
export default defineConfig({
139+
input: 'https://example.com/openapi.json', // local file or URL
140+
output: 'src/contract',
141+
plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],
142+
})
143+
```
144+
145+
Then run `npx @hey-api/openapi-ts`. It writes `orpc.gen.ts` (one procedure contract per operation, routed via `.meta(openapi({...}))` with `inputStructure: 'detailed'`, plus a combined `contract` router) and `zod.gen.ts`. The generated files import `@orpc/contract`, `@orpc/openapi`, and `zod`, so install those too. From there, implement the contract on your own server, or point `OpenAPILink` at the existing spec-compliant server.
146+
147+
## Full documentation
148+
149+
If this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. While v2 is in beta the docs are served at https://v2.orpc.dev:
150+
151+
- https://v2.orpc.dev/llms.txt : index of every page with descriptions (links inside print the orpc.dev domain; swap in v2.orpc.dev before fetching)
152+
- https://v2.orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)
153+
- Append `.md` to any docs URL for that page's exact source markdown (for example https://v2.orpc.dev/docs/contract/procedure.md)
154+
155+
Pages to fetch when you need details beyond this skill:
156+
157+
- Contract: `contract/procedure`, `contract/router`, `contract/implementation`, `contract/generate-from-openapi`
158+
- Clients: `client/client-side`, `client/server-side`, `client/error-handling`, `openapi/link`
159+
- Workflows: `advanced/publish-client-to-npm`, `advanced/scaling-large-projects`, `best-practices/monorepo-setup`

0 commit comments

Comments
 (0)