Skip to content

Commit

Permalink
feat: add middleware for rate limitting
Browse files Browse the repository at this point in the history
  • Loading branch information
ccrsxx committed Nov 15, 2023
1 parent a4c5697 commit 3af1bac
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 2 deletions.
5 changes: 4 additions & 1 deletion src/app/(public)/links/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import { checkIfUrlIsValid } from '@/lib/helper';
import { checkSlugExists, generateRandomSlug } from '@/lib/helper-server';
import { NEXT_PUBLIC_URL } from '@/lib/env';
import { linkSchema } from '@/app/schema';
import type { Link } from '@/app/schema';
import type { APIResponse } from '@/lib/types/api';
import type { LinkMeta } from '@/lib/types/meta';

export async function POST(
request: Request
): Promise<NextResponse<APIResponse<LinkMeta>>> {
try {
const body = (await request.json()) as unknown;
const body = (await request.json().catch(() => null)) as Link | null;

if (!body) throw new Error('Invalid request body');

const { url, slug } = linkSchema.parse(body);

Expand Down
49 changes: 49 additions & 0 deletions src/lib/rate-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { LRUCache } from 'lru-cache';

type Options = {
interval?: number;
uniqueTokenPerInterval?: number;
};

/**
* Rate limit helper.
*
* @param {Options} [options] Options
* @param {number} [options.interval=60000] Interval in milliseconds
* @param {number} [options.uniqueTokenPerInterval=500] Max unique tokens per interval
*/
export function rateLimit({
interval = 60 * 1000,
uniqueTokenPerInterval = 500
}: Options = {}): {
check: (options: {
limit: number;
token: string;
headers: Headers;
}) => Promise<void>;
} {
const tokenCache = new LRUCache({
ttl: interval,
max: uniqueTokenPerInterval
});

return {
check: ({ headers, limit, token }) =>
new Promise((resolve, reject) => {
const tokenCount = (tokenCache.get(token) as number[]) || [0];

if (tokenCount[0] === 0) tokenCache.set(token, tokenCount);

tokenCount[0] += 1;

const currentUsage = tokenCount[0];
const isRateLimited = currentUsage >= limit;
const limitRemaining = isRateLimited ? 0 : limit - currentUsage;

headers.set('X-RateLimit-Limit', limit.toString());
headers.set('X-RateLimit-Remaining', limitRemaining.toString());

return isRateLimited ? reject() : resolve();
})
};
}
2 changes: 1 addition & 1 deletion src/lib/types/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type APIResponse<T = void> = {
data?: T;
message: string;
data?: T;
};
34 changes: 34 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
import { rateLimit } from './lib/rate-limit';

const limiter = rateLimit({
interval: 60 * 1000, // 1 minute
uniqueTokenPerInterval: 500 // Max 500 users per minute
});

export async function middleware(request: Request): Promise<NextResponse> {
const headers = new Headers(request.headers);

try {
await limiter.check({
limit: 10,
token: 'CACHE_TOKEN',
headers: headers
});
} catch {
return NextResponse.json(
{ message: 'Too many requests' },
{ status: 429, headers: headers }
);
}

return NextResponse.next({ headers });
}

type Config = {
matcher: string;
};

export const config: Config = {
matcher: '/links'
};

0 comments on commit 3af1bac

Please sign in to comment.