Skip to content

Commit 3312214

Browse files
authored
feat(server): ratelimit helpers & plugins (#1183)
Closes: #910 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced rate-limiting with Memory, Redis, and Upstash adapters; middleware supports per-request dedupe and optional blocking mode; automatic HTTP headers (RateLimit-Limit/Remaining/Reset) and Retry-After. * **Documentation** * Added comprehensive "Ratelimit" docs, examples, navigation entry, and package README. * **Tests** * Added unit, integration, and end-to-end tests covering adapters, middleware, handler integration, and headers. * **Chores** * New experimental ratelimit package manifest, devDependency added, and package ignore updates. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 05d8e79 commit 3312214

23 files changed

+2105
-0
lines changed

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export default withMermaid(defineConfig({
164164
{ text: 'Encryption', link: '/docs/helpers/encryption' },
165165
{ text: 'Form Data', link: '/docs/helpers/form-data' },
166166
{ text: 'Publisher', link: '/docs/helpers/publisher' },
167+
{ text: 'Ratelimit', link: '/docs/helpers/ratelimit' },
167168
{ text: 'Signing', link: '/docs/helpers/signing' },
168169
],
169170
},
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
---
2+
title: Rate Limit
3+
description: Rate limiting features for oRPC with multiple adapters support.
4+
---
5+
6+
# Rate Limit
7+
8+
The Rate Limit package provides flexible rate limiting for oRPC with multiple storage backend support. It includes adapters for in-memory, Redis, and Upstash, along with middleware and plugin helpers for seamless integration.
9+
10+
## Installation
11+
12+
::: code-group
13+
14+
```sh [npm]
15+
npm install @orpc/experimental-ratelimit@latest
16+
```
17+
18+
```sh [yarn]
19+
yarn add @orpc/experimental-ratelimit@latest
20+
```
21+
22+
```sh [pnpm]
23+
pnpm add @orpc/experimental-ratelimit@latest
24+
```
25+
26+
```sh [bun]
27+
bun add @orpc/experimental-ratelimit@latest
28+
```
29+
30+
```sh [deno]
31+
deno add npm:@orpc/experimental-ratelimit@latest
32+
```
33+
34+
:::
35+
36+
## Available Adapters
37+
38+
### Memory Adapter
39+
40+
A simple in-memory rate limiter using a sliding window log algorithm. Ideal for single-instance applications or development.
41+
42+
```ts
43+
import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory'
44+
45+
const limiter = new MemoryRatelimiter({
46+
maxRequests: 10, // Maximum requests allowed
47+
window: 60000, // Time window in milliseconds (60 seconds)
48+
})
49+
```
50+
51+
### Redis Adapter
52+
53+
Redis-based rate limiter using atomic Lua scripts for distributed rate limiting.
54+
55+
```ts
56+
import { RedisRatelimiter } from '@orpc/experimental-ratelimit/redis'
57+
import { Redis } from 'ioredis'
58+
59+
const redis = new Redis('redis://localhost:6379')
60+
61+
const limiter = new RedisRatelimiter({
62+
eval: async (script, numKeys, ...rest) => {
63+
return redis.eval(script, numKeys, ...rest)
64+
},
65+
maxRequests: 100,
66+
window: 60000,
67+
prefix: 'orpc:ratelimit:', // Optional key prefix
68+
})
69+
```
70+
71+
::: info
72+
You can use any Redis client that supports Lua script evaluation by providing an `eval` function.
73+
:::
74+
75+
### Upstash Adapter
76+
77+
Adapter for [@upstash/ratelimit](https://www.npmjs.com/package/@upstash/ratelimit), optimized for serverless environments like Vercel Edge and Cloudflare Workers.
78+
79+
```ts
80+
import { Ratelimit } from '@upstash/ratelimit'
81+
import { Redis } from '@upstash/redis'
82+
import { UpstashRatelimiter } from '@orpc/experimental-ratelimit/upstash-ratelimit'
83+
84+
const redis = Redis.fromEnv()
85+
86+
const ratelimit = new Ratelimit({
87+
redis,
88+
limiter: Ratelimit.slidingWindow(10, '60 s'),
89+
prefix: 'my-app:',
90+
})
91+
92+
const limiter = new UpstashRatelimiter(ratelimit)
93+
```
94+
95+
::: tip Edge Runtime Support
96+
For Edge runtime like Vercel Edge or Cloudflare Workers, pass the `waitUntil` function to better handle background tasks:
97+
98+
```ts
99+
const limiter = new UpstashRatelimiter(ratelimit, {
100+
waitUntil: ctx.waitUntil.bind(ctx),
101+
})
102+
```
103+
104+
:::
105+
106+
## Blocking Mode
107+
108+
Some adapters support blocking mode, which waits for the rate limit to reset instead of immediately rejecting requests.
109+
110+
```ts
111+
const limiter = new MemoryRatelimiter({
112+
maxRequests: 10,
113+
window: 60000,
114+
blockingUntilReady: {
115+
enabled: true,
116+
timeout: 5000, // Wait up to 5 seconds
117+
},
118+
})
119+
```
120+
121+
## Manual Usage
122+
123+
You can use adapters directly without middleware for custom rate limiting logic:
124+
125+
```ts twoslash
126+
import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory'
127+
import { ORPCError } from '@orpc/server'
128+
129+
const limiter = new MemoryRatelimiter({
130+
maxRequests: 5,
131+
window: 60000,
132+
})
133+
134+
const result = await limiter.limit('user:123')
135+
136+
if (!result.success) {
137+
throw new ORPCError('TOO_MANY_REQUESTS', {
138+
data: {
139+
limit: result.limit,
140+
remaining: result.remaining,
141+
reset: result.reset,
142+
},
143+
})
144+
}
145+
```
146+
147+
## `createRatelimitMiddleware`
148+
149+
The `createRatelimitMiddleware` helper creates middleware for oRPC procedures to enforce rate limits.
150+
151+
```ts twoslash
152+
import { call, os } from '@orpc/server'
153+
import { MemoryRatelimiter } from '@orpc/experimental-ratelimit/memory'
154+
import { createRatelimitMiddleware, Ratelimiter } from '@orpc/experimental-ratelimit'
155+
import { z } from 'zod'
156+
157+
const loginProcedure = os
158+
.$context<{ ratelimiter: Ratelimiter }>()
159+
.input(z.object({ email: z.email() }))
160+
.use(
161+
createRatelimitMiddleware({
162+
limiter: ({ context }) => context.ratelimiter,
163+
key: ({ context }, input) => `login:${input.email}`,
164+
}),
165+
)
166+
.handler(({ input }) => {
167+
return { success: true }
168+
})
169+
170+
const ratelimiter = new MemoryRatelimiter({
171+
maxRequests: 10,
172+
window: 60000,
173+
})
174+
175+
const result = await call(
176+
loginProcedure,
177+
{ email: 'user@example.com' },
178+
{ context: { ratelimiter } }
179+
)
180+
```
181+
182+
::: info Automatic Deduplication
183+
The `createRatelimitMiddleware` automatically deduplicates rate limit checks when the same `limiter` and `key` combination is used multiple times in a request chain. This behavior follows the [Dedupe Middleware Best Practice](/docs/best-practices/dedupe-middleware). To disable deduplication, set the `dedupe: false` option.
184+
:::
185+
186+
::: tip Conditional Limiter
187+
You can dynamically choose different limiters based on context:
188+
189+
```ts
190+
const premiumLimiter = new MemoryRatelimiter({
191+
maxRequests: 100,
192+
window: 60000,
193+
})
194+
195+
const standardLimiter = new MemoryRatelimiter({
196+
maxRequests: 10,
197+
window: 60000,
198+
})
199+
200+
const result = await call(
201+
loginProcedure,
202+
{ email: 'user@example.com' },
203+
{
204+
context: {
205+
ratelimiter: isPremiumUser ? premiumLimiter : standardLimiter,
206+
},
207+
},
208+
)
209+
```
210+
211+
:::
212+
213+
## Handler Plugin
214+
215+
The `RatelimitHandlerPlugin` automatically adds HTTP rate-limiting headers (`RateLimit-*` and `Retry-After`) to responses when used with middleware created by [`createRatelimitMiddleware`](#createratelimitmiddleware).
216+
217+
```ts
218+
import { RatelimitHandlerPlugin } from '@orpc/experimental-ratelimit'
219+
220+
const handler = new RPCHandler(router, {
221+
plugins: [
222+
new RatelimitHandlerPlugin(),
223+
],
224+
})
225+
```
226+
227+
::: info
228+
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or other custom handlers.
229+
:::

apps/content/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@orpc/client": "workspace:*",
1919
"@orpc/contract": "workspace:*",
2020
"@orpc/experimental-publisher": "workspace:*",
21+
"@orpc/experimental-ratelimit": "workspace:*",
2122
"@orpc/experimental-react-swr": "workspace:*",
2223
"@orpc/openapi": "workspace:*",
2324
"@orpc/openapi-client": "workspace:*",

packages/ratelimit/.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/ratelimit/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<div align="center">
2+
<image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" />
3+
</div>
4+
5+
<h1></h1>
6+
7+
<div align="center">
8+
<a href="https://codecov.io/gh/unnoq/orpc">
9+
<img alt="codecov" src="https://codecov.io/gh/unnoq/orpc/branch/main/graph/badge.svg">
10+
</a>
11+
<a href="https://www.npmjs.com/package/@orpc/experimental-ratelimit">
12+
<img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40orpc%2Fexperimental-ratelimit?logo=npm" />
13+
</a>
14+
<a href="https://github.com/unnoq/orpc/blob/main/LICENSE">
15+
<img alt="MIT License" src="https://img.shields.io/github/license/unnoq/orpc?logo=open-source-initiative" />
16+
</a>
17+
<a href="https://discord.gg/TXEbwRBvQn">
18+
<img alt="Discord" src="https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff" />
19+
</a>
20+
<a href="https://deepwiki.com/unnoq/orpc">
21+
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
22+
</a>
23+
</div>
24+
25+
<h3 align="center">Typesafe APIs Made Simple 🪄</h3>
26+
27+
**oRPC is a powerful combination of RPC and OpenAPI**, makes it easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards
28+
29+
---
30+
31+
## Highlights
32+
33+
- **🔗 End-to-End Type Safety**: Ensure type-safe inputs, outputs, and errors from client to server.
34+
- **📘 First-Class OpenAPI**: Built-in support that fully adheres to the OpenAPI standard.
35+
- **📝 Contract-First Development**: Optionally define your API contract before implementation.
36+
- **🔍 First-Class OpenTelemetry**: Seamlessly integrate with OpenTelemetry for observability.
37+
- **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte, Angular), SWR, Pinia Colada, and more.
38+
- **🚀 Server Actions**: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms.
39+
- **🔠 Standard Schema Support**: Works out of the box with Zod, Valibot, ArkType, and other schema validators.
40+
- **🗃️ Native Types**: Supports native types like Date, File, Blob, BigInt, URL, and more.
41+
- **⏱️ Lazy Router**: Enhance cold start times with our lazy routing feature.
42+
- **📡 SSE & Streaming**: Enjoy full type-safe support for SSE and streaming.
43+
- **🌍 Multi-Runtime Support**: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond.
44+
- **🔌 Extendability**: Easily extend functionality with plugins, middleware, and interceptors.
45+
46+
## Documentation
47+
48+
You can find the full documentation [here](https://orpc.unnoq.com).
49+
50+
## Packages
51+
52+
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
53+
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
54+
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
55+
- [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Generate OpenAPI specs and handle OpenAPI requests.
56+
- [@orpc/otel](https://www.npmjs.com/package/@orpc/otel): [OpenTelemetry](https://opentelemetry.io/) integration for observability.
57+
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with [NestJS](https://nestjs.com/).
58+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
59+
- [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): [TanStack Query](https://tanstack.com/query/latest) integration.
60+
- [@orpc/experimental-react-swr](https://www.npmjs.com/package/@orpc/experimental-react-swr): [SWR](https://swr.vercel.app/) integration.
61+
- [@orpc/vue-colada](https://www.npmjs.com/package/@orpc/vue-colada): Integration with [Pinia Colada](https://pinia-colada.esm.dev/).
62+
- [@orpc/hey-api](https://www.npmjs.com/package/@orpc/hey-api): [Hey API](https://heyapi.dev/) integration.
63+
- [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): More schemas that [Zod](https://zod.dev/) doesn't support yet.
64+
- [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): OpenAPI spec generation from [Valibot](https://valibot.dev/).
65+
- [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): OpenAPI spec generation from [ArkType](https://arktype.io/).
66+
67+
## `@orpc/experimental-ratelimit`
68+
69+
Rate Limiting Feature for oRPC
70+
71+
## Sponsors
72+
73+
<p align="center">
74+
<a href="https://cdn.jsdelivr.net/gh/unnoq/unnoq/sponsors.svg">
75+
<img src='https://cdn.jsdelivr.net/gh/unnoq/unnoq/sponsors.svg'/>
76+
</a>
77+
</p>
78+
79+
## License
80+
81+
Distributed under the MIT License. See [LICENSE](https://github.com/unnoq/orpc/blob/main/LICENSE) for more information.

0 commit comments

Comments
 (0)