@upstash/ratelimit 0.4.3 → 0.4.5-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.github/actions/redis/action.yaml +58 -0
  2. package/.github/img/dashboard.png +0 -0
  3. package/.github/workflows/release.yml +46 -0
  4. package/.github/workflows/stale.yaml +31 -0
  5. package/.github/workflows/tests.yaml +79 -0
  6. package/README.md +10 -3
  7. package/biome.json +37 -0
  8. package/bun.lockb +0 -0
  9. package/cmd/set-version.js +14 -0
  10. package/examples/cloudflare-workers/package.json +18 -0
  11. package/examples/cloudflare-workers/src/index.ts +35 -0
  12. package/examples/cloudflare-workers/tsconfig.json +105 -0
  13. package/examples/cloudflare-workers/wrangler.toml +3 -0
  14. package/examples/nextjs/LICENSE +21 -0
  15. package/examples/nextjs/README.md +17 -0
  16. package/examples/nextjs/components/Breadcrumb.tsx +67 -0
  17. package/examples/nextjs/components/Header.tsx +18 -0
  18. package/examples/nextjs/components/ReadBlogPost.tsx +9 -0
  19. package/examples/nextjs/components/StarButton.tsx +27 -0
  20. package/examples/nextjs/middleware.ts +35 -0
  21. package/examples/nextjs/next-env.d.ts +5 -0
  22. package/examples/nextjs/package.json +27 -0
  23. package/examples/nextjs/pages/_app.tsx +47 -0
  24. package/examples/nextjs/pages/api/blocked.ts +6 -0
  25. package/examples/nextjs/pages/api/hello.ts +5 -0
  26. package/examples/nextjs/pages/index.tsx +62 -0
  27. package/examples/nextjs/postcss.config.js +6 -0
  28. package/examples/nextjs/public/favicon.ico +0 -0
  29. package/examples/nextjs/public/github.svg +11 -0
  30. package/examples/nextjs/public/upstash.svg +27 -0
  31. package/examples/nextjs/styles/globals.css +76 -0
  32. package/examples/nextjs/tailwind.config.js +19 -0
  33. package/examples/nextjs/tsconfig.json +21 -0
  34. package/examples/nextjs13/README.md +38 -0
  35. package/examples/nextjs13/app/favicon.ico +0 -0
  36. package/examples/nextjs13/app/globals.css +107 -0
  37. package/examples/nextjs13/app/layout.tsx +18 -0
  38. package/examples/nextjs13/app/page.module.css +271 -0
  39. package/examples/nextjs13/app/route.tsx +14 -0
  40. package/examples/nextjs13/next.config.js +8 -0
  41. package/examples/nextjs13/package.json +22 -0
  42. package/examples/nextjs13/public/next.svg +1 -0
  43. package/examples/nextjs13/public/thirteen.svg +1 -0
  44. package/examples/nextjs13/public/vercel.svg +1 -0
  45. package/examples/nextjs13/tsconfig.json +28 -0
  46. package/examples/remix/.env.example +2 -0
  47. package/examples/remix/.eslintrc.js +4 -0
  48. package/examples/remix/README.md +59 -0
  49. package/examples/remix/app/root.tsx +25 -0
  50. package/examples/remix/app/routes/index.tsx +47 -0
  51. package/examples/remix/package.json +32 -0
  52. package/examples/remix/public/favicon.ico +0 -0
  53. package/examples/remix/remix.config.js +12 -0
  54. package/examples/remix/remix.env.d.ts +2 -0
  55. package/examples/remix/server.js +4 -0
  56. package/examples/remix/tsconfig.json +22 -0
  57. package/examples/with-vercel-kv/README.md +51 -0
  58. package/examples/with-vercel-kv/app/favicon.ico +0 -0
  59. package/examples/with-vercel-kv/app/globals.css +27 -0
  60. package/examples/with-vercel-kv/app/layout.tsx +21 -0
  61. package/examples/with-vercel-kv/app/page.tsx +71 -0
  62. package/examples/with-vercel-kv/next.config.js +8 -0
  63. package/examples/with-vercel-kv/package.json +25 -0
  64. package/examples/with-vercel-kv/postcss.config.js +6 -0
  65. package/examples/with-vercel-kv/public/next.svg +1 -0
  66. package/examples/with-vercel-kv/public/vercel.svg +1 -0
  67. package/examples/with-vercel-kv/tailwind.config.js +17 -0
  68. package/examples/with-vercel-kv/tsconfig.json +28 -0
  69. package/package.json +12 -33
  70. package/src/analytics.test.ts +23 -0
  71. package/src/analytics.ts +92 -0
  72. package/src/blockUntilReady.test.ts +56 -0
  73. package/src/cache.test.ts +41 -0
  74. package/src/cache.ts +43 -0
  75. package/src/duration.test.ts +23 -0
  76. package/src/duration.ts +30 -0
  77. package/src/index.ts +17 -0
  78. package/src/multi.ts +365 -0
  79. package/src/ratelimit.test.ts +155 -0
  80. package/src/ratelimit.ts +238 -0
  81. package/src/single.ts +487 -0
  82. package/src/test_utils.ts +65 -0
  83. package/src/tools/seed.ts +37 -0
  84. package/src/types.ts +78 -0
  85. package/src/version.ts +1 -0
  86. package/tsconfig.json +103 -0
  87. package/tsup.config.js +11 -0
  88. package/turbo.json +16 -0
  89. package/dist/index.d.ts +0 -558
  90. package/dist/index.js +0 -828
  91. package/dist/index.js.map +0 -1
  92. package/dist/index.mjs +0 -799
  93. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,271 @@
1
+ .main {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+ padding: 6rem;
7
+ min-height: 100vh;
8
+ }
9
+
10
+ .description {
11
+ display: inherit;
12
+ justify-content: inherit;
13
+ align-items: inherit;
14
+ font-size: 0.85rem;
15
+ max-width: var(--max-width);
16
+ width: 100%;
17
+ z-index: 2;
18
+ font-family: var(--font-mono);
19
+ }
20
+
21
+ .description a {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ gap: 0.5rem;
26
+ }
27
+
28
+ .description p {
29
+ position: relative;
30
+ margin: 0;
31
+ padding: 1rem;
32
+ background-color: rgba(var(--callout-rgb), 0.5);
33
+ border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34
+ border-radius: var(--border-radius);
35
+ }
36
+
37
+ .code {
38
+ font-weight: 700;
39
+ font-family: var(--font-mono);
40
+ }
41
+
42
+ .grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(3, minmax(33%, auto));
45
+ width: var(--max-width);
46
+ max-width: 100%;
47
+ }
48
+
49
+ .card {
50
+ padding: 1rem 1.2rem;
51
+ border-radius: var(--border-radius);
52
+ background: rgba(var(--card-rgb), 0);
53
+ border: 1px solid rgba(var(--card-border-rgb), 0);
54
+ transition: background 200ms, border 200ms;
55
+ }
56
+
57
+ .card span {
58
+ display: inline-block;
59
+ transition: transform 200ms;
60
+ }
61
+
62
+ .card h2 {
63
+ font-weight: 600;
64
+ margin-bottom: 0.7rem;
65
+ }
66
+
67
+ .card p {
68
+ margin: 0;
69
+ opacity: 0.6;
70
+ font-size: 0.9rem;
71
+ line-height: 1.5;
72
+ max-width: 34ch;
73
+ }
74
+
75
+ .center {
76
+ display: flex;
77
+ justify-content: center;
78
+ align-items: center;
79
+ position: relative;
80
+ padding: 4rem 0;
81
+ }
82
+
83
+ .center::before {
84
+ background: var(--secondary-glow);
85
+ border-radius: 50%;
86
+ width: 480px;
87
+ height: 360px;
88
+ margin-left: -400px;
89
+ }
90
+
91
+ .center::after {
92
+ background: var(--primary-glow);
93
+ width: 240px;
94
+ height: 180px;
95
+ z-index: -1;
96
+ }
97
+
98
+ .center::before,
99
+ .center::after {
100
+ content: '';
101
+ left: 50%;
102
+ position: absolute;
103
+ filter: blur(45px);
104
+ transform: translateZ(0);
105
+ }
106
+
107
+ .logo,
108
+ .thirteen {
109
+ position: relative;
110
+ }
111
+
112
+ .thirteen {
113
+ display: flex;
114
+ justify-content: center;
115
+ align-items: center;
116
+ width: 75px;
117
+ height: 75px;
118
+ padding: 25px 10px;
119
+ margin-left: 16px;
120
+ transform: translateZ(0);
121
+ border-radius: var(--border-radius);
122
+ overflow: hidden;
123
+ box-shadow: 0px 2px 8px -1px #0000001a;
124
+ }
125
+
126
+ .thirteen::before,
127
+ .thirteen::after {
128
+ content: '';
129
+ position: absolute;
130
+ z-index: -1;
131
+ }
132
+
133
+ /* Conic Gradient Animation */
134
+ .thirteen::before {
135
+ animation: 6s rotate linear infinite;
136
+ width: 200%;
137
+ height: 200%;
138
+ background: var(--tile-border);
139
+ }
140
+
141
+ /* Inner Square */
142
+ .thirteen::after {
143
+ inset: 0;
144
+ padding: 1px;
145
+ border-radius: var(--border-radius);
146
+ background: linear-gradient(
147
+ to bottom right,
148
+ rgba(var(--tile-start-rgb), 1),
149
+ rgba(var(--tile-end-rgb), 1)
150
+ );
151
+ background-clip: content-box;
152
+ }
153
+
154
+ /* Enable hover only on non-touch devices */
155
+ @media (hover: hover) and (pointer: fine) {
156
+ .card:hover {
157
+ background: rgba(var(--card-rgb), 0.1);
158
+ border: 1px solid rgba(var(--card-border-rgb), 0.15);
159
+ }
160
+
161
+ .card:hover span {
162
+ transform: translateX(4px);
163
+ }
164
+ }
165
+
166
+ @media (prefers-reduced-motion) {
167
+ .thirteen::before {
168
+ animation: none;
169
+ }
170
+
171
+ .card:hover span {
172
+ transform: none;
173
+ }
174
+ }
175
+
176
+ /* Mobile and Tablet */
177
+ @media (max-width: 1023px) {
178
+ .content {
179
+ padding: 4rem;
180
+ }
181
+
182
+ .grid {
183
+ grid-template-columns: 1fr;
184
+ margin-bottom: 120px;
185
+ max-width: 320px;
186
+ text-align: center;
187
+ }
188
+
189
+ .card {
190
+ padding: 1rem 2.5rem;
191
+ }
192
+
193
+ .card h2 {
194
+ margin-bottom: 0.5rem;
195
+ }
196
+
197
+ .center {
198
+ padding: 8rem 0 6rem;
199
+ }
200
+
201
+ .center::before {
202
+ transform: none;
203
+ height: 300px;
204
+ }
205
+
206
+ .description {
207
+ font-size: 0.8rem;
208
+ }
209
+
210
+ .description a {
211
+ padding: 1rem;
212
+ }
213
+
214
+ .description p,
215
+ .description div {
216
+ display: flex;
217
+ justify-content: center;
218
+ position: fixed;
219
+ width: 100%;
220
+ }
221
+
222
+ .description p {
223
+ align-items: center;
224
+ inset: 0 0 auto;
225
+ padding: 2rem 1rem 1.4rem;
226
+ border-radius: 0;
227
+ border: none;
228
+ border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
229
+ background: linear-gradient(
230
+ to bottom,
231
+ rgba(var(--background-start-rgb), 1),
232
+ rgba(var(--callout-rgb), 0.5)
233
+ );
234
+ background-clip: padding-box;
235
+ backdrop-filter: blur(24px);
236
+ }
237
+
238
+ .description div {
239
+ align-items: flex-end;
240
+ pointer-events: none;
241
+ inset: auto 0 0;
242
+ padding: 2rem;
243
+ height: 200px;
244
+ background: linear-gradient(
245
+ to bottom,
246
+ transparent 0%,
247
+ rgb(var(--background-end-rgb)) 40%
248
+ );
249
+ z-index: 1;
250
+ }
251
+ }
252
+
253
+ @media (prefers-color-scheme: dark) {
254
+ .vercelLogo {
255
+ filter: invert(1);
256
+ }
257
+
258
+ .logo,
259
+ .thirteen img {
260
+ filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
261
+ }
262
+ }
263
+
264
+ @keyframes rotate {
265
+ from {
266
+ transform: rotate(360deg);
267
+ }
268
+ to {
269
+ transform: rotate(0deg);
270
+ }
271
+ }
@@ -0,0 +1,14 @@
1
+ import { Ratelimit } from "@upstash/ratelimit";
2
+ import { Redis } from "@upstash/redis";
3
+ import { NextRequest, NextResponse } from "next/server";
4
+
5
+ const ratelimit = new Ratelimit({
6
+ redis: Redis.fromEnv(),
7
+ analytics: true,
8
+ limiter: Ratelimit.slidingWindow(2, "5s"),
9
+ });
10
+ export async function GET(req: NextRequest) {
11
+ const id = req.ip ?? "anonymous";
12
+ const limit = await ratelimit.limit(id ?? "anonymous");
13
+ return NextResponse.json(limit, { status: limit.success ? 200 : 429 });
14
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ experimental: {
4
+ appDir: true,
5
+ },
6
+ };
7
+
8
+ module.exports = nextConfig;
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "nextjs13",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@types/node": "20.1.2",
13
+ "@types/react": "18.2.6",
14
+ "@types/react-dom": "18.2.4",
15
+ "@upstash/ratelimit": "workspace:*",
16
+ "@upstash/redis": "^1.20.6",
17
+ "next": "13.4.1",
18
+ "react": "18.2.0",
19
+ "react-dom": "18.2.0",
20
+ "typescript": "5.0.4"
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "preserve",
16
+ "incremental": true,
17
+ "plugins": [
18
+ {
19
+ "name": "next"
20
+ }
21
+ ],
22
+ "paths": {
23
+ "@/*": ["./*"]
24
+ }
25
+ },
26
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27
+ "exclude": ["node_modules"]
28
+ }
@@ -0,0 +1,2 @@
1
+ UPSTASH_REDIS_REST_URL=
2
+ UPSTASH_REDIS_REST_TOKEN=
@@ -0,0 +1,4 @@
1
+ /** @type {import('eslint').Linter.Config} */
2
+ module.exports = {
3
+ extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4
+ };
@@ -0,0 +1,59 @@
1
+ # @upstash/ratelimit in Remix
2
+
3
+ This example shows how to use `@upstash/ratelimit` in a Remix app.
4
+
5
+ ## Getting Started
6
+
7
+ Create a database on [Upstash](https://console.upstash.com/redis?new=true) and copy the `Upstash_REDIS_REST_URL` and `Upstash_REDIS_REST_TOKEN` from the database settings to your `.env` file.
8
+
9
+ Then add a `loader` to your route like this:
10
+
11
+ ```tsx
12
+ import { json } from "@remix-run/node";
13
+ import type { LoaderArgs } from "@remix-run/node";
14
+ import { useLoaderData } from "@remix-run/react";
15
+ import { Ratelimit } from "@upstash/ratelimit";
16
+ import { Redis } from "@upstash/redis";
17
+
18
+ const ratelimit = new Ratelimit({
19
+ redis: Redis.fromEnv(),
20
+ limiter: Ratelimit.fixedWindow(10, "60 s"),
21
+ analytics: true,
22
+ });
23
+
24
+ export const loader = async (args: LoaderArgs) => {
25
+ // getting the ip can be different depending on your hosting provider
26
+ const ip = args.request.headers.get("X-Forwarded-For") ?? args.request.headers.get("x-real-ip");
27
+ const identifier = ip ?? "global";
28
+ const { success, limit, remaining, reset } = await ratelimit.limit(identifier);
29
+ return json(
30
+ {
31
+ success,
32
+ limit,
33
+ remaining,
34
+ reset,
35
+ identifier,
36
+ },
37
+ {
38
+ headers: {
39
+ "X-RateLimit-Limit": limit.toString(),
40
+ "X-RateLimit-Remaining": remaining.toString(),
41
+ "X-RateLimit-Reset": reset.toString(),
42
+ },
43
+ },
44
+ );
45
+ };
46
+
47
+ export default function Index() {
48
+ const ratelimitResponse = useLoaderData<typeof loader>();
49
+
50
+ return (
51
+ <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
52
+ <h1>Welcome to @upstash/ratelimit in Remix app</h1>
53
+ <code>
54
+ <pre>{JSON.stringify(ratelimitResponse, null, 2)}</pre>
55
+ </code>
56
+ </div>
57
+ );
58
+ }
59
+ ```
@@ -0,0 +1,25 @@
1
+ import type { MetaFunction } from "@remix-run/node";
2
+ import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
3
+
4
+ export const meta: MetaFunction = () => ({
5
+ charset: "utf-8",
6
+ title: "New Remix App",
7
+ viewport: "width=device-width,initial-scale=1",
8
+ });
9
+
10
+ export default function App() {
11
+ return (
12
+ <html lang="en">
13
+ <head>
14
+ <Meta />
15
+ <Links />
16
+ </head>
17
+ <body>
18
+ <Outlet />
19
+ <ScrollRestoration />
20
+ <Scripts />
21
+ <LiveReload />
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,47 @@
1
+ import { json } from "@remix-run/node";
2
+ import type { LoaderArgs } from "@remix-run/node";
3
+ import { useLoaderData } from "@remix-run/react";
4
+ import { Ratelimit } from "@upstash/ratelimit";
5
+ import { Redis } from "@upstash/redis";
6
+
7
+ const ratelimit = new Ratelimit({
8
+ redis: Redis.fromEnv(),
9
+ limiter: Ratelimit.fixedWindow(10, "60 s"),
10
+ analytics: true,
11
+ });
12
+
13
+ export const loader = async (args: LoaderArgs) => {
14
+ // getting the ip can be different depending on your hosting provider
15
+ const ip = args.request.headers.get("X-Forwarded-For") ?? args.request.headers.get("x-real-ip");
16
+ const identifier = ip ?? "global";
17
+ const { success, limit, remaining, reset } = await ratelimit.limit(identifier);
18
+ return json(
19
+ {
20
+ success,
21
+ limit,
22
+ remaining,
23
+ reset,
24
+ identifier,
25
+ },
26
+ {
27
+ headers: {
28
+ "X-RateLimit-Limit": limit.toString(),
29
+ "X-RateLimit-Remaining": remaining.toString(),
30
+ "X-RateLimit-Reset": reset.toString(),
31
+ },
32
+ },
33
+ );
34
+ };
35
+
36
+ export default function Index() {
37
+ const ratelimitResponse = useLoaderData<typeof loader>();
38
+
39
+ return (
40
+ <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
41
+ <h1>Welcome to @upstash/ratelimit in Remix app</h1>
42
+ <code>
43
+ <pre>{JSON.stringify(ratelimitResponse, null, 2)}</pre>
44
+ </code>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "private": true,
3
+ "sideEffects": false,
4
+ "scripts": {
5
+ "build": "remix build",
6
+ "dev": "remix dev",
7
+ "typecheck": "tsc"
8
+ },
9
+ "dependencies": {
10
+ "@remix-run/node": "^1.16.0",
11
+ "@remix-run/react": "^1.16.0",
12
+ "@remix-run/vercel": "^1.16.0",
13
+ "@upstash/ratelimit": "^0.4.3",
14
+ "@upstash/redis": "^1.20.6",
15
+ "@vercel/node": "^2.14.2",
16
+ "isbot": "^3.6.10",
17
+ "react": "^18.2.0",
18
+ "react-dom": "^18.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "@remix-run/dev": "^1.16.0",
22
+ "@remix-run/eslint-config": "^1.16.0",
23
+ "@remix-run/serve": "^1.16.0",
24
+ "@types/react": "^18.2.6",
25
+ "@types/react-dom": "^18.2.4",
26
+ "eslint": "^8.40.0",
27
+ "typescript": "^5.0.4"
28
+ },
29
+ "engines": {
30
+ "node": ">=14"
31
+ }
32
+ }
@@ -0,0 +1,12 @@
1
+ /** @type {import('@remix-run/dev').AppConfig} */
2
+ module.exports = {
3
+ ignoredRouteFiles: ["**/.*"],
4
+ // When running locally in development mode, we use the built-in remix
5
+ // server. This does not understand the vercel lambda module format,
6
+ // so we default back to the standard build output.
7
+ server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
8
+ serverBuildPath: "api/index.js",
9
+ // appDirectory: "app",
10
+ // assetsBuildDirectory: "public/build",
11
+ // publicPath: "/build/",
12
+ };
@@ -0,0 +1,2 @@
1
+ /// <reference types="@remix-run/dev" />
2
+ /// <reference types="@remix-run/node" />
@@ -0,0 +1,4 @@
1
+ import * as build from "@remix-run/dev/server-build";
2
+ import { createRequestHandler } from "@remix-run/vercel";
3
+
4
+ export default createRequestHandler({ build, mode: process.env.NODE_ENV });
@@ -0,0 +1,22 @@
1
+ {
2
+ "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "lib": ["DOM", "DOM.Iterable", "ES2019"],
5
+ "isolatedModules": true,
6
+ "esModuleInterop": true,
7
+ "jsx": "react-jsx",
8
+ "moduleResolution": "node",
9
+ "resolveJsonModule": true,
10
+ "target": "ES2019",
11
+ "strict": true,
12
+ "allowJs": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "~/*": ["./app/*"]
17
+ },
18
+
19
+ // Remix takes care of building everything in `remix build`.
20
+ "noEmit": true
21
+ }
22
+ }
@@ -0,0 +1,51 @@
1
+ This is a [Next.js](https://nextjs.org/) project bootstrapped with
2
+ [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
3
+
4
+ ## Getting Started
5
+
6
+ First, run the development server:
7
+
8
+ ```bash
9
+ npm run dev
10
+ # or
11
+ yarn dev
12
+ # or
13
+ pnpm dev
14
+ ```
15
+
16
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
17
+ result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page
20
+ auto-updates as you edit the file.
21
+
22
+ [http://localhost:3000/api/hello](http://localhost:3000/api/hello) is an
23
+ endpoint that uses
24
+ [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers). This
25
+ endpoint can be edited in `app/api/hello/route.ts`.
26
+
27
+ This project uses
28
+ [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to
29
+ automatically optimize and load Inter, a custom Google Font.
30
+
31
+ ## Learn More
32
+
33
+ To learn more about Next.js, take a look at the following resources:
34
+
35
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
36
+ features and API.
37
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
38
+
39
+ You can check out
40
+ [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your
41
+ feedback and contributions are welcome!
42
+
43
+ ## Deploy on Vercel
44
+
45
+ The easiest way to deploy your Next.js app is to use the
46
+ [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
47
+ from the creators of Next.js.
48
+
49
+ Check out our
50
+ [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more
51
+ details.
@@ -0,0 +1,27 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --foreground-rgb: 0, 0, 0;
7
+ --background-start-rgb: 214, 219, 220;
8
+ --background-end-rgb: 255, 255, 255;
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :root {
13
+ --foreground-rgb: 255, 255, 255;
14
+ --background-start-rgb: 0, 0, 0;
15
+ --background-end-rgb: 0, 0, 0;
16
+ }
17
+ }
18
+
19
+ body {
20
+ color: rgb(var(--foreground-rgb));
21
+ background: linear-gradient(
22
+ to bottom,
23
+ transparent,
24
+ rgb(var(--background-end-rgb))
25
+ )
26
+ rgb(var(--background-start-rgb));
27
+ }
@@ -0,0 +1,21 @@
1
+ import { Inter } from "next/font/google";
2
+ import "./globals.css";
3
+
4
+ const inter = Inter({ subsets: ["latin"] });
5
+
6
+ export const metadata = {
7
+ title: "Create Next App",
8
+ description: "Generated by create next app",
9
+ };
10
+
11
+ export default function RootLayout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode;
15
+ }) {
16
+ return (
17
+ <html lang="en">
18
+ <body className={inter.className}>{children}</body>
19
+ </html>
20
+ );
21
+ }