Skip to content

Commit b84477d

Browse files
dinwwwhclaude
andauthored
feat(trpc): tRPC integration (#1683)
## Summary Brings the tRPC integration (`@orpc/trpc`) to v2. You can now convert a tRPC router into an oRPC router and use it with any oRPC feature: ```ts const orpcRouter = toORPCRouter(trpcRouter) ``` - Works with RPC/OpenAPI handlers, server-side clients, OpenAPI spec generation, subscriptions (including tracked events), and lazy routers - `toTRPCMeta` lets you use oRPC meta plugins (like `openapi(...)`) directly in tRPC's `.meta()` - New [tRPC Integration](https://orpc.dev/docs/integrations/trpc) docs page - 100% test coverage --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 61ef170 commit b84477d

14 files changed

Lines changed: 1103 additions & 1 deletion

File tree

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export default withMermaid(defineConfig({
193193
{ text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' },
194194
{ text: 'Pino', link: '/docs/integrations/pino' },
195195
{ text: 'Tanstack Query', link: '/docs/integrations/tanstack-query' },
196+
{ text: 'tRPC', link: '/docs/integrations/trpc' },
196197
],
197198
},
198199
{
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# tRPC Integration
2+
3+
This guide shows how to integrate [tRPC](https://trpc.io/) with oRPC, so you can use oRPC features in your existing tRPC applications.
4+
5+
## Installation
6+
7+
::: code-group
8+
9+
```sh [npm]
10+
npm install @orpc/trpc@beta
11+
```
12+
13+
```sh [yarn]
14+
yarn add @orpc/trpc@beta
15+
```
16+
17+
```sh [pnpm]
18+
pnpm add @orpc/trpc@beta
19+
```
20+
21+
```sh [bun]
22+
bun add @orpc/trpc@beta
23+
```
24+
25+
```sh [deno]
26+
deno add npm:@orpc/trpc@beta
27+
```
28+
29+
:::
30+
31+
## Router Conversion
32+
33+
`toORPCRouter` converts a [tRPC router](https://trpc.io/docs/server/routers) into an [oRPC router](/docs/router):
34+
35+
```ts
36+
import { toORPCRouter } from '@orpc/trpc'
37+
38+
const orpcRouter = toORPCRouter(trpcRouter)
39+
```
40+
41+
The result is a regular oRPC router that works with any oRPC feature. For example, you can expose it through an [RPC Handler](/docs/rpc/handler) or [OpenAPI Handler](/docs/openapi/handler), or call it directly with [Server-Side Clients](/docs/client/server-side).
42+
43+
### Error Formatting
44+
45+
`toORPCRouter` does not support [tRPC Error Formatting](https://trpc.io/docs/server/error-formatting). Instead, errors thrown by tRPC are wrapped in `ORPCError`.
46+
47+
```ts
48+
const handler = new OpenAPIHandler(orpcRouter, {
49+
interceptors: [
50+
async ({ next }) => {
51+
try {
52+
return await next()
53+
}
54+
catch (error) {
55+
if (
56+
error instanceof ORPCError
57+
&& error.cause instanceof TRPCError
58+
&& error.cause.cause instanceof z.ZodError
59+
) {
60+
throw new ORPCError('UNPROCESSABLE_CONTENT', {
61+
message: z.prettifyError(error.cause.cause),
62+
data: z.flattenError(error.cause.cause),
63+
cause: error.cause.cause,
64+
})
65+
}
66+
67+
throw error
68+
}
69+
},
70+
],
71+
})
72+
```
73+
74+
## Metadata
75+
76+
`toTRPCMeta` bridges [oRPC metadata](/docs/metadata) with tRPC meta. It returns a plain object that you can pass to tRPC `.meta` calls.
77+
78+
```ts
79+
import { openapi } from '@orpc/openapi'
80+
import { toTRPCMeta } from '@orpc/trpc'
81+
82+
export const t = initTRPC.context<Context>().create()
83+
84+
const example = t.procedure
85+
.meta(toTRPCMeta(openapi({ path: '/hello', summary: 'Hello procedure' }))) // [!code highlight]
86+
.input(z.object({ name: z.string() }))
87+
.query(({ input }) => {
88+
return `Hello, ${input.name}!`
89+
})
90+
91+
const merged = t.procedure
92+
.meta({
93+
...toTRPCMeta( // [!code highlight]
94+
openapi({ path: '/hello' }), // [!code highlight]
95+
openapi({ method: 'POST' }), // [!code highlight]
96+
), // [!code highlight]
97+
other: 'value',
98+
})
99+
.input(z.object({ name: z.string() }))
100+
.mutation(({ input }) => {
101+
return `Hello, ${input.name}!`
102+
})
103+
```
104+
105+
::: warning
106+
Chained tRPC `.meta()` calls merge shallowly, so oRPC metadata merge logic (e.g. accumulating `openapi.tags`) only works within a single `toTRPCMeta` call.
107+
:::

apps/content/docs/migrations/from-trpc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This guide shows how to migrate an existing tRPC app to oRPC. Because oRPC is heavily inspired by tRPC, most concepts map directly, so the migration should feel familiar.
44

55
::: info
6-
If you want to add oRPC features to an existing tRPC app without a full migration, see [tRPC Integration](/docs/openapi/integrations/trpc).
6+
If you want to add oRPC features to an existing tRPC app without a full migration, see [tRPC Integration](/docs/integrations/trpc).
77
:::
88

99
## Core Concepts Comparison

packages/trpc/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Hidden folders and files
2+
.*
3+
!.gitignore
4+
!.*.example
5+
6+
# Common generated folders
7+
logs/
8+
node_modules/
9+
out/
10+
dist/
11+
dist-ssr/
12+
build/
13+
coverage/
14+
temp/
15+
16+
# Common generated files
17+
*.log
18+
*.log.*
19+
*.tsbuildinfo
20+
*.vitest-temp.json
21+
vite.config.ts.timestamp-*
22+
vitest.config.ts.timestamp-*
23+
24+
# Common manual ignore files
25+
*.local
26+
*.pem

packages/trpc/README.md

Lines changed: 195 additions & 0 deletions
Large diffs are not rendered by default.

packages/trpc/package.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@orpc/trpc",
3+
"type": "module",
4+
"version": "2.0.0-beta.18",
5+
"license": "MIT",
6+
"homepage": "https://orpc.dev",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/middleapi/orpc.git",
10+
"directory": "packages/trpc"
11+
},
12+
"keywords": [
13+
"orpc",
14+
"trpc"
15+
],
16+
"sideEffects": false,
17+
"publishConfig": {
18+
"exports": {
19+
"./package.json": "./package.json",
20+
".": {
21+
"types": "./dist/index.d.mts",
22+
"import": "./dist/index.mjs",
23+
"default": "./dist/index.mjs"
24+
}
25+
}
26+
},
27+
"exports": {
28+
"./package.json": "./package.json",
29+
".": "./src/index.ts"
30+
},
31+
"files": [
32+
"dist"
33+
],
34+
"scripts": {
35+
"build": "unbuild",
36+
"type:check": "tsc -b"
37+
},
38+
"peerDependencies": {
39+
"@trpc/server": ">=11.4.2"
40+
},
41+
"dependencies": {
42+
"@orpc/contract": "workspace:*",
43+
"@orpc/server": "workspace:*",
44+
"@orpc/shared": "workspace:*"
45+
},
46+
"devDependencies": {
47+
"@orpc/openapi": "workspace:*",
48+
"@trpc/server": "^11.12.0",
49+
"zod": "^4.4.3"
50+
}
51+
}

packages/trpc/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './to-orpc-router'
2+
export * from './to-trpc-meta'
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { InferRouterInitialContext, Procedure, Router, Schema } from '@orpc/server'
2+
import type { AsyncIteratorClass } from '@orpc/shared'
3+
import type { TrackedData } from '@trpc/server/unstable-core-do-not-import'
4+
import { initTRPC, lazy, tracked, TRPCError } from '@trpc/server'
5+
import * as z from 'zod'
6+
import { toORPCRouter } from './to-orpc-router'
7+
8+
type TRPCContext = { a: string }
9+
10+
const inputSchema = z.object({ input: z.number().transform(n => `${n}`) })
11+
12+
const outputSchema = z.object({ output: z.number().transform(n => `${n}`) })
13+
14+
const t = initTRPC.context<(req: Request) => TRPCContext>().create()
15+
16+
const trpcRouter = t.router({
17+
ping: t.procedure
18+
.input(inputSchema)
19+
.output(outputSchema)
20+
.query(({ input }) => {
21+
return { output: Number(input.input) }
22+
}),
23+
24+
throw: t.procedure
25+
.input(z.object({ b: z.number(), c: z.string() }))
26+
.query(() => {
27+
throw new TRPCError({
28+
code: 'PARSE_ERROR',
29+
message: 'throw',
30+
})
31+
}),
32+
33+
subscribe: t.procedure
34+
.input(z.object({ u: z.string() }))
35+
.subscription(async function* () {
36+
yield 'pong'
37+
yield tracked('id-1', { order: 1 })
38+
}),
39+
40+
nested: {
41+
ping: t.procedure
42+
.input(z.object({ a: z.string() }))
43+
.output(z.string().transform(val => Number(val)))
44+
.query(({ input }) => {
45+
return `1234${input.a}`
46+
}),
47+
},
48+
49+
lazy: lazy(() => Promise.resolve({ default: t.router({
50+
subscribe: t.procedure
51+
.subscription(async function* () {
52+
yield 'pong'
53+
}),
54+
55+
lazy: lazy(() => Promise.resolve({ default: t.router({
56+
throw: t.procedure
57+
.input(inputSchema)
58+
.output(outputSchema)
59+
.query(() => {
60+
throw new Error('lazy.lazy.throw')
61+
}),
62+
}) })),
63+
}) })),
64+
})
65+
66+
describe('toORPCRouter', () => {
67+
const orpcRouter = toORPCRouter(trpcRouter)
68+
69+
expectTypeOf(orpcRouter).toExtend<Router<TRPCContext>>()
70+
71+
expectTypeOf<InferRouterInitialContext<typeof orpcRouter>>().toEqualTypeOf<{ a: string }>()
72+
73+
expectTypeOf(orpcRouter.ping).toEqualTypeOf<
74+
Procedure<TRPCContext, object, Schema<{ input: number }, unknown>, Schema<unknown, { output: string }>, object, never>
75+
>()
76+
77+
expectTypeOf(orpcRouter.throw).toEqualTypeOf<
78+
Procedure<TRPCContext, object, Schema<{ b: number, c: string }, unknown>, Schema<unknown, never>, object, never>
79+
>()
80+
81+
expectTypeOf(orpcRouter.subscribe).toEqualTypeOf<
82+
Procedure<TRPCContext, object, Schema<{ u: string }, unknown>, Schema<unknown, AsyncIteratorClass<'pong' | TrackedData<{ order: number }>, void, any>>, object, never>
83+
>()
84+
85+
expectTypeOf(orpcRouter.nested).toEqualTypeOf<
86+
{
87+
ping: Procedure<TRPCContext, object, Schema<{ a: string }, unknown>, Schema<unknown, number>, object, never>
88+
}
89+
>()
90+
91+
expectTypeOf(orpcRouter.lazy).toEqualTypeOf<
92+
{
93+
subscribe: Procedure<TRPCContext, object, Schema<void, unknown>, Schema<unknown, AsyncIteratorClass<string, void, any>>, object, never>
94+
lazy: {
95+
throw: Procedure<TRPCContext, object, Schema<{ input: number }, unknown>, Schema<unknown, { output: string }>, object, never>
96+
}
97+
}
98+
>()
99+
})

0 commit comments

Comments
 (0)