create-tigra 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/create-tigra.js +292 -0
- package/package.json +41 -0
- package/template/.agent/rules/client/01-project-structure.md +326 -0
- package/template/.agent/rules/client/02-component-patterns.md +249 -0
- package/template/.agent/rules/client/03-typescript-rules.md +226 -0
- package/template/.agent/rules/client/04-state-management.md +474 -0
- package/template/.agent/rules/client/05-api-integration.md +129 -0
- package/template/.agent/rules/client/06-forms-validation.md +129 -0
- package/template/.agent/rules/client/07-common-patterns.md +150 -0
- package/template/.agent/rules/client/08-color-system.md +93 -0
- package/template/.agent/rules/client/09-security-rules.md +97 -0
- package/template/.agent/rules/client/10-testing-strategy.md +370 -0
- package/template/.agent/rules/global/ai-edit-safety.md +38 -0
- package/template/.agent/rules/server/01-db-and-migrations.md +242 -0
- package/template/.agent/rules/server/02-general-rules.md +111 -0
- package/template/.agent/rules/server/03-migrations.md +20 -0
- package/template/.agent/rules/server/04-pagination.md +130 -0
- package/template/.agent/rules/server/05-project-conventions.md +71 -0
- package/template/.agent/rules/server/06-response-handling.md +173 -0
- package/template/.agent/rules/server/07-testing-strategy.md +506 -0
- package/template/.agent/rules/server/08-observability.md +180 -0
- package/template/.agent/rules/server/09-api-documentation-v2.md +168 -0
- package/template/.agent/rules/server/10-background-jobs-v2.md +185 -0
- package/template/.agent/rules/server/11-rate-limiting-v2.md +210 -0
- package/template/.agent/rules/server/12-performance-optimization.md +567 -0
- package/template/.claude/rules/client-01-project-structure.md +327 -0
- package/template/.claude/rules/client-02-component-patterns.md +250 -0
- package/template/.claude/rules/client-03-typescript-rules.md +227 -0
- package/template/.claude/rules/client-04-state-management.md +475 -0
- package/template/.claude/rules/client-05-api-integration.md +130 -0
- package/template/.claude/rules/client-06-forms-validation.md +130 -0
- package/template/.claude/rules/client-07-common-patterns.md +151 -0
- package/template/.claude/rules/client-08-color-system.md +94 -0
- package/template/.claude/rules/client-09-security-rules.md +98 -0
- package/template/.claude/rules/client-10-testing-strategy.md +371 -0
- package/template/.claude/rules/global-ai-edit-safety.md +39 -0
- package/template/.claude/rules/server-01-db-and-migrations.md +243 -0
- package/template/.claude/rules/server-02-general-rules.md +112 -0
- package/template/.claude/rules/server-03-migrations.md +21 -0
- package/template/.claude/rules/server-04-pagination.md +131 -0
- package/template/.claude/rules/server-05-project-conventions.md +72 -0
- package/template/.claude/rules/server-06-response-handling.md +174 -0
- package/template/.claude/rules/server-07-testing-strategy.md +507 -0
- package/template/.claude/rules/server-08-observability.md +181 -0
- package/template/.claude/rules/server-09-api-documentation-v2.md +169 -0
- package/template/.claude/rules/server-10-background-jobs-v2.md +186 -0
- package/template/.claude/rules/server-11-rate-limiting-v2.md +211 -0
- package/template/.claude/rules/server-12-performance-optimization.md +568 -0
- package/template/.cursor/rules/client-01-project-structure.mdc +327 -0
- package/template/.cursor/rules/client-02-component-patterns.mdc +250 -0
- package/template/.cursor/rules/client-03-typescript-rules.mdc +227 -0
- package/template/.cursor/rules/client-04-state-management.mdc +475 -0
- package/template/.cursor/rules/client-05-api-integration.mdc +130 -0
- package/template/.cursor/rules/client-06-forms-validation.mdc +130 -0
- package/template/.cursor/rules/client-07-common-patterns.mdc +151 -0
- package/template/.cursor/rules/client-08-color-system.mdc +94 -0
- package/template/.cursor/rules/client-09-security-rules.mdc +98 -0
- package/template/.cursor/rules/client-10-testing-strategy.mdc +371 -0
- package/template/.cursor/rules/global-ai-edit-safety.mdc +39 -0
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +243 -0
- package/template/.cursor/rules/server-02-general-rules.mdc +112 -0
- package/template/.cursor/rules/server-03-migrations.mdc +21 -0
- package/template/.cursor/rules/server-04-pagination.mdc +131 -0
- package/template/.cursor/rules/server-05-project-conventions.mdc +72 -0
- package/template/.cursor/rules/server-06-response-handling.mdc +174 -0
- package/template/.cursor/rules/server-07-testing-strategy.mdc +507 -0
- package/template/.cursor/rules/server-08-observability.mdc +181 -0
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +169 -0
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +186 -0
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +211 -0
- package/template/.cursor/rules/server-12-performance-optimization.mdc +568 -0
- package/template/CLAUDE.md +207 -0
- package/template/server/.env.example +148 -0
- package/template/server/.tsc-aliasrc.json +12 -0
- package/template/server/README.md +175 -0
- package/template/server/SECURITY.md +190 -0
- package/template/server/biome.json +42 -0
- package/template/server/docker-compose.yml +111 -0
- package/template/server/package.json +83 -0
- package/template/server/postman_collection.json +733 -0
- package/template/server/prisma/schema.prisma +92 -0
- package/template/server/prisma/seed.ts +142 -0
- package/template/server/scripts/wait-for-db.js +60 -0
- package/template/server/src/app.ts +74 -0
- package/template/server/src/config/env.ts +101 -0
- package/template/server/src/hooks/request-timing.hook.ts +26 -0
- package/template/server/src/libs/auth/authenticate.middleware.ts +22 -0
- package/template/server/src/libs/auth/rbac.middleware.test.ts +134 -0
- package/template/server/src/libs/auth/rbac.middleware.ts +147 -0
- package/template/server/src/libs/db.ts +76 -0
- package/template/server/src/libs/error-handler.ts +89 -0
- package/template/server/src/libs/logger.ts +60 -0
- package/template/server/src/libs/queue.ts +79 -0
- package/template/server/src/libs/redis.ts +79 -0
- package/template/server/src/libs/swagger-schemas.ts +16 -0
- package/template/server/src/modules/admin/admin.controller.ts +122 -0
- package/template/server/src/modules/admin/admin.routes.ts +100 -0
- package/template/server/src/modules/admin/admin.schemas.ts +35 -0
- package/template/server/src/modules/admin/admin.service.ts +167 -0
- package/template/server/src/modules/auth/auth.controller.ts +141 -0
- package/template/server/src/modules/auth/auth.integration.test.ts +150 -0
- package/template/server/src/modules/auth/auth.repo.ts +218 -0
- package/template/server/src/modules/auth/auth.routes.ts +204 -0
- package/template/server/src/modules/auth/auth.schemas.ts +137 -0
- package/template/server/src/modules/auth/auth.service.test.ts +119 -0
- package/template/server/src/modules/auth/auth.service.ts +329 -0
- package/template/server/src/modules/auth/auth.types.ts +97 -0
- package/template/server/src/modules/resources/resources.controller.ts +218 -0
- package/template/server/src/modules/resources/resources.repo.ts +253 -0
- package/template/server/src/modules/resources/resources.routes.ts +355 -0
- package/template/server/src/modules/resources/resources.schemas.ts +146 -0
- package/template/server/src/modules/resources/resources.service.ts +218 -0
- package/template/server/src/modules/resources/resources.types.ts +73 -0
- package/template/server/src/plugins/rate-limit.plugin.ts +21 -0
- package/template/server/src/plugins/security.plugin.ts +21 -0
- package/template/server/src/plugins/swagger.plugin.ts +41 -0
- package/template/server/src/routes/health.routes.ts +31 -0
- package/template/server/src/server.ts +142 -0
- package/template/server/src/test/setup.ts +38 -0
- package/template/server/src/types/fastify.d.ts +36 -0
- package/template/server/src/utils/errors.ts +108 -0
- package/template/server/src/utils/pagination.ts +120 -0
- package/template/server/src/utils/response.ts +110 -0
- package/template/server/src/workers/file.worker.ts +106 -0
- package/template/server/tsconfig.build.json +30 -0
- package/template/server/tsconfig.build.tsbuildinfo +1 -0
- package/template/server/tsconfig.json +89 -0
- package/template/server/tsconfig.test.json +22 -0
- package/template/server/vitest.config.ts +98 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
globs: "server/**/*"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **SCOPE**: These rules apply specifically to the **server** directory.
|
|
7
|
+
|
|
8
|
+
# API Documentation
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@fastify/swagger": "^8.12.0",
|
|
16
|
+
"@fastify/swagger-ui": "^2.0.0",
|
|
17
|
+
"zod-to-json-schema": "^3.22.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// app.ts
|
|
26
|
+
import swagger from '@fastify/swagger';
|
|
27
|
+
import swaggerUI from '@fastify/swagger-ui';
|
|
28
|
+
|
|
29
|
+
await app.register(swagger, {
|
|
30
|
+
openapi: {
|
|
31
|
+
info: {
|
|
32
|
+
title: 'API Server',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
},
|
|
35
|
+
servers: [
|
|
36
|
+
{ url: 'http://localhost:3000', description: 'Development' },
|
|
37
|
+
],
|
|
38
|
+
tags: [
|
|
39
|
+
{ name: 'auth', description: 'Authentication' },
|
|
40
|
+
{ name: 'resources', description: 'Resource operations' },
|
|
41
|
+
],
|
|
42
|
+
components: {
|
|
43
|
+
securitySchemes: {
|
|
44
|
+
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await app.register(swaggerUI, {
|
|
51
|
+
routePrefix: '/docs',
|
|
52
|
+
uiConfig: { docExpansion: 'list', deepLinking: true },
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Access:** `http://localhost:3000/docs`
|
|
57
|
+
|
|
58
|
+
## Route Documentation Pattern
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// modules/resources/resources.schemas.ts
|
|
62
|
+
import { z } from 'zod';
|
|
63
|
+
|
|
64
|
+
export const CreateResourceSchema = z.object({
|
|
65
|
+
title: z.string().min(1).max(200),
|
|
66
|
+
price: z.number().positive(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const ResourceResponseSchema = z.object({
|
|
70
|
+
success: z.literal(true),
|
|
71
|
+
message: z.string(),
|
|
72
|
+
data: z.object({
|
|
73
|
+
id: z.string().uuid(),
|
|
74
|
+
title: z.string(),
|
|
75
|
+
price: z.number(),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// modules/resources/resources.routes.ts
|
|
82
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
83
|
+
import * as schemas from './resources.schemas';
|
|
84
|
+
|
|
85
|
+
app.post('/resources', {
|
|
86
|
+
schema: {
|
|
87
|
+
description: 'Create a new resource',
|
|
88
|
+
tags: ['resources'],
|
|
89
|
+
summary: 'Create resource',
|
|
90
|
+
body: zodToJsonSchema(schemas.CreateResourceSchema, 'CreateResource'),
|
|
91
|
+
response: {
|
|
92
|
+
201: zodToJsonSchema(schemas.ResourceResponseSchema, 'ResourceResponse'),
|
|
93
|
+
400: {
|
|
94
|
+
description: 'Validation error',
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
success: { type: 'boolean', enum: [false] },
|
|
98
|
+
error: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
code: { type: 'string' },
|
|
102
|
+
message: { type: 'string' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
security: [{ bearerAuth: [] }],
|
|
109
|
+
},
|
|
110
|
+
handler: resourceController.createResource,
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Document All Responses
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
response: {
|
|
118
|
+
200: successSchema,
|
|
119
|
+
400: validationErrorSchema,
|
|
120
|
+
401: unauthorizedSchema,
|
|
121
|
+
404: notFoundSchema,
|
|
122
|
+
500: internalErrorSchema,
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Production Config
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
if (process.env.NODE_ENV === 'production') {
|
|
130
|
+
// Option 1: Disable docs
|
|
131
|
+
// Don't register swagger-ui
|
|
132
|
+
|
|
133
|
+
// Option 2: Require auth for docs
|
|
134
|
+
await app.register(swaggerUI, {
|
|
135
|
+
routePrefix: '/docs',
|
|
136
|
+
transformSpecification: (swaggerObject, request, reply) => {
|
|
137
|
+
if (!request.headers.authorization) {
|
|
138
|
+
reply.code(401).send({ error: 'Unauthorized' });
|
|
139
|
+
}
|
|
140
|
+
return swaggerObject;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Best Practices
|
|
147
|
+
|
|
148
|
+
### ✅ DO:
|
|
149
|
+
- Use Zod schemas for all routes
|
|
150
|
+
- Document all error responses
|
|
151
|
+
- Add descriptions and summaries
|
|
152
|
+
- Include security requirements
|
|
153
|
+
- Tag routes by domain
|
|
154
|
+
|
|
155
|
+
### ❌ DON'T:
|
|
156
|
+
- Skip documentation on routes
|
|
157
|
+
- Forget error response schemas
|
|
158
|
+
- Expose docs publicly in production
|
|
159
|
+
- Use generic descriptions
|
|
160
|
+
|
|
161
|
+
## Checklist
|
|
162
|
+
|
|
163
|
+
- [ ] Swagger and Swagger UI registered
|
|
164
|
+
- [ ] All routes have schemas
|
|
165
|
+
- [ ] Zod schemas converted to JSON Schema
|
|
166
|
+
- [ ] Error responses documented
|
|
167
|
+
- [ ] Tags used for grouping
|
|
168
|
+
- [ ] Security requirements documented
|
|
169
|
+
- [ ] Production docs secured
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
globs: "server/**/*"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **SCOPE**: These rules apply specifically to the **server** directory.
|
|
7
|
+
|
|
8
|
+
# Background Jobs & Queue Processing
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"bullmq": "^5.0.0",
|
|
16
|
+
"ioredis": "^5.3.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Redis Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// libs/redis.ts
|
|
25
|
+
import Redis from 'ioredis';
|
|
26
|
+
|
|
27
|
+
export const redis = new Redis({
|
|
28
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
29
|
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
30
|
+
maxRetriesPerRequest: null, // Required for BullMQ
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Queue Setup
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// libs/queue.ts
|
|
38
|
+
import { Queue, Worker, QueueEvents } from 'bullmq';
|
|
39
|
+
import { redis } from './redis';
|
|
40
|
+
|
|
41
|
+
export const emailQueue = new Queue('emails', {
|
|
42
|
+
connection: redis,
|
|
43
|
+
defaultJobOptions: {
|
|
44
|
+
attempts: 3,
|
|
45
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
46
|
+
removeOnComplete: { age: 3600, count: 100 },
|
|
47
|
+
removeOnFail: { age: 86400 },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Worker Pattern
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// workers/email.worker.ts
|
|
56
|
+
import { Worker, Job } from 'bullmq';
|
|
57
|
+
|
|
58
|
+
interface EmailJobData {
|
|
59
|
+
to: string;
|
|
60
|
+
subject: string;
|
|
61
|
+
body: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const emailWorker = new Worker<EmailJobData>(
|
|
65
|
+
'emails',
|
|
66
|
+
async (job: Job<EmailJobData>) => {
|
|
67
|
+
const { to, subject, body } = job.data;
|
|
68
|
+
|
|
69
|
+
logger.info({ jobId: job.id, to }, 'Processing email');
|
|
70
|
+
|
|
71
|
+
await sendEmail({ to, subject, body });
|
|
72
|
+
|
|
73
|
+
return { success: true, sentAt: new Date().toISOString() };
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
connection: redis,
|
|
77
|
+
concurrency: 5,
|
|
78
|
+
limiter: { max: 100, duration: 60000 }, // 100 jobs/minute
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Job Patterns
|
|
84
|
+
|
|
85
|
+
### 1. Immediate Background Job
|
|
86
|
+
```typescript
|
|
87
|
+
// Add to queue (non-blocking)
|
|
88
|
+
await emailQueue.add('welcome-email', {
|
|
89
|
+
to: user.email,
|
|
90
|
+
subject: 'Welcome!',
|
|
91
|
+
body: 'Thanks for signing up',
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Delayed Job
|
|
96
|
+
```typescript
|
|
97
|
+
// Send after 24 hours
|
|
98
|
+
await emailQueue.add(
|
|
99
|
+
'reminder-email',
|
|
100
|
+
{ to: user.email, subject: 'Reminder', body: 'Complete your profile' },
|
|
101
|
+
{ delay: 24 * 60 * 60 * 1000 }
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. Scheduled Job (Cron)
|
|
106
|
+
```typescript
|
|
107
|
+
// Daily at 2 AM
|
|
108
|
+
await cleanupQueue.add(
|
|
109
|
+
'daily-cleanup',
|
|
110
|
+
{},
|
|
111
|
+
{ repeat: { pattern: '0 2 * * *' } }
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Priority Job
|
|
116
|
+
```typescript
|
|
117
|
+
await paymentQueue.add(
|
|
118
|
+
'process-payment',
|
|
119
|
+
{ orderId, amount },
|
|
120
|
+
{ priority: 1 } // Lower = higher priority
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Error Handling
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
emailWorker.on('failed', (job, error) => {
|
|
128
|
+
logger.error({ jobId: job?.id, error }, 'Job failed');
|
|
129
|
+
|
|
130
|
+
// Move to dead letter queue after all retries
|
|
131
|
+
if (job && job.attemptsMade >= job.opts.attempts!) {
|
|
132
|
+
deadLetterQueue.add('failed-email', {
|
|
133
|
+
originalJob: job.data,
|
|
134
|
+
failedReason: error.message,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Graceful Shutdown
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// server.ts
|
|
144
|
+
const workers = [emailWorker, fileWorker];
|
|
145
|
+
|
|
146
|
+
async function gracefulShutdown() {
|
|
147
|
+
logger.info('Shutting down workers...');
|
|
148
|
+
await Promise.all(workers.map((w) => w.close()));
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
153
|
+
process.on('SIGINT', gracefulShutdown);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Use Cases
|
|
157
|
+
|
|
158
|
+
- **Email sending** (welcome, verification, notifications)
|
|
159
|
+
- **File processing** (image thumbnails, video encoding)
|
|
160
|
+
- **Report generation** (PDFs, exports)
|
|
161
|
+
- **Data cleanup** (expired records, temp files)
|
|
162
|
+
- **External API calls** (webhooks, third-party sync)
|
|
163
|
+
|
|
164
|
+
## Best Practices
|
|
165
|
+
|
|
166
|
+
### ✅ DO:
|
|
167
|
+
- Use background jobs for slow operations (>1s)
|
|
168
|
+
- Set appropriate retry strategies
|
|
169
|
+
- Implement rate limiting for external APIs
|
|
170
|
+
- Monitor job failures
|
|
171
|
+
- Clean up old jobs
|
|
172
|
+
|
|
173
|
+
### ❌ DON'T:
|
|
174
|
+
- Block HTTP responses with heavy processing
|
|
175
|
+
- Forget error handling
|
|
176
|
+
- Skip monitoring
|
|
177
|
+
- Use queues for everything (simple tasks can run inline)
|
|
178
|
+
|
|
179
|
+
## Checklist
|
|
180
|
+
|
|
181
|
+
- [ ] BullMQ and Redis installed
|
|
182
|
+
- [ ] Queues created with retry config
|
|
183
|
+
- [ ] Workers implemented with error handling
|
|
184
|
+
- [ ] Graceful shutdown configured
|
|
185
|
+
- [ ] Job monitoring in place
|
|
186
|
+
- [ ] Dead letter queue for failures
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
globs: "server/**/*"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **SCOPE**: These rules apply specifically to the **server** directory.
|
|
7
|
+
|
|
8
|
+
# Rate Limiting & Throttling
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@fastify/rate-limit": "^9.0.0",
|
|
16
|
+
"ioredis": "^5.3.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Basic Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// app.ts
|
|
25
|
+
import rateLimit from '@fastify/rate-limit';
|
|
26
|
+
import { redis } from './libs/redis';
|
|
27
|
+
|
|
28
|
+
await app.register(rateLimit, {
|
|
29
|
+
global: true,
|
|
30
|
+
max: 100,
|
|
31
|
+
timeWindow: '15 minutes',
|
|
32
|
+
redis,
|
|
33
|
+
nameSpace: 'rate-limit:',
|
|
34
|
+
addHeaders: {
|
|
35
|
+
'x-ratelimit-limit': true,
|
|
36
|
+
'x-ratelimit-remaining': true,
|
|
37
|
+
'x-ratelimit-reset': true,
|
|
38
|
+
'retry-after': true,
|
|
39
|
+
},
|
|
40
|
+
errorResponseBuilder: (request, context) => ({
|
|
41
|
+
success: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
44
|
+
message: `Rate limit exceeded. Retry in ${Math.ceil(context.ttl / 1000)}s.`,
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Per-Route Limits
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Disable global, configure per route
|
|
54
|
+
await app.register(rateLimit, { global: false, redis });
|
|
55
|
+
|
|
56
|
+
// Strict limit for login
|
|
57
|
+
app.post('/auth/login', {
|
|
58
|
+
config: {
|
|
59
|
+
rateLimit: {
|
|
60
|
+
max: 5,
|
|
61
|
+
timeWindow: '15 minutes',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
handler: authController.login,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Generous limit for authenticated routes
|
|
68
|
+
app.get('/resources', {
|
|
69
|
+
preHandler: [app.authenticate],
|
|
70
|
+
config: {
|
|
71
|
+
rateLimit: {
|
|
72
|
+
max: 1000,
|
|
73
|
+
timeWindow: '15 minutes',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
handler: resourceController.listResources,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Custom Rate Limit Keys
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
await app.register(rateLimit, {
|
|
84
|
+
redis,
|
|
85
|
+
keyGenerator: (request) => {
|
|
86
|
+
// Authenticated: rate limit by user ID
|
|
87
|
+
if (request.user) {
|
|
88
|
+
return `user:${request.user.id}`;
|
|
89
|
+
}
|
|
90
|
+
// Anonymous: rate limit by IP
|
|
91
|
+
return `ip:${request.ip}`;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## User-Based Tiered Limits
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
await app.register(rateLimit, {
|
|
100
|
+
redis,
|
|
101
|
+
max: async (request) => {
|
|
102
|
+
if (!request.user) return 100; // Anonymous
|
|
103
|
+
if (request.user.role === 'ADMIN') return 10000;
|
|
104
|
+
if (request.user.tier === 'PREMIUM') return 5000;
|
|
105
|
+
return 1000; // Standard
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Rate Limit Tiers
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// lib/constants/rate-limits.ts
|
|
114
|
+
export const RATE_LIMITS = {
|
|
115
|
+
LOGIN: { max: 5, timeWindow: '15 minutes' },
|
|
116
|
+
REGISTER: { max: 3, timeWindow: '1 hour' },
|
|
117
|
+
PASSWORD_RESET: { max: 3, timeWindow: '1 hour' },
|
|
118
|
+
PUBLIC: { max: 100, timeWindow: '15 minutes' },
|
|
119
|
+
AUTHENTICATED: { max: 1000, timeWindow: '15 minutes' },
|
|
120
|
+
PREMIUM: { max: 5000, timeWindow: '15 minutes' },
|
|
121
|
+
ADMIN: { max: 10000, timeWindow: '15 minutes' },
|
|
122
|
+
FILE_UPLOAD: { max: 10, timeWindow: '1 hour' },
|
|
123
|
+
} as const;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Monitoring
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
await app.register(rateLimit, {
|
|
130
|
+
redis,
|
|
131
|
+
onExceeding: (request, key) => {
|
|
132
|
+
logger.warn({ key, ip: request.ip, url: request.url }, 'Approaching limit');
|
|
133
|
+
},
|
|
134
|
+
onExceeded: (request, key) => {
|
|
135
|
+
logger.error({ key, ip: request.ip, url: request.url }, 'Limit exceeded');
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Whitelist IPs
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
144
|
+
const whitelistedIPs = ['127.0.0.1', '::1'];
|
|
145
|
+
if (whitelistedIPs.includes(request.ip)) {
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
request.bypassRateLimit = true;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await app.register(rateLimit, {
|
|
152
|
+
redis,
|
|
153
|
+
skip: (request) => request.bypassRateLimit === true,
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Check Rate Limit Status
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
app.get('/me/rate-limit', {
|
|
161
|
+
preHandler: [app.authenticate],
|
|
162
|
+
handler: async (request, reply) => {
|
|
163
|
+
const key = `user:${request.user.id}`;
|
|
164
|
+
const current = await redis.get(`rate-limit:${key}`);
|
|
165
|
+
const ttl = await redis.ttl(`rate-limit:${key}`);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
limit: 1000,
|
|
169
|
+
used: parseInt(current || '0'),
|
|
170
|
+
remaining: 1000 - parseInt(current || '0'),
|
|
171
|
+
resetIn: ttl,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Strategy Summary
|
|
178
|
+
|
|
179
|
+
| Endpoint | Max | Window | Key |
|
|
180
|
+
|----------|-----|--------|-----|
|
|
181
|
+
| Login | 5 | 15 min | IP |
|
|
182
|
+
| Register | 3 | 1 hour | IP + email |
|
|
183
|
+
| Public API | 100 | 15 min | IP |
|
|
184
|
+
| Authenticated | 1000 | 15 min | User ID |
|
|
185
|
+
| Premium | 5000 | 15 min | User ID |
|
|
186
|
+
| Admin | 10000 | 15 min | User ID |
|
|
187
|
+
|
|
188
|
+
## Best Practices
|
|
189
|
+
|
|
190
|
+
### ✅ DO:
|
|
191
|
+
- Use Redis for distributed rate limiting
|
|
192
|
+
- Set stricter limits for auth endpoints
|
|
193
|
+
- Return helpful error messages with retry time
|
|
194
|
+
- Log rate limit violations
|
|
195
|
+
- Add rate limit headers to responses
|
|
196
|
+
- Whitelist localhost for testing
|
|
197
|
+
|
|
198
|
+
### ❌ DON'T:
|
|
199
|
+
- Use in-memory rate limiting in production
|
|
200
|
+
- Apply same limits to all endpoints
|
|
201
|
+
- Block users permanently without investigation
|
|
202
|
+
- Forget to test rate limits
|
|
203
|
+
|
|
204
|
+
## Checklist
|
|
205
|
+
|
|
206
|
+
- [ ] Rate limiting configured with Redis
|
|
207
|
+
- [ ] Per-route limits set appropriately
|
|
208
|
+
- [ ] Custom error responses implemented
|
|
209
|
+
- [ ] Rate limit headers enabled
|
|
210
|
+
- [ ] Monitoring and logging in place
|
|
211
|
+
- [ ] Whitelist configured for testing
|