Limitly
Guides

Express.js Integration

Protect your Express routes with Limitly. Learn how to create middleware, handle errors, and set proper HTTP headers.

Express.js Integration

Basic Middleware

// middleware/rate-limit.ts
import { createClient } from 'limitly-sdk';
const client = createClient({ serviceId: 'express-api' });

export async function rateLimitMiddleware(req, res, next) {
  const identifier = req.user?.id || req.ip || 'anonymous';
  const result = await client.checkRateLimit(identifier);
  
  if (!result.allowed) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  next();
}

Usage

// app.ts
import { rateLimitMiddleware } from './middleware/rate-limit';

app.use('/api', rateLimitMiddleware);
// or
app.get('/api/data', rateLimitMiddleware, (req, res) => {
  res.json({ data: 'Protected data' });
});

Per-Route Limits

function createRateLimitMiddleware(capacity: number, refillRate: number) {
  const client = createClient({ serviceId: 'express-api' });
  return async (req, res, next) => {
    const result = await client.checkRateLimit({ identifier: req.user?.id || req.ip, capacity, refillRate });
    if (!result.allowed) return res.status(429).json({ error: 'Rate limit exceeded' });
    next();
  };
}

export const strictRateLimit = createRateLimitMiddleware(10, 1);
export const moderateRateLimit = createRateLimitMiddleware(100, 10);

// Usage
app.post('/api/login', strictRateLimit, handler);
app.get('/api/data', moderateRateLimit, handler);

User-Based Rate Limiting

Rate limit based on authenticated users:

// middleware/user-rate-limit.ts
import { createClient } from 'limitly-sdk';
import type { Request, Response, NextFunction } from 'express';

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

interface AuthenticatedRequest extends Request {
  user?: {
    id: string;
    plan: 'free' | 'pro' | 'enterprise';
  };
}

export async function userRateLimitMiddleware(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
) {
  // Require authentication
  if (!req.user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  // Different limits based on user plan
  const limits = {
    free: { capacity: 100, refillRate: 10 },
    pro: { capacity: 1000, refillRate: 100 },
    enterprise: { capacity: 10000, refillRate: 1000 }
  };
  
  const userLimits = limits[req.user.plan] || limits.free;
  
  const result = await client.checkRateLimit({
    identifier: req.user.id,
    ...userLimits
  });
  
  // Set headers
  if (result.limit) res.setHeader('X-RateLimit-Limit', result.limit.toString());
  if (result.remaining !== undefined) {
    res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
  }
  
  if (!result.allowed) {
    const retryAfter = result.reset 
      ? Math.ceil((result.reset - Date.now()) / 1000) 
      : 60;
    res.setHeader('Retry-After', retryAfter.toString());
    return res.status(429).json({
      error: 'Rate limit exceeded',
      message: `You have exceeded your ${req.user.plan} plan limits`,
      retryAfter
    });
  }
  
  next();
}

Error Handling

Handle rate limit errors gracefully:

// middleware/rate-limit.ts
import { createClient } from 'limitly-sdk';
import type { Request, Response, NextFunction } from 'express';

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

export async function rateLimitMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    const identifier = (req as any).user?.id || req.ip || 'anonymous';
    const result = await client.checkRateLimit(identifier);
    
    // Set headers
    if (result.limit) res.setHeader('X-RateLimit-Limit', result.limit.toString());
    if (result.remaining !== undefined) {
      res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
    }
    if (result.reset) {
      res.setHeader('X-RateLimit-Reset', Math.ceil(result.reset / 1000).toString());
    }
    
    if (!result.allowed) {
      const retryAfter = result.reset 
        ? Math.ceil((result.reset - Date.now()) / 1000) 
        : 60;
      
      res.setHeader('Retry-After', retryAfter.toString());
      return res.status(429).json({
        error: 'Rate limit exceeded',
        message: 'Too many requests, please try again later',
        retryAfter,
        resetAt: result.reset ? new Date(result.reset).toISOString() : undefined
      });
    }
    
    next();
  } catch (error) {
    // Handle Redis connection errors, timeouts, etc.
    console.error('Rate limit check failed:', error);
    
    // Fail open - allow request if rate limiting fails
    // In production, you might want to log this and alert
    next();
  }
}

Router-Level Application

Apply rate limiting to Express routers:

// routes/api.ts
import { Router } from 'express';
import { rateLimitMiddleware } from '../middleware/rate-limit';

const router = Router();

// Apply to all routes in this router
router.use(rateLimitMiddleware);

router.get('/data', (req, res) => {
  res.json({ data: 'Protected data' });
});

router.post('/submit', (req, res) => {
  res.json({ success: true });
});

export default router;

Conditional Limiting

export async function conditionalRateLimitMiddleware(req, res, next) {
  if (req.user?.isAdmin) return next();
  if (req.ip?.startsWith('192.168.')) return next();
  
  const result = await checkLimit(req.user?.id || req.ip);
  if (!result.allowed) return res.status(429).json({ error: 'Rate limit exceeded' });
  next();
}

Best Practices

  • Use service IDs for isolation
  • Handle errors gracefully (fail open)
  • Use authenticated user IDs when available
  • Apply different limits to different routes