AI Prompt: Forma3D.Connect — Phase 5n: Rate Limiting Implementation¶
Purpose: This prompt instructs an AI to implement rate limiting on API endpoints to prevent abuse
Estimated Effort: 1-2 days (~8-12 hours)
Prerequisites: Phase 5m completed (Unused Components)
Output: Rate limiting on all public API endpoints with configurable limits
Status: 🟡 PENDING
🎯 Mission¶
You are continuing development of Forma3D.Connect, building on the Phase 5m foundation. Your task is to implement Phase 5n: Rate Limiting — specifically addressing TD-012 (Missing Rate Limiting) from the technical debt register.
Why This Matters:
Without rate limiting, the system is vulnerable to:
- DoS Attacks: Malicious actors can overwhelm the API
- Webhook Replay Attacks: Rapid webhook replays could cause issues
- Accidental Client Loops: Buggy clients can create infinite request loops
- Resource Exhaustion: Database connections and memory can be depleted
Phase 5n delivers:
- Rate limiting on all API endpoints
- Per-endpoint configurable limits
- Webhook-specific rate limiting
- IP-based and API-key-based limiting
- Clear rate limit headers in responses
📋 Context: Technical Debt Item¶
TD-012: Missing Rate Limiting¶
| Attribute | Value |
|---|---|
| Type | Architecture Debt |
| Priority | Medium |
| Location | All public API endpoints |
| Interest Rate | Medium |
| Principal (Effort) | 1-2 days |
🛠️ Implementation Phases¶
Phase 1: Install and Configure Throttler (1 hour)¶
Priority: Critical | Impact: High | Dependencies: None
1. Install NestJS Throttler¶
pnpm add @nestjs/throttler
2. Add Configuration Variables¶
Update apps/api/src/config/configuration.ts:
// === Rate Limiting ===
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
RATE_LIMIT_TTL_SECONDS: number = 60;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
RATE_LIMIT_DEFAULT: number = 100;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
RATE_LIMIT_WEBHOOK: number = 50;
@Transform(({ value }) => parseInt(value, 10))
@IsNumber()
@Min(1)
RATE_LIMIT_AUTH: number = 10;
3. Update AppConfigService¶
Add to apps/api/src/config/config.service.ts:
export interface RateLimitConfig {
ttlSeconds: number;
defaultLimit: number;
webhookLimit: number;
authLimit: number;
}
// In AppConfigService class:
get rateLimit(): RateLimitConfig {
return {
ttlSeconds: this.configService.get<number>('RATE_LIMIT_TTL_SECONDS', 60),
defaultLimit: this.configService.get<number>('RATE_LIMIT_DEFAULT', 100),
webhookLimit: this.configService.get<number>('RATE_LIMIT_WEBHOOK', 50),
authLimit: this.configService.get<number>('RATE_LIMIT_AUTH', 10),
};
}
Phase 2: Configure Throttler Module (1 hour)¶
Priority: High | Impact: High | Dependencies: Phase 1
1. Create Throttler Configuration¶
Create apps/api/src/throttler/throttler.module.ts:
import { Module } from '@nestjs/common';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { AppConfigService } from '../config/config.service';
@Module({
imports: [
ThrottlerModule.forRootAsync({
inject: [AppConfigService],
useFactory: (config: AppConfigService) => ({
throttlers: [
{
name: 'default',
ttl: config.rateLimit.ttlSeconds * 1000,
limit: config.rateLimit.defaultLimit,
},
{
name: 'webhook',
ttl: config.rateLimit.ttlSeconds * 1000,
limit: config.rateLimit.webhookLimit,
},
{
name: 'auth',
ttl: config.rateLimit.ttlSeconds * 1000,
limit: config.rateLimit.authLimit,
},
],
}),
}),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppThrottlerModule {}
2. Import in App Module¶
Update apps/api/src/app.module.ts:
import { AppThrottlerModule } from './throttler/throttler.module';
@Module({
imports: [
// ... other imports
AppThrottlerModule,
],
})
export class AppModule {}
Phase 3: Apply Rate Limits to Controllers (2 hours)¶
Priority: High | Impact: High | Dependencies: Phase 2
1. Create Custom Decorators¶
Create apps/api/src/throttler/throttle.decorators.ts:
import { Throttle, SkipThrottle } from '@nestjs/throttler';
/**
* Apply webhook rate limits (lower than default)
*/
export const WebhookThrottle = () => Throttle({ webhook: { ttl: 60000, limit: 50 } });
/**
* Apply strict rate limits for sensitive endpoints
*/
export const AuthThrottle = () => Throttle({ auth: { ttl: 60000, limit: 10 } });
/**
* Apply higher limits for read-heavy endpoints
*/
export const ReadThrottle = () => Throttle({ default: { ttl: 60000, limit: 200 } });
/**
* Skip rate limiting (use sparingly, document why)
*/
export { SkipThrottle };
2. Apply to Webhook Controllers¶
Update apps/api/src/shopify/shopify.controller.ts:
import { WebhookThrottle } from '../throttler/throttle.decorators';
@Controller('webhooks/shopify')
@WebhookThrottle()
export class ShopifyController {
@Post('orders/create')
async handleOrderCreate(@Body() payload: unknown) {
// ...
}
}
Update apps/api/src/simplyprint/simplyprint-webhook.controller.ts:
import { WebhookThrottle } from '../throttler/throttle.decorators';
@Controller('webhooks/simplyprint')
@WebhookThrottle()
export class SimplyPrintWebhookController {
// ...
}
Update apps/api/src/sendcloud/sendcloud.controller.ts:
import { WebhookThrottle } from '../throttler/throttle.decorators';
@Controller('webhooks/sendcloud')
@WebhookThrottle()
export class SendcloudController {
// ...
}
3. Apply to Read-Heavy Endpoints¶
Update apps/api/src/orders/orders.controller.ts:
import { ReadThrottle } from '../throttler/throttle.decorators';
@Controller('api/v1/orders')
export class OrdersController {
@Get()
@ReadThrottle()
async findAll(@Query() query: PaginationDto) {
// Higher limit for list endpoints
}
@Get(':id')
@ReadThrottle()
async findOne(@Param('id') id: string) {
// ...
}
}
4. Skip Health Check Endpoints¶
Update apps/api/src/health/health.controller.ts:
import { SkipThrottle } from '../throttler/throttle.decorators';
@Controller('health')
@SkipThrottle()
export class HealthController {
@Get()
check() {
return { status: 'ok' };
}
}
Phase 4: Custom Rate Limit Storage (Optional, 2 hours)¶
Priority: Medium | Impact: Medium | Dependencies: Phase 3
For horizontal scaling, implement Redis-based storage:
1. Install Redis Throttler Storage¶
pnpm add @nestjs/throttler-storage-redis ioredis
2. Create Redis Storage Configuration¶
Update apps/api/src/throttler/throttler.module.ts:
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import Redis from 'ioredis';
@Module({
imports: [
ThrottlerModule.forRootAsync({
inject: [AppConfigService],
useFactory: (config: AppConfigService) => ({
throttlers: [
// ... throttlers config
],
storage: config.redisUrl
? new ThrottlerStorageRedisService(new Redis(config.redisUrl))
: undefined,
}),
}),
],
})
export class AppThrottlerModule {}
Phase 5: Add Rate Limit Headers (1 hour)¶
Priority: Medium | Impact: Medium | Dependencies: Phase 3
1. Create Rate Limit Header Interceptor¶
Create apps/api/src/throttler/rate-limit-headers.interceptor.ts:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Response } from 'express';
@Injectable()
export class RateLimitHeadersInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse<Response>();
// Headers are set by ThrottlerGuard, but we can add custom ones
if (!response.headersSent) {
response.setHeader('X-RateLimit-Policy', 'sliding-window');
}
}),
);
}
}
Phase 6: Testing (1 hour)¶
Priority: High | Impact: High | Dependencies: Phase 3
1. Create Throttler Unit Tests¶
Create apps/api/src/throttler/throttler.module.spec.ts:
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import * as request from 'supertest';
describe('Rate Limiting', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ThrottlerModule.forRoot({
throttlers: [{ ttl: 60000, limit: 2 }],
}),
],
// ... test controller
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('should allow requests under the limit', async () => {
await request(app.getHttpServer())
.get('/test')
.expect(200);
});
it('should block requests over the limit', async () => {
// Make requests up to limit
await request(app.getHttpServer()).get('/test');
await request(app.getHttpServer()).get('/test');
// Third request should be blocked
await request(app.getHttpServer())
.get('/test')
.expect(429);
});
});
📁 Files to Create/Modify¶
New Files¶
apps/api/src/throttler/throttler.module.ts
apps/api/src/throttler/throttle.decorators.ts
apps/api/src/throttler/rate-limit-headers.interceptor.ts
apps/api/src/throttler/throttler.module.spec.ts
Modified Files¶
apps/api/package.json (add @nestjs/throttler)
apps/api/src/app.module.ts
apps/api/src/config/configuration.ts
apps/api/src/config/config.service.ts
apps/api/src/shopify/shopify.controller.ts
apps/api/src/simplyprint/simplyprint-webhook.controller.ts
apps/api/src/sendcloud/sendcloud.controller.ts
apps/api/src/orders/orders.controller.ts
apps/api/src/health/health.controller.ts
apps/api/.env.example
✅ Validation Checklist¶
- @nestjs/throttler installed
- ThrottlerModule configured with defaults
- Configuration variables added for limits
- Webhook controllers have lower limits
- Health endpoints skip throttling
- Rate limit headers returned in responses
- Tests verify throttling behavior
-
pnpm nx build apipasses -
pnpm nx test apipasses
Final Verification¶
# Build passes
pnpm nx build api
# Tests pass
pnpm nx test api
# Manual verification
curl -i http://localhost:3000/api/v1/orders
# Check for X-RateLimit-* headers
📝 .env.example Addition¶
# === Rate Limiting ===
RATE_LIMIT_TTL_SECONDS=60
RATE_LIMIT_DEFAULT=100
RATE_LIMIT_WEBHOOK=50
RATE_LIMIT_AUTH=10
REDIS_URL=redis://localhost:6379 # Optional, for distributed rate limiting
END OF PROMPT
This prompt resolves TD-012 from the technical debt register by implementing rate limiting on all public API endpoints.