npm package: @kyrasource/rate-limit
Description: Storage-agnostic rate limiting utilities for NestJS applications with in-memory store by default and pluggable backend support
Author: Kyrasource
Latest version: 1.0.1

# @kyrasource/rate-limit

Storage-agnostic rate limiting utilities for NestJS applications. Ships with a fast in-memory store by default and a clean store interface so you can plug in Redis or any other backend without coupling.

## Features

- ✅ **Flexible Storage**: In-memory by default, swap to Redis or any custom backend
- ✅ **Global Guard**: Automatically rate limits all endpoints (configurable)
- ✅ **Per-Route Configuration**: Override limits on specific handlers
- ✅ **IP-Based Tracking**: Extracts client IP from headers and socket
- ✅ **Whitelist Support**: Bypass rate limits for trusted IPs
- ✅ **Standard Headers**: Returns `X-RateLimit-*` and `Retry-After` headers
- ✅ **TypeScript Support**: Full type safety with interfaces
- ✅ **Error Handling**: Returns HTTP 429 when limit exceeded
- ✅ **Production Ready**: Used in distributed deployments with Redis

## Installation

```bash
npm install @kyrasource/rate-limit
```

## Quick Start

### 1. Setup in Your Module

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { RateLimitModule } from '@kyrasource/rate-limit';
import rateLimitConfig from './config/rate-limit.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [rateLimitConfig],
    }),
    RateLimitModule, // Registers as APP_GUARD automatically
  ],
})
export class AppModule {}
```

### 2. Configure Rate Limits in `.env`

```env
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_PREFIX=rate-limit
RATE_LIMIT_MESSAGE=Too many requests, please try again later.
RATE_LIMIT_WHITELIST=127.0.0.1,::1
```

### 3. Apply Decorators to Routes

```typescript
import { Controller, Get } from '@nestjs/common';
import { RateLimit } from '@kyrasource/rate-limit';

@Controller('api')
export class ApiController {
  // Uses global rate limit (100 requests per 15 minutes)
  @Get('public')
  public() {
    return { message: 'public endpoint' };
  }

  // Custom: 30 requests per minute
  @Get('search')
  @RateLimit({ windowMs: 60_000, maxRequests: 30 })
  search() {
    return { results: [] };
  }

  // Custom: 5 requests per hour
  @Get('expensive')
  @RateLimit({ windowMs: 3_600_000, maxRequests: 5 })
  expensiveOperation() {
    return { data: 'heavy computation' };
  }

  // No rate limiting
  @Get('health')
  @RateLimit({ enabled: false })
  health() {
    return { status: 'ok' };
  }
}
```

## Configuration

Create `src/config/rate-limit.config.ts`:

```typescript
import { registerAs } from '@nestjs/config';
import { RateLimitConfig } from '@kyrasource/rate-limit';

export default registerAs('rateLimitConfig', (): RateLimitConfig => ({
  enabled: process.env.RATE_LIMIT_ENABLED !== 'false',
  windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '900000', 10), // 15 min
  maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? '100', 10),
  keyPrefix: process.env.RATE_LIMIT_PREFIX ?? 'rate-limit',
  message: process.env.RATE_LIMIT_MESSAGE ?? 'Too many requests',
  whitelist: (process.env.RATE_LIMIT_WHITELIST ?? '')
    .split(',')
    .map(ip => ip.trim())
    .filter(Boolean),
}));
```

## API Reference

### RateLimit Decorator

Override global settings per route:

```typescript
@RateLimit({
  enabled: boolean;        // Enable/disable limiting for this route
  windowMs: number;        // Time window in milliseconds
  maxRequests: number;     // Max requests allowed in window
  keyPrefix?: string;      // Redis key prefix (optional)
  message?: string;        // Custom error message (optional)
  whitelist?: string[];    // IPs to bypass limits (optional)
})
```

### HTTP Headers

Every response includes:

```
X-RateLimit-Limit: 100          // Max requests in window
X-RateLimit-Remaining: 95       // Requests left
X-RateLimit-Reset: 1705718200   // UNIX timestamp when window resets
Retry-After: 30                 // (Only when 429) Seconds to retry
```

### Error Response (HTTP 429)

When rate limit is exceeded:

```json
{
  "statusCode": 429,
  "message": "Too many requests, please try again later",
  "retryAfter": 42
}
```

## Advanced Usage

### Using Redis Store (Distributed)

For multi-instance deployments, use Redis:

```typescript
// src/shared/rate-limit/redis-rate-limit.store.ts
import { Injectable } from '@nestjs/common';
import { RateLimitStore } from '@kyrasource/rate-limit';
import { RedisService } from '@kyrasource/redis';

@Injectable()
export class RedisRateLimitStore implements RateLimitStore {
  constructor(private readonly redis: RedisService) {}

  async get<T = any>(key: string): Promise<T | null> {
    return this.redis.get<T>(key);
  }

  async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
    await this.redis.set(key, value, ttlSeconds);
  }
}

// In your module:
import { RateLimitModule, RATE_LIMIT_STORE } from '@kyrasource/rate-limit';

@Module({
  imports: [RateLimitModule],
  providers: [
    RedisService,
    RedisRateLimitStore,
    {
      provide: RATE_LIMIT_STORE,
      useClass: RedisRateLimitStore,
    },
  ],
})
export class ApiModule {}
```

### Whitelisting Trusted IPs

```typescript
@Controller('admin')
export class AdminController {
  // Admin endpoints - stricter limits, but whitelist internal IPs
  @Get('users')
  @RateLimit({
    windowMs: 60_000,
    maxRequests: 10,
    whitelist: ['127.0.0.1', '192.168.1.0/24', '::1'],
  })
  listUsers() {
    return { users: [] };
  }
}
```

### Disabling for Specific Routes

```typescript
@Controller('health')
export class HealthController {
  // No rate limiting for health checks
  @Get()
  @RateLimit({ enabled: false })
  check() {
    return { status: 'healthy' };
  }
}
```

### Custom Key Strategy

By default, keys are built as: `${keyPrefix}:${method}:${path}:${clientIp}`

This means:
- Each HTTP method is tracked separately
- Each route is tracked separately
- Each client IP is tracked separately

Example: `rate-limit:POST:/api/search:203.0.113.45`

## IP Detection

The guard extracts client IP in this order:

1. `x-forwarded-for` header (supports multiple IPs, takes first)
2. `x-real-ip` header
3. `request.ip` from Express
4. `request.socket.remoteAddress`
5. Falls back to `'unknown'`

## Exports

From `@kyrasource/rate-limit` you can import:

```typescript
export { RateLimitModule } from './rate-limit.module';
export { RateLimitGuard } from './rate-limit.guard';
export { RateLimitService } from './rate-limit.service';
export { RateLimit } from './rate-limit.decorator';
export { RATE_LIMIT_STORE } from './rate-limit.constants';
export { RateLimitStore } from './rate-limit.store';
export { RateLimitConfig, RateLimitOptions, RateLimitResult } from './rate-limit.types';
```

## Default Configuration

If not configured via environment or ConfigService:

```typescript
{
  enabled: true,
  windowMs: 15 * 60 * 1000,  // 15 minutes
  maxRequests: 100,
  keyPrefix: 'ratelimit',
  message: 'Too many requests, please try again later',
  whitelist: [],
}
```

## Troubleshooting

### "Rate limit exceeded" immediately
- Check `RATE_LIMIT_MAX_REQUESTS` value (too low?)
- Check `RATE_LIMIT_WINDOW_MS` value (too short?)
- Verify IP detection is working (check logs for client IP)

### Whitelist not working
- Ensure IPs are comma-separated in `.env`
- Check that client IP matches whitelist entry
- Note: X-Forwarded-For may have multiple IPs

### Store errors
- If using Redis store, verify Redis is running
- Check `RATE_LIMIT_STORE` provider is registered
- Monitor logs for connection errors

## License

MIT - See LICENSE file

## Support

For issues, feature requests, or documentation, visit:
- GitHub: [kyrasource/rate-limit](https://github.com/kyrasource/rate-limit)
- npm: [@kyrasource/rate-limit](https://www.npmjs.com/package/@kyrasource/rate-limit)
  enabled: true,
  windowMs: 15 * 60 * 1000, // 15 minutes
  maxRequests: 100,
  keyPrefix: 'ratelimit',
  message: 'Too many requests, please try again later',
  whitelist: [] as string[],
};
```

Provide your own in `AppModule` (optional):

```ts
import { Module } from '@nestjs/common';
import { RateLimitModule } from '@kushi/rate-limit-lib';

@Module({
  imports: [RateLimitModule],
  providers: [
    { provide: 'RATE_LIMIT_CONFIG', useValue: {
        enabled: true,
        windowMs: 10_000,
        maxRequests: 20,
        keyPrefix: 'api',
        message: 'Rate limit exceeded',
        whitelist: ['127.0.0.1'],
    } },
  ],
})
export class AppModule {}
```

## Storage Abstraction (Replaceable Store)

This package is storage-agnostic. It relies on a minimal interface and a DI token so you can swap implementations.

- Token: `RATE_LIMIT_STORE`
- Interface:

```ts
export interface RateLimitStore {
  get<T = any>(key: string): Promise<T | null>;
  set(key: string, value: any, ttlSeconds?: number): Promise<void>;
}
```

It ships with a default `InMemoryRateLimitStore`. For multi-instance or distributed environments, override the token with your own store (e.g., Redis).

### Example: Redis-backed store (adapter in your app)

```ts
// src/shared/rate-limit/redis-rate-limit.store.ts
import { Injectable } from '@nestjs/common';
import { RateLimitStore } from '@kyrasource/rate-limit';
import { RedisService } from '@kyrasource/redis'; // your Redis client wrapper

@Injectable()
export class RedisRateLimitStore implements RateLimitStore {
  constructor(private readonly redis: RedisService) {}

  async get<T = any>(key: string): Promise<T | null> {
    return this.redis.get<T>(key);
  }

  async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
    await this.redis.set(key, value, ttlSeconds);
  }
}
```

Register it to override the default store:

```ts
import { Module } from '@nestjs/common';
import { RateLimitModule, RATE_LIMIT_STORE } from '@kyrasource/rate-limit';
import { RedisService } from '@kyrasource/redis';
import { RedisRateLimitStore } from './shared/rate-limit/redis-rate-limit.store';

@Module({
  imports: [RateLimitModule],
  providers: [
    RedisService,
    { provide: RATE_LIMIT_STORE, useClass: RedisRateLimitStore },
  ],
})
export class AppModule {}
```

## Per-Route Options

Use the `RateLimit` decorator to override any of the following per handler/class:

```ts
export interface RateLimitOptions {
  enabled: boolean;
  windowMs: number;     // duration of the window in ms
  maxRequests: number;  // max requests allowed in window
  keyPrefix: string;    // prefix for storage keys
  message: string;      // error message when blocked
  whitelist?: string[]; // list of IPs to bypass limiting
}
```

## Headers and Errors

On every request, the guard sets the following headers:
- `X-RateLimit-Limit`: the configured limit for the window
- `X-RateLimit-Remaining`: remaining requests in the current window
- `X-RateLimit-Reset`: UNIX seconds when the window resets
- `Retry-After`: seconds until reset (only when blocked)

When blocked, the guard throws HTTP 429 with a body like:

```json
{
  "message": "Too many requests, please try again later",
  "retryAfter": 42
}
```

## Exports

From `@kyrasource/rate-limit` you can import:
- `RateLimitModule`
- `RateLimitGuard`
- `RateLimitService`
- `RateLimit` (decorator)
- `RATE_LIMIT_STORE` (DI token)
- `InMemoryRateLimitStore` and `RateLimitStore` (for custom adapters)
- Types: `RateLimitOptions`, `RateLimitResult`, `RateLimitState`

## Notes

- The in-memory store is great for single-instance setups and tests. For distributed/clustered deployments, use a shared store (e.g., Redis) by overriding `RATE_LIMIT_STORE`.
- Keys are built as: `${keyPrefix}:${method}:${normalizedPath}:${ip}` where `normalizedPath` strips query strings.

## Build

```bash
cd packages/rate-limit-lib
npm run build
```

This compiles to `dist/` with type declarations.