Skip to content

Commit 504ff63

Browse files
authored
feat(rpc, openapi): AWS Lambda and Fastify adapters (#1767)
Upgrades `@standardserver/*` to `^0.7.1` and adds first-class adapters for AWS Lambda and Fastify: `RPCHandler` at `@orpc/server/aws-lambda` and `@orpc/server/fastify`, plus matching `OpenAPIHandler` at `@orpc/openapi/aws-lambda` and `@orpc/openapi/fastify`. Both follow the existing node adapter shape (interceptors, plugin composition, CSRF guard on by default in `RPCHandler`). ## Adapters - AWS Lambda supports API Gateway payload 1.0/2.0 and Function URL events, and streams responses via `awslambda.streamifyResponse` (event streams / SSE work on Lambda). - Fastify plugs into an existing app via `handler.handle(req, reply)`; with the documented catch-all content type parser, every content type oRPC understands is accepted and parsed by oRPC itself. - `fastify` is an optional peer dependency of `@orpc/server`, mirroring `crossws`. ## Upgrade - All `@standardserver/*` deps bumped from `^0.6.0` to `^0.7.1`; no code changes were needed (the only breaking change, `toNodeHttpBody` becoming sync, is not used directly, and 0.7.1 is API-identical to 0.7.0). ## Docs - New adapter pages for AWS Lambda and Fastify with RPC/OpenAPI examples and sidebar entries; examples verified against the real types via `tsc`. ## Testing - 18 new tests cover context/prefix handling, unmatched routes, plugin interceptors, and CSRF guard default/opt-out for both transports; full `pnpm test`, `pnpm type:check`, and lint pass across the monorepo.
1 parent 5405899 commit 504ff63

44 files changed

Lines changed: 1475 additions & 110 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/content/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ export default withMermaid(defineConfig({
141141
items: [
142142
{ text: 'Fetch API', link: '/docs/adapters/fetch-api' },
143143
{ text: 'Node HTTP', link: '/docs/adapters/node-http' },
144+
{ text: 'AWS Lambda', link: '/docs/adapters/aws-lambda' },
145+
{ text: 'Fastify', link: '/docs/adapters/fastify' },
144146
{ text: 'WebSocket', link: '/docs/adapters/websocket' },
145147
{ text: 'Message Port', link: '/docs/adapters/message-port' },
146148
{ text: '---' },
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# AWS Lambda Adapter
2+
3+
oRPC supports [AWS Lambda](https://aws.amazon.com/lambda/) behind [API Gateway](https://aws.amazon.com/api-gateway/) (payload format 1.0 and 2.0) and [Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html).
4+
5+
::: warning
6+
This adapter requires the Lambda Node.js runtime with [response streaming](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html) enabled, so handlers must be wrapped with `awslambda.streamifyResponse`.
7+
:::
8+
9+
## Server Usage
10+
11+
::: code-group
12+
13+
```ts [RPC]
14+
import type { APIGatewayProxyEventV2, AwsLambdaGlobal } from '@standardserver/aws-lambda'
15+
import { onError } from '@orpc/server'
16+
import { RPCHandler } from '@orpc/server/aws-lambda'
17+
import { CORSHandlerPlugin } from '@orpc/server/plugins'
18+
19+
declare const awslambda: AwsLambdaGlobal
20+
21+
const handler = new RPCHandler(router, {
22+
plugins: [
23+
new CORSHandlerPlugin()
24+
],
25+
interceptors: [
26+
onError((error) => {
27+
console.error(error)
28+
}),
29+
],
30+
})
31+
32+
export const rpc = awslambda.streamifyResponse<APIGatewayProxyEventV2>(async (event, responseStream, context) => {
33+
const { matched } = await handler.handle(event, responseStream, {
34+
prefix: '/rpc',
35+
context: {} // Provide initial context if needed
36+
})
37+
38+
if (matched) {
39+
return
40+
}
41+
42+
awslambda.HttpResponseStream.from(responseStream, {
43+
statusCode: 404,
44+
headers: {},
45+
cookies: [],
46+
}).end('Not found')
47+
})
48+
```
49+
50+
```ts [OpenAPI]
51+
import type { APIGatewayProxyEventV2, AwsLambdaGlobal } from '@standardserver/aws-lambda'
52+
import { OpenAPIHandler } from '@orpc/openapi/aws-lambda'
53+
import { onError } from '@orpc/server'
54+
import { CORSHandlerPlugin } from '@orpc/server/plugins'
55+
56+
declare const awslambda: AwsLambdaGlobal
57+
58+
const handler = new OpenAPIHandler(router, {
59+
plugins: [
60+
new CORSHandlerPlugin()
61+
],
62+
interceptors: [
63+
onError((error) => {
64+
console.error(error)
65+
}),
66+
],
67+
})
68+
69+
export const api = awslambda.streamifyResponse<APIGatewayProxyEventV2>(async (event, responseStream, context) => {
70+
const { matched } = await handler.handle(event, responseStream, {
71+
prefix: '/api',
72+
context: {} // Provide initial context if needed
73+
})
74+
75+
if (matched) {
76+
return
77+
}
78+
79+
awslambda.HttpResponseStream.from(responseStream, {
80+
statusCode: 404,
81+
headers: {},
82+
cookies: [],
83+
}).end('Not found')
84+
})
85+
```
86+
87+
:::
88+
89+
<!--@include: @/shared/standard-server-cors-warning.md -->
90+
91+
## Event Stream Options
92+
93+
You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler.
94+
95+
```ts
96+
const handler = new OpenAPIHandler(router, {
97+
sendStandardResponse: {
98+
eventStream: {
99+
initialComment: {
100+
/**
101+
* If true, an initial comment is sent immediately upon stream start to flush headers.
102+
* This allows the receiving side to establish the connection without waiting for the first event.
103+
*
104+
* @default true
105+
*/
106+
enabled: true,
107+
/**
108+
* The content of the initial comment sent upon stream start. Must not include newline characters.
109+
*
110+
* @default ''
111+
*/
112+
comment: '',
113+
},
114+
keepAlive: {
115+
/**
116+
* If true, a ping comment is sent periodically to keep the connection alive.
117+
*
118+
* @default true
119+
*/
120+
enabled: true,
121+
/**
122+
* Interval (in milliseconds) between ping comments sent after the last event.
123+
*
124+
* @default 15000
125+
*/
126+
interval: 15000,
127+
/**
128+
* The content of the ping comment. Must not include newline characters.
129+
*
130+
* @default ''
131+
*/
132+
comment: '',
133+
},
134+
/**
135+
* If true, a `close` event is sent even when the iterator completes with `undefined`.
136+
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
137+
*
138+
* @default true
139+
*/
140+
emptyCloseEventEnabled: true,
141+
},
142+
},
143+
})
144+
```
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Fastify Adapter
2+
3+
oRPC supports [Fastify](https://fastify.dev/) servers out of the box.
4+
5+
## Server Usage
6+
7+
::: code-group
8+
9+
```ts [RPC]
10+
import { onError } from '@orpc/server'
11+
import { RPCHandler } from '@orpc/server/fastify'
12+
import { CORSHandlerPlugin } from '@orpc/server/plugins'
13+
import Fastify from 'fastify'
14+
15+
const handler = new RPCHandler(router, {
16+
plugins: [
17+
new CORSHandlerPlugin()
18+
],
19+
interceptors: [
20+
onError((error) => {
21+
console.error(error)
22+
}),
23+
],
24+
})
25+
26+
const app = Fastify()
27+
28+
app.all('/rpc/*', async (req, reply) => {
29+
const { matched } = await handler.handle(req, reply, {
30+
prefix: '/rpc',
31+
context: {} // Provide initial context if needed
32+
})
33+
34+
if (matched) {
35+
return reply
36+
}
37+
38+
return reply.status(404).send('Not found')
39+
})
40+
41+
app.listen({ port: 3000 }).then(() => console.log('Listening on port 3000'))
42+
```
43+
44+
```ts [OpenAPI]
45+
import { OpenAPIHandler } from '@orpc/openapi/fastify'
46+
import { onError } from '@orpc/server'
47+
import { CORSHandlerPlugin } from '@orpc/server/plugins'
48+
import Fastify from 'fastify'
49+
50+
const handler = new OpenAPIHandler(router, {
51+
plugins: [
52+
new CORSHandlerPlugin()
53+
],
54+
interceptors: [
55+
onError((error) => {
56+
console.error(error)
57+
}),
58+
],
59+
})
60+
61+
const app = Fastify()
62+
63+
app.all('/api/*', async (req, reply) => {
64+
const { matched } = await handler.handle(req, reply, {
65+
prefix: '/api',
66+
context: {} // Provide initial context if needed
67+
})
68+
69+
if (matched) {
70+
return reply
71+
}
72+
73+
return reply.status(404).send('Not found')
74+
})
75+
76+
app.listen({ port: 3000 }).then(() => console.log('Listening on port 3000'))
77+
```
78+
79+
:::
80+
81+
::: tip
82+
Fastify only accepts content types it has a registered parser for, and parses request bodies itself. For the best oRPC experience, register a catch-all parser with `app.addContentTypeParser('*', ...)` so every content type is supported, and call `app.removeAllContentTypeParsers()` so every body is parsed by oRPC instead of Fastify:
83+
84+
```ts
85+
// Optional, let oRPC parse all content types
86+
app.removeAllContentTypeParsers()
87+
88+
// Optional, support all content types
89+
app.addContentTypeParser('*', (request, payload, done) => {
90+
done(null, undefined)
91+
})
92+
```
93+
94+
:::
95+
96+
<!--@include: @/shared/standard-server-cors-warning.md -->
97+
98+
## Event Stream Options
99+
100+
You can configure how an [AsyncIteratorObject](/docs/async-iterator-object) is streamed to the client using the `sendStandardResponse.eventStream` options when creating the handler.
101+
102+
```ts
103+
const handler = new OpenAPIHandler(router, {
104+
sendStandardResponse: {
105+
eventStream: {
106+
initialComment: {
107+
/**
108+
* If true, an initial comment is sent immediately upon stream start to flush headers.
109+
* This allows the receiving side to establish the connection without waiting for the first event.
110+
*
111+
* @default true
112+
*/
113+
enabled: true,
114+
/**
115+
* The content of the initial comment sent upon stream start. Must not include newline characters.
116+
*
117+
* @default ''
118+
*/
119+
comment: '',
120+
},
121+
keepAlive: {
122+
/**
123+
* If true, a ping comment is sent periodically to keep the connection alive.
124+
*
125+
* @default true
126+
*/
127+
enabled: true,
128+
/**
129+
* Interval (in milliseconds) between ping comments sent after the last event.
130+
*
131+
* @default 15000
132+
*/
133+
interval: 15000,
134+
/**
135+
* The content of the ping comment. Must not include newline characters.
136+
*
137+
* @default ''
138+
*/
139+
comment: '',
140+
},
141+
/**
142+
* If true, a `close` event is sent even when the iterator completes with `undefined`.
143+
* When the iterator returns a value, a `close` event is always emitted regardless of this setting.
144+
*
145+
* @default true
146+
*/
147+
emptyCloseEventEnabled: true,
148+
},
149+
},
150+
})
151+
```

apps/content/docs/adapters/fetch-api.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ export default {
8989
Deno.serve(fetch)
9090
```
9191

92-
```ts [Hono Lambda]
93-
import { handle } from 'hono/aws-lambda'
94-
95-
export const handler = handle({ fetch })
96-
```
97-
9892
:::
9993

10094
<!--@include: @/shared/standard-server-cors-warning.md -->

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@
3737
"@orpc/tanstack-query": "workspace:*",
3838
"@orpc/valibot": "workspace:*",
3939
"@orpc/zod": "workspace:*",
40-
"@standardserver/core": "^0.6.0",
41-
"@standardserver/fetch": "^0.6.0",
42-
"@standardserver/peer": "^0.6.0",
43-
"@standardserver/shared": "^0.6.0",
40+
"@standardserver/core": "^0.7.1",
41+
"@standardserver/fetch": "^0.7.1",
42+
"@standardserver/peer": "^0.7.1",
43+
"@standardserver/shared": "^0.7.1",
4444
"@testing-library/dom": "^10.4.1",
4545
"@testing-library/react": "^16.3.2",
4646
"@types/node": "^26.0.1",

packages/bun/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"@orpc/ratelimit": "workspace:*",
4040
"@orpc/server": "workspace:*",
4141
"@orpc/shared": "workspace:*",
42-
"@standardserver/core": "^0.6.0"
42+
"@standardserver/core": "^0.7.1"
4343
},
4444
"devDependencies": {
4545
"@types/bun": "^1.3.14",

packages/client/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@
6666
},
6767
"dependencies": {
6868
"@orpc/shared": "workspace:*",
69-
"@standardserver/core": "^0.6.0",
70-
"@standardserver/fetch": "^0.6.0",
71-
"@standardserver/peer": "^0.6.0"
69+
"@standardserver/core": "^0.7.1",
70+
"@standardserver/fetch": "^0.7.1",
71+
"@standardserver/peer": "^0.7.1"
7272
},
7373
"devDependencies": {
7474
"zod": "^4.4.3"

packages/client/src/adapters/fetch/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface FetchLinkTransportPlugin<T extends ClientContext> extends Stand
88
}
99

1010
export class CompositeFetchLinkTransportPlugin<T extends ClientContext> implements FetchLinkTransportPlugin<T> {
11-
name = '~composite/fetch-link-transport'
11+
name = '~composite/fetch'
1212

1313
constructor(
1414
protected readonly plugins: FetchLinkTransportPlugin<T>[] = [],

packages/cloudflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@orpc/publisher": "workspace:*",
4343
"@orpc/ratelimit": "workspace:*",
4444
"@orpc/shared": "workspace:*",
45-
"@standardserver/core": "^0.6.0"
45+
"@standardserver/core": "^0.7.1"
4646
},
4747
"devDependencies": {
4848
"@cloudflare/vitest-pool-workers": "^0.19.0",

packages/evlog/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"@orpc/client": "workspace:*",
4949
"@orpc/server": "workspace:*",
5050
"@orpc/shared": "workspace:*",
51-
"@standardserver/core": "^0.6.0"
51+
"@standardserver/core": "^0.7.1"
5252
},
5353
"devDependencies": {
5454
"evlog": "^2.22.3"

0 commit comments

Comments
 (0)