New projects created: - michangarrito (marketplace mobile) - template-saas (SaaS template) - clinica-dental (dental ERP) - clinica-veterinaria (veterinary ERP) Architecture updates: - Move catalog from core/ to shared/ - Add MCP servers structure and templates - Add git management scripts - Update SUBREPOSITORIOS.md with 15 new repos - Update .gitignore for new projects Repository infrastructure: - 4 main repositories - 11 subrepositorios - Gitea remotes configured 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
402 lines
8.1 KiB
Markdown
402 lines
8.1 KiB
Markdown
# Guía de Implementación: Rate Limiting
|
|
|
|
**Versión:** 1.0.0
|
|
**Tiempo estimado:** 30 min - 1 hora
|
|
**Complejidad:** Baja
|
|
|
|
---
|
|
|
|
## Pre-requisitos
|
|
|
|
- [ ] Proyecto NestJS existente
|
|
- [ ] (Opcional) Redis para producción
|
|
|
|
---
|
|
|
|
## Paso 1: Instalar Dependencias
|
|
|
|
```bash
|
|
npm install @nestjs/throttler
|
|
|
|
# Para producción con Redis (opcional)
|
|
npm install @nestjs/throttler-storage-redis redis
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 2: Configurar Módulo
|
|
|
|
### Opción A: Configuración Simple
|
|
|
|
```typescript
|
|
// src/app.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
|
import { APP_GUARD } from '@nestjs/core';
|
|
|
|
@Module({
|
|
imports: [
|
|
ThrottlerModule.forRoot([{
|
|
ttl: 60000, // 60 segundos
|
|
limit: 100, // 100 requests por minuto
|
|
}]),
|
|
],
|
|
providers: [
|
|
{
|
|
provide: APP_GUARD,
|
|
useClass: ThrottlerGuard,
|
|
},
|
|
],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
### Opción B: Configuración con Ambiente
|
|
|
|
```typescript
|
|
// src/app.module.ts
|
|
import { Module } from '@nestjs/common';
|
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
|
import { APP_GUARD } from '@nestjs/core';
|
|
|
|
@Module({
|
|
imports: [
|
|
ConfigModule.forRoot(),
|
|
ThrottlerModule.forRootAsync({
|
|
imports: [ConfigModule],
|
|
inject: [ConfigService],
|
|
useFactory: (config: ConfigService) => ([{
|
|
ttl: config.get<number>('THROTTLE_TTL', 60000),
|
|
limit: config.get<number>('THROTTLE_LIMIT', 100),
|
|
}]),
|
|
}),
|
|
],
|
|
providers: [
|
|
{
|
|
provide: APP_GUARD,
|
|
useClass: ThrottlerGuard,
|
|
},
|
|
],
|
|
})
|
|
export class AppModule {}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 3: Variables de Entorno
|
|
|
|
```env
|
|
# .env
|
|
THROTTLE_TTL=60000
|
|
THROTTLE_LIMIT=100
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 4: Personalizar por Endpoint
|
|
|
|
### Limitar endpoint específico
|
|
|
|
```typescript
|
|
// src/modules/auth/controllers/auth.controller.ts
|
|
import { Controller, Post, Body } from '@nestjs/common';
|
|
import { Throttle } from '@nestjs/throttler';
|
|
|
|
@Controller('auth')
|
|
export class AuthController {
|
|
|
|
// Solo 5 intentos de login por minuto
|
|
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
|
@Post('login')
|
|
async login(@Body() dto: LoginDto) {
|
|
return this.authService.login(dto);
|
|
}
|
|
|
|
// Solo 3 registros por hora (anti-spam)
|
|
@Throttle({ default: { limit: 3, ttl: 3600000 } })
|
|
@Post('register')
|
|
async register(@Body() dto: RegisterDto) {
|
|
return this.authService.register(dto);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Excluir endpoint del rate limiting
|
|
|
|
```typescript
|
|
import { SkipThrottle } from '@nestjs/throttler';
|
|
|
|
@Controller('health')
|
|
export class HealthController {
|
|
@SkipThrottle()
|
|
@Get()
|
|
check() {
|
|
return { status: 'ok' };
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 5: Rate Limiting por Usuario (Opcional)
|
|
|
|
```typescript
|
|
// src/common/guards/custom-throttler.guard.ts
|
|
import { Injectable, ExecutionContext } from '@nestjs/common';
|
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
|
|
|
@Injectable()
|
|
export class CustomThrottlerGuard extends ThrottlerGuard {
|
|
protected async getTracker(req: Record<string, any>): Promise<string> {
|
|
// Si hay usuario autenticado, limitar por usuario
|
|
if (req.user?.id) {
|
|
return `user-${req.user.id}`;
|
|
}
|
|
// Si no, limitar por IP
|
|
return req.ip;
|
|
}
|
|
|
|
// Opcional: Personalizar mensaje de error
|
|
protected throwThrottlingException(): void {
|
|
throw new HttpException(
|
|
{
|
|
statusCode: 429,
|
|
message: 'Demasiadas solicitudes. Intente de nuevo más tarde.',
|
|
error: 'Too Many Requests',
|
|
},
|
|
HttpStatus.TOO_MANY_REQUESTS,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
Registrar en app.module.ts:
|
|
|
|
```typescript
|
|
providers: [
|
|
{
|
|
provide: APP_GUARD,
|
|
useClass: CustomThrottlerGuard, // En lugar de ThrottlerGuard
|
|
},
|
|
],
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 6: Múltiples Límites (Opcional)
|
|
|
|
```typescript
|
|
// src/app.module.ts
|
|
ThrottlerModule.forRoot([
|
|
{
|
|
name: 'short',
|
|
ttl: 1000, // 1 segundo
|
|
limit: 3, // Máximo 3 por segundo (anti-DDoS básico)
|
|
},
|
|
{
|
|
name: 'medium',
|
|
ttl: 10000, // 10 segundos
|
|
limit: 20, // Máximo 20 por 10 segundos
|
|
},
|
|
{
|
|
name: 'long',
|
|
ttl: 60000, // 1 minuto
|
|
limit: 100, // Máximo 100 por minuto
|
|
},
|
|
])
|
|
```
|
|
|
|
Usar en endpoint:
|
|
|
|
```typescript
|
|
@Throttle({
|
|
short: { limit: 1, ttl: 1000 },
|
|
long: { limit: 10, ttl: 60000 },
|
|
})
|
|
@Post('heavy-operation')
|
|
async heavyOperation() {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 7: Redis para Producción (Opcional)
|
|
|
|
```bash
|
|
npm install @nestjs/throttler-storage-redis ioredis
|
|
```
|
|
|
|
```typescript
|
|
// src/app.module.ts
|
|
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
|
|
import Redis from 'ioredis';
|
|
|
|
ThrottlerModule.forRootAsync({
|
|
imports: [ConfigModule],
|
|
inject: [ConfigService],
|
|
useFactory: (config: ConfigService) => ({
|
|
throttlers: [{
|
|
ttl: config.get('THROTTLE_TTL', 60000),
|
|
limit: config.get('THROTTLE_LIMIT', 100),
|
|
}],
|
|
storage: new ThrottlerStorageRedisService(
|
|
new Redis({
|
|
host: config.get('REDIS_HOST', 'localhost'),
|
|
port: config.get('REDIS_PORT', 6379),
|
|
password: config.get('REDIS_PASSWORD'),
|
|
}),
|
|
),
|
|
}),
|
|
})
|
|
```
|
|
|
|
Variables de entorno adicionales:
|
|
|
|
```env
|
|
REDIS_HOST=localhost
|
|
REDIS_PORT=6379
|
|
REDIS_PASSWORD=
|
|
```
|
|
|
|
---
|
|
|
|
## Paso 8: Logging de Rate Limits (Opcional)
|
|
|
|
```typescript
|
|
// src/common/guards/throttler-logger.guard.ts
|
|
import { Injectable, ExecutionContext, Logger } from '@nestjs/common';
|
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
|
|
|
@Injectable()
|
|
export class ThrottlerLoggerGuard extends ThrottlerGuard {
|
|
private readonly logger = new Logger('RateLimit');
|
|
|
|
protected async handleRequest(
|
|
context: ExecutionContext,
|
|
limit: number,
|
|
ttl: number,
|
|
throttler: any,
|
|
getTracker: any,
|
|
generateKey: any,
|
|
): Promise<boolean> {
|
|
const result = await super.handleRequest(
|
|
context,
|
|
limit,
|
|
ttl,
|
|
throttler,
|
|
getTracker,
|
|
generateKey,
|
|
);
|
|
|
|
if (!result) {
|
|
const req = context.switchToHttp().getRequest();
|
|
this.logger.warn(
|
|
`Rate limit exceeded: ${req.ip} - ${req.method} ${req.url}`,
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Configuraciones Recomendadas
|
|
|
|
### Desarrollo
|
|
|
|
```typescript
|
|
ThrottlerModule.forRoot([{
|
|
ttl: 60000,
|
|
limit: 1000, // Alto para no molestar durante desarrollo
|
|
}])
|
|
```
|
|
|
|
### Producción
|
|
|
|
```typescript
|
|
ThrottlerModule.forRoot([
|
|
{ name: 'short', ttl: 1000, limit: 10 },
|
|
{ name: 'long', ttl: 60000, limit: 100 },
|
|
])
|
|
```
|
|
|
|
### Por tipo de endpoint
|
|
|
|
| Tipo | Límite Recomendado |
|
|
|------|-------------------|
|
|
| Login | 5 por minuto |
|
|
| Registro | 3 por hora |
|
|
| API pública | 100 por minuto |
|
|
| API autenticada | 1000 por minuto |
|
|
| Operaciones costosas | 10 por minuto |
|
|
| Webhooks | Sin límite (SkipThrottle) |
|
|
| Health check | Sin límite (SkipThrottle) |
|
|
|
|
---
|
|
|
|
## Checklist de Implementación
|
|
|
|
- [ ] @nestjs/throttler instalado
|
|
- [ ] ThrottlerModule configurado en AppModule
|
|
- [ ] ThrottlerGuard registrado como APP_GUARD
|
|
- [ ] Variables de entorno configuradas
|
|
- [ ] Límites ajustados para endpoints críticos (login, register)
|
|
- [ ] Endpoints públicos excluidos (health, webhooks)
|
|
- [ ] Redis configurado (producción)
|
|
- [ ] Build pasa sin errores
|
|
- [ ] Test manual: verificar respuesta 429
|
|
|
|
---
|
|
|
|
## Verificar Funcionamiento
|
|
|
|
```bash
|
|
# Test simple con curl
|
|
for i in {1..10}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/test; done
|
|
|
|
# Debería mostrar 200 hasta el límite, luego 429
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Error: "ThrottlerModule is not imported"
|
|
Asegurarse de importar `ThrottlerModule.forRoot()` en AppModule.
|
|
|
|
### Rate limiting no funciona
|
|
Verificar que `ThrottlerGuard` esté registrado como `APP_GUARD`.
|
|
|
|
### 429 en todos los requests
|
|
El límite es muy bajo. Aumentar `limit` en configuración.
|
|
|
|
### Memory leaks en producción
|
|
Usar Redis como storage en lugar de memoria.
|
|
|
|
---
|
|
|
|
## Código de Referencia
|
|
|
|
Estructura típica:
|
|
|
|
```
|
|
src/
|
|
├── app.module.ts # ThrottlerModule configurado
|
|
├── common/
|
|
│ └── guards/
|
|
│ └── custom-throttler.guard.ts # Guard personalizado
|
|
└── modules/
|
|
└── auth/
|
|
└── controllers/
|
|
└── auth.controller.ts # @Throttle en endpoints
|
|
```
|
|
|
|
---
|
|
|
|
**Versión:** 1.0.0
|
|
**Sistema:** SIMCO Catálogo
|