From 5678d24f56e84efe8f110ae58608aa4230e3b765 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Mon, 4 Jan 2021 16:19:31 -0600 Subject: [PATCH] Add new example for rate limiting API routes. (#19509) Adds an example using `lru-cache` to implement a simple rate limiter for API routes ([Serverless Functions](https://vercel.com/docs/serverless-functions/introduction)). **Demo: https://nextjs-rate-limit.vercel.app/** --- examples/api-routes-rate-limit/.gitignore | 34 ++++++++++ examples/api-routes-rate-limit/README.md | 35 ++++++++++ examples/api-routes-rate-limit/package.json | 17 +++++ .../api-routes-rate-limit/pages/api/user.js | 16 +++++ examples/api-routes-rate-limit/pages/index.js | 52 ++++++++++++++ .../api-routes-rate-limit/styles.module.css | 67 +++++++++++++++++++ .../api-routes-rate-limit/utils/rate-limit.js | 31 +++++++++ 7 files changed, 252 insertions(+) create mode 100644 examples/api-routes-rate-limit/.gitignore create mode 100644 examples/api-routes-rate-limit/README.md create mode 100644 examples/api-routes-rate-limit/package.json create mode 100644 examples/api-routes-rate-limit/pages/api/user.js create mode 100644 examples/api-routes-rate-limit/pages/index.js create mode 100644 examples/api-routes-rate-limit/styles.module.css create mode 100644 examples/api-routes-rate-limit/utils/rate-limit.js diff --git a/examples/api-routes-rate-limit/.gitignore b/examples/api-routes-rate-limit/.gitignore new file mode 100644 index 0000000000000..1437c53f70bc2 --- /dev/null +++ b/examples/api-routes-rate-limit/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/examples/api-routes-rate-limit/README.md b/examples/api-routes-rate-limit/README.md new file mode 100644 index 0000000000000..381b6e93e3691 --- /dev/null +++ b/examples/api-routes-rate-limit/README.md @@ -0,0 +1,35 @@ +# API Routes Rate Limiting Example + +This example uses `lru-cache` to implement a simple rate limiter for API routes ([Serverless Functions](https://vercel.com/docs/serverless-functions/introduction)). + +**Demo: https://nextjs-rate-limit.vercel.app/** + +```bash +curl http://localhost:3000/api/user -I +HTTP/1.1 200 OK +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 9 + +curl http://localhost:3000/api/user -I +HTTP/1.1 429 Too Many Requests +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 0 +``` + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/api-routes-rate-limit) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example api-routes api-routes-rate-limit +# or +yarn create next-app --example api-routes api-routes-rate-limit +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/api-routes-rate-limit/package.json b/examples/api-routes-rate-limit/package.json new file mode 100644 index 0000000000000..b43827f60cb63 --- /dev/null +++ b/examples/api-routes-rate-limit/package.json @@ -0,0 +1,17 @@ +{ + "name": "nextjs-rate-limit", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "lru-cache": "^6.0.0", + "next": "10.0.3", + "react": "17.0.1", + "react-dom": "17.0.1", + "uuid": "^8.3.1" + } +} diff --git a/examples/api-routes-rate-limit/pages/api/user.js b/examples/api-routes-rate-limit/pages/api/user.js new file mode 100644 index 0000000000000..2380dad3577f3 --- /dev/null +++ b/examples/api-routes-rate-limit/pages/api/user.js @@ -0,0 +1,16 @@ +import * as uuid from 'uuid' +import rateLimit from '../../utils/rate-limit' + +const limiter = rateLimit({ + interval: 60 * 1000, // 60 seconds + uniqueTokenPerInterval: 500, // Max 500 users per second +}) + +export default async function handler(req, res) { + try { + await limiter.check(res, 10, 'CACHE_TOKEN') // 10 requests per minute + res.status(200).json({ id: uuid.v4() }) + } catch { + res.status(429).json({ error: 'Rate limit exceeded' }) + } +} diff --git a/examples/api-routes-rate-limit/pages/index.js b/examples/api-routes-rate-limit/pages/index.js new file mode 100644 index 0000000000000..3c5cfc8564998 --- /dev/null +++ b/examples/api-routes-rate-limit/pages/index.js @@ -0,0 +1,52 @@ +import { useState } from 'react' +import styles from '../styles.module.css' + +export default function Index() { + const [response, setResponse] = useState() + + const makeRequest = async () => { + const res = await fetch('/api/user') + + setResponse({ + status: res.status, + body: await res.json(), + limit: res.headers.get('X-RateLimit-Limit'), + remaining: res.headers.get('X-RateLimit-Remaining'), + }) + } + + return ( +
+

Next.js API Routes Rate Limiting

+

+ This example uses lru-cache{' '} + to implement a simple rate limiter for API routes (Serverless + Functions). +

+ + +
+ Status Code: + {response?.status || 'None'} +
+
+ Request Limit: + {response?.limit || 'None'} +
+
+ Remaining Requests: + {response?.remaining || 'None'} +
+
+ Body: + {JSON.stringify(response?.body) || 'None'} +
+
+
+ View Source + {' | '} + Deploy You Own ▲ +
+
+ ) +} diff --git a/examples/api-routes-rate-limit/styles.module.css b/examples/api-routes-rate-limit/styles.module.css new file mode 100644 index 0000000000000..f2cd0c79e7de7 --- /dev/null +++ b/examples/api-routes-rate-limit/styles.module.css @@ -0,0 +1,67 @@ +.container { + padding: 4rem 1rem; + max-width: 50rem; + margin: 0 auto; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container h1 { + font-weight: 800; +} + +.container p { + margin: 1.5rem 0; + line-height: 1.5; +} + +.container button { + border-radius: 4px; + height: 40px; + padding: 0.5rem 1rem; + font-size: 16px; + border: none; + transition: 0.25s all ease; + background-color: #eaeaea; + font-size: 14px; + font-weight: 600; + color: #111; +} + +.container button:hover { + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12); +} + +.container a { + text-decoration: none; + color: #0070f3; +} + +.inlineCode { + color: #be00ff; + font-size: 16px; + white-space: pre-wrap; +} + +.inlineCode::before, +.inlineCode::after { + content: '`'; +} + +.code { + margin-top: 16px; + display: block; + background: #222222; + border-radius: 8px; + padding: 16px; + color: white; + font-size: 16px; + line-height: 1.4; +} + +.links { + margin-top: 16px; + color: #9c9c9c; +} diff --git a/examples/api-routes-rate-limit/utils/rate-limit.js b/examples/api-routes-rate-limit/utils/rate-limit.js new file mode 100644 index 0000000000000..6727949ada679 --- /dev/null +++ b/examples/api-routes-rate-limit/utils/rate-limit.js @@ -0,0 +1,31 @@ +const LRU = require('lru-cache') + +const rateLimit = (options) => { + const tokenCache = new LRU({ + max: parseInt(options.uniqueTokenPerInterval || 500, 10), + maxAge: parseInt(options.interval || 60000, 10), + }) + + return { + check: (res, limit, token) => + new Promise((resolve, reject) => { + const tokenCount = tokenCache.get(token) || [0] + if (tokenCount[0] === 0) { + tokenCache.set(token, tokenCount) + } + tokenCount[0] += 1 + + const currentUsage = tokenCount[0] + const isRateLimited = currentUsage >= parseInt(limit, 10) + res.setHeader('X-RateLimit-Limit', limit) + res.setHeader( + 'X-RateLimit-Remaining', + isRateLimited ? 0 : limit - currentUsage + ) + + return isRateLimited ? reject() : resolve() + }), + } +} + +export default rateLimit