Skip to content

Commit

Permalink
Add new example for rate limiting API routes.
Browse files Browse the repository at this point in the history
  • Loading branch information
leerob committed Nov 25, 2020
1 parent 86a0c7b commit 2bd58c9
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 0 deletions.
34 changes: 34 additions & 0 deletions examples/api-routes-rate-limit/.gitignore
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions examples/api-routes-rate-limit/README.md
Original file line number Diff line number Diff line change
@@ -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)).
17 changes: 17 additions & 0 deletions examples/api-routes-rate-limit/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
16 changes: 16 additions & 0 deletions examples/api-routes-rate-limit/pages/api/user.js
Original file line number Diff line number Diff line change
@@ -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' })
}
}
52 changes: 52 additions & 0 deletions examples/api-routes-rate-limit/pages/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<main className={styles.container}>
<h1>Next.js API Routes Rate Limiting</h1>
<p>
This example uses <code className={styles.inlineCode}>lru-cache</code>{' '}
to implement a simple rate limiter for API routes (Serverless
Functions).
</p>
<button onClick={() => makeRequest()}>Make Request</button>
<code className={styles.code}>
<div>
<b>Status Code: </b>
{response?.status || 'None'}
</div>
<div>
<b>Request Limit: </b>
{response?.limit || 'None'}
</div>
<div>
<b>Remaining Requests: </b>
{response?.remaining || 'None'}
</div>
<div>
<b>Body: </b>
{JSON.stringify(response?.body) || 'None'}
</div>
</code>
<div className={styles.links}>
<a href="#">View Source</a>
{' | '}
<a href="#">Deploy You Own ▲</a>
</div>
</main>
)
}
67 changes: 67 additions & 0 deletions examples/api-routes-rate-limit/styles.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
31 changes: 31 additions & 0 deletions examples/api-routes-rate-limit/utils/rate-limit.js
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 2bd58c9

Please sign in to comment.