Limitly
Guides

Next.js Integration

Integrate rate limiting into your Next.js API routes. Works with both App Router and Pages Router.

Next.js Integration

App Router

// app/api/route.ts
import { createClient } from 'limitly-sdk';
import { NextResponse } from 'next/server';

const client = createClient({ serviceId: 'nextjs-api' });

export async function GET(request: Request) {
  const userId = request.headers.get('x-user-id') || 'anonymous';
  const result = await client.checkRateLimit(userId);
  
  if (!result.allowed) {
    return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 });
  }
  
  return NextResponse.json({ success: true });
}

Reusable Helper

// lib/rate-limit.ts
import { createClient } from 'limitly-sdk';

// Recommended: Use your own Redis
const client = createClient({
  redisUrl: process.env.REDIS_URL,
  serviceId: 'nextjs-api'
});

export async function withRateLimit(request: Request, handler: Function) {
  const userId = request.headers.get('x-user-id') || 'anonymous';
  const result = await client.checkRateLimit(userId);
  
  if (!result.allowed) {
    return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 });
  }
  
  return handler(request);
}

Usage:

export async function GET(request: Request) {
  return withRateLimit(request, async () => {
    return NextResponse.json({ data: 'Protected data' });
  });
}

Per-Route Limits

// lib/rate-limit.ts
import { createClient } from 'limitly-sdk';
import { NextResponse } from 'next/server';

const client = createClient({ serviceId: 'nextjs-api' });

export async function checkRouteLimit(
  request: Request,
  route: string,
  capacity: number,
  refillRate: number
): Promise<NextResponse | null> {
  const userId = request.headers.get('x-user-id') || 
                 request.headers.get('x-forwarded-for')?.split(',')[0] || 
                 'anonymous';
  
  const result = await client.checkRateLimit({
    identifier: `${userId}:${route}`,
    capacity,
    refillRate
  });
  
  const headers = new Headers();
  if (result.limit) headers.set('X-RateLimit-Limit', result.limit.toString());
  if (result.remaining !== undefined) {
    headers.set('X-RateLimit-Remaining', result.remaining.toString());
  }
  
  if (!result.allowed) {
    const retryAfter = result.reset 
      ? Math.ceil((result.reset - Date.now()) / 1000) 
      : 60;
    headers.set('Retry-After', retryAfter.toString());
    
    return NextResponse.json(
      { error: 'Rate limit exceeded', retryAfter },
      { status: 429, headers }
    );
  }
  
  return null; // Rate limit passed
}

Use in routes:

// app/api/login/route.ts
import { checkRouteLimit } from '@/lib/rate-limit';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  // Strict limits for login
  const rateLimitResponse = await checkRouteLimit(request, '/api/login', 5, 0.1);
  if (rateLimitResponse) return rateLimitResponse;
  
  // Login logic
  return NextResponse.json({ success: true });
}

Pages Router

// pages/api/route.ts
import { createClient } from 'limitly-sdk';

const client = createClient({ serviceId: 'nextjs-api' });

export default async function handler(req, res) {
  const userId = req.headers['x-user-id'] || 'anonymous';
  const result = await client.checkRateLimit(userId);
  
  if (!result.allowed) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  
  res.status(200).json({ success: true });
}

Server Actions

'use server';
import { createClient } from 'limitly-sdk';
import { headers } from 'next/headers';

const client = createClient({ serviceId: 'server-actions' });

export async function protectedAction(data: FormData) {
  const userId = headers().get('x-user-id') || 'anonymous';
  const result = await client.checkRateLimit(userId);
  
  if (!result.allowed) throw new Error('Rate limit exceeded');
  return { success: true };
}

Best Practices

  • Use environment variables for configuration
  • Handle errors gracefully
  • Use authenticated user IDs when available