@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rag-patterns
|
|
3
|
+
description: Build retrieval-augmented generation systems with chunking, embeddings, vector stores, hybrid search, and evaluation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RAG Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply when your LLM needs access to private data, documents, or knowledge that
|
|
11
|
+
isn't in its training set. RAG is the right choice when fine-tuning is too
|
|
12
|
+
expensive or when the source data changes frequently.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### 1. Chunking Strategies
|
|
17
|
+
|
|
18
|
+
Split documents into chunks that preserve semantic meaning. Chunk size directly
|
|
19
|
+
impacts retrieval quality.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Fixed-size with overlap — simple baseline
|
|
23
|
+
function chunkFixed(text: string, size = 512, overlap = 64): string[] {
|
|
24
|
+
const chunks: string[] = [];
|
|
25
|
+
for (let i = 0; i < text.length; i += size - overlap) {
|
|
26
|
+
chunks.push(text.slice(i, i + size));
|
|
27
|
+
}
|
|
28
|
+
return chunks;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Recursive character splitting — respects document structure
|
|
32
|
+
// Split on paragraphs first, then sentences, then words
|
|
33
|
+
const separators = ['\n\n', '\n', '. ', ' '];
|
|
34
|
+
|
|
35
|
+
// Semantic chunking — group sentences by embedding similarity
|
|
36
|
+
// 1. Embed each sentence
|
|
37
|
+
// 2. Compute cosine similarity between adjacent sentences
|
|
38
|
+
// 3. Split where similarity drops below threshold (e.g., 0.75)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rules of thumb: 256-512 tokens for precise retrieval, 1024+ for broader context.
|
|
42
|
+
Always include overlap (10-20%) to avoid splitting mid-concept.
|
|
43
|
+
|
|
44
|
+
### 2. Embedding Models
|
|
45
|
+
|
|
46
|
+
| Model | Dimensions | Speed | Quality | Cost |
|
|
47
|
+
|-------|-----------|-------|---------|------|
|
|
48
|
+
| `text-embedding-3-small` | 1536 | Fast | Good | $0.02/1M tokens |
|
|
49
|
+
| `text-embedding-3-large` | 3072 | Medium | Best | $0.13/1M tokens |
|
|
50
|
+
| `voyage-3` | 1024 | Fast | Excellent | $0.06/1M tokens |
|
|
51
|
+
| `nomic-embed-text` (local) | 768 | Varies | Good | Free |
|
|
52
|
+
|
|
53
|
+
Normalize embeddings before storing. Use Matryoshka dimensions (OpenAI) to
|
|
54
|
+
reduce storage — 256d retains ~95% quality of full 1536d.
|
|
55
|
+
|
|
56
|
+
### 3. Vector Store Selection
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// Pinecone — managed, scales to billions
|
|
60
|
+
import { Pinecone } from '@pinecone-database/pinecone';
|
|
61
|
+
const index = pc.index('docs').namespace('v2');
|
|
62
|
+
await index.upsert([{ id: 'chunk-1', values: embedding, metadata: { source } }]);
|
|
63
|
+
|
|
64
|
+
// pgvector — runs in your existing Postgres
|
|
65
|
+
// CREATE EXTENSION vector;
|
|
66
|
+
// CREATE TABLE chunks (id serial, embedding vector(1536), content text);
|
|
67
|
+
// CREATE INDEX ON chunks USING ivfflat (embedding vector_cosine_ops);
|
|
68
|
+
|
|
69
|
+
// Chroma — local dev, easy to start
|
|
70
|
+
import { ChromaClient } from 'chromadb';
|
|
71
|
+
const collection = await client.getOrCreateCollection({ name: 'docs' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Choose pgvector when you already run Postgres and have < 10M vectors. Use
|
|
75
|
+
Pinecone/Qdrant for billion-scale or when you need managed infrastructure.
|
|
76
|
+
|
|
77
|
+
### 4. Hybrid Search (Vector + Keyword)
|
|
78
|
+
|
|
79
|
+
Pure vector search misses exact matches. Combine with BM25 for best results.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
async function hybridSearch(query: string, topK = 10) {
|
|
83
|
+
const [vectorResults, keywordResults] = await Promise.all([
|
|
84
|
+
vectorStore.similaritySearch(query, topK),
|
|
85
|
+
fullTextSearch(query, topK), // BM25 via Postgres ts_vector or Elasticsearch
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Reciprocal Rank Fusion — merges two ranked lists
|
|
89
|
+
const scores = new Map<string, number>();
|
|
90
|
+
const k = 60; // RRF constant
|
|
91
|
+
for (const [rank, doc] of vectorResults.entries()) {
|
|
92
|
+
scores.set(doc.id, (scores.get(doc.id) ?? 0) + 1 / (k + rank + 1));
|
|
93
|
+
}
|
|
94
|
+
for (const [rank, doc] of keywordResults.entries()) {
|
|
95
|
+
scores.set(doc.id, (scores.get(doc.id) ?? 0) + 1 / (k + rank + 1));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return [...scores.entries()]
|
|
99
|
+
.sort((a, b) => b[1] - a[1])
|
|
100
|
+
.slice(0, topK);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 5. Reranking
|
|
105
|
+
|
|
106
|
+
Retrieve broadly (top 20-50), then rerank with a cross-encoder for precision.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// Cohere reranker — best quality/cost trade-off
|
|
110
|
+
const reranked = await cohere.rerank({
|
|
111
|
+
model: 'rerank-english-v3.0',
|
|
112
|
+
query,
|
|
113
|
+
documents: candidates.map(c => c.content),
|
|
114
|
+
topN: 5,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Filter by relevance score threshold
|
|
118
|
+
const relevant = reranked.results.filter(r => r.relevanceScore > 0.3);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 6. Evaluation
|
|
122
|
+
|
|
123
|
+
Measure retrieval quality before tuning generation.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Hit rate — does the correct chunk appear in top-K?
|
|
127
|
+
// MRR (Mean Reciprocal Rank) — how high does it rank?
|
|
128
|
+
// Faithfulness — does the LLM answer actually use the retrieved context?
|
|
129
|
+
// Answer relevance — does the answer address the question?
|
|
130
|
+
|
|
131
|
+
// RAGAS framework for automated evaluation
|
|
132
|
+
// 1. Build a test set: 50-100 question/answer/context triples
|
|
133
|
+
// 2. Run retrieval, measure hit@5 and MRR
|
|
134
|
+
// 3. Run generation, score faithfulness and relevance with LLM-as-judge
|
|
135
|
+
// 4. Track metrics per chunk strategy and model combination
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Examples
|
|
139
|
+
|
|
140
|
+
| Problem | Pattern | Result |
|
|
141
|
+
|---------|---------|--------|
|
|
142
|
+
| Legal docs need exact clause matching | Hybrid search (BM25 + vector) | Catches both semantic and keyword matches |
|
|
143
|
+
| Code documentation with mixed languages | Recursive chunking on markdown headers | Preserves code blocks intact |
|
|
144
|
+
| Customer support over 100K articles | Retrieve 50, rerank to 5 | 3x precision vs. vector-only |
|
|
145
|
+
| Answers hallucinate despite good retrieval | Add source citations in system prompt | User can verify, trust increases |
|
|
146
|
+
| Embedding costs growing fast | Matryoshka 256d + cache embeddings | 6x storage reduction, minimal quality loss |
|
|
147
|
+
|
|
148
|
+
## Checklist
|
|
149
|
+
|
|
150
|
+
- [ ] Chunk size is tuned for your content type (not just default 512)
|
|
151
|
+
- [ ] Chunks include metadata (source URL, section title, timestamp)
|
|
152
|
+
- [ ] Overlap between chunks prevents mid-sentence splits
|
|
153
|
+
- [ ] Hybrid search combines vector similarity with keyword matching
|
|
154
|
+
- [ ] Reranker filters retrieval results before sending to LLM
|
|
155
|
+
- [ ] System prompt instructs the model to cite sources and say "I don't know"
|
|
156
|
+
- [ ] Evaluation set of 50+ question/context/answer triples exists
|
|
157
|
+
- [ ] Hit rate and MRR are measured and tracked across changes
|
|
158
|
+
- [ ] Embedding model choice is benchmarked, not assumed
|
|
159
|
+
- [ ] Vector index uses appropriate type (IVFFlat for < 1M, HNSW for quality)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rate-limiting
|
|
3
|
+
description: Rate limiting patterns for token bucket, sliding window, Redis-backed distributed limits, per-user quotas, and response headers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rate Limiting Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Implement rate limiting to protect APIs from abuse, enforce usage quotas, ensure fair resource allocation, and prevent cascading failures. Apply rate limiting at the gateway level for global protection and at the route level for sensitive endpoints (login, payment, search). Choose the algorithm based on your needs: fixed window for simplicity, sliding window for accuracy, or token bucket for burst tolerance.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Fixed Window Rate Limiter
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/rate-limiters/fixed-window.ts
|
|
17
|
+
export class FixedWindowLimiter {
|
|
18
|
+
private windows = new Map<string, { count: number; resetAt: number }>();
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly maxRequests: number,
|
|
22
|
+
private readonly windowMs: number,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
check(key: string): { allowed: boolean; remaining: number; resetAt: number } {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const window = this.windows.get(key);
|
|
28
|
+
|
|
29
|
+
if (!window || now >= window.resetAt) {
|
|
30
|
+
const resetAt = now + this.windowMs;
|
|
31
|
+
this.windows.set(key, { count: 1, resetAt });
|
|
32
|
+
return { allowed: true, remaining: this.maxRequests - 1, resetAt };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (window.count >= this.maxRequests) {
|
|
36
|
+
return { allowed: false, remaining: 0, resetAt: window.resetAt };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
window.count++;
|
|
40
|
+
return { allowed: true, remaining: this.maxRequests - window.count, resetAt: window.resetAt };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Sliding Window Rate Limiter
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// src/rate-limiters/sliding-window.ts
|
|
49
|
+
export class SlidingWindowLimiter {
|
|
50
|
+
private requests = new Map<string, number[]>();
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly maxRequests: number,
|
|
54
|
+
private readonly windowMs: number,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
check(key: string): { allowed: boolean; remaining: number; retryAfterMs: number } {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const windowStart = now - this.windowMs;
|
|
60
|
+
|
|
61
|
+
// Get or initialize timestamps
|
|
62
|
+
let timestamps = this.requests.get(key) ?? [];
|
|
63
|
+
|
|
64
|
+
// Remove expired timestamps
|
|
65
|
+
timestamps = timestamps.filter((t) => t > windowStart);
|
|
66
|
+
|
|
67
|
+
if (timestamps.length >= this.maxRequests) {
|
|
68
|
+
const oldestInWindow = timestamps[0];
|
|
69
|
+
const retryAfterMs = oldestInWindow + this.windowMs - now;
|
|
70
|
+
this.requests.set(key, timestamps);
|
|
71
|
+
return { allowed: false, remaining: 0, retryAfterMs };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
timestamps.push(now);
|
|
75
|
+
this.requests.set(key, timestamps);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
allowed: true,
|
|
79
|
+
remaining: this.maxRequests - timestamps.length,
|
|
80
|
+
retryAfterMs: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Token Bucket (Burst-Tolerant)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// src/rate-limiters/token-bucket.ts
|
|
90
|
+
export class TokenBucketLimiter {
|
|
91
|
+
private buckets = new Map<string, { tokens: number; lastRefill: number }>();
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
private readonly capacity: number,
|
|
95
|
+
private readonly refillRate: number, // tokens per second
|
|
96
|
+
) {}
|
|
97
|
+
|
|
98
|
+
check(key: string, cost: number = 1): { allowed: boolean; remaining: number } {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
let bucket = this.buckets.get(key);
|
|
101
|
+
|
|
102
|
+
if (!bucket) {
|
|
103
|
+
bucket = { tokens: this.capacity, lastRefill: now };
|
|
104
|
+
this.buckets.set(key, bucket);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Refill tokens based on elapsed time
|
|
108
|
+
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
109
|
+
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed * this.refillRate);
|
|
110
|
+
bucket.lastRefill = now;
|
|
111
|
+
|
|
112
|
+
if (bucket.tokens < cost) {
|
|
113
|
+
return { allowed: false, remaining: Math.floor(bucket.tokens) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
bucket.tokens -= cost;
|
|
117
|
+
return { allowed: true, remaining: Math.floor(bucket.tokens) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Usage: 100 tokens capacity, refill 10/sec, allows bursts up to 100
|
|
122
|
+
const limiter = new TokenBucketLimiter(100, 10);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Redis-Backed Distributed Rate Limiter
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// src/rate-limiters/redis-sliding-window.ts
|
|
129
|
+
import { Redis } from 'ioredis';
|
|
130
|
+
|
|
131
|
+
export class RedisRateLimiter {
|
|
132
|
+
constructor(
|
|
133
|
+
private readonly redis: Redis,
|
|
134
|
+
private readonly maxRequests: number,
|
|
135
|
+
private readonly windowMs: number,
|
|
136
|
+
private readonly prefix: string = 'rl',
|
|
137
|
+
) {}
|
|
138
|
+
|
|
139
|
+
async check(key: string): Promise<{
|
|
140
|
+
allowed: boolean;
|
|
141
|
+
remaining: number;
|
|
142
|
+
resetAt: number;
|
|
143
|
+
retryAfterMs: number;
|
|
144
|
+
}> {
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const windowStart = now - this.windowMs;
|
|
147
|
+
const redisKey = `${this.prefix}:${key}`;
|
|
148
|
+
|
|
149
|
+
// Atomic Lua script: remove expired, count, add if under limit
|
|
150
|
+
const result = await this.redis.eval(
|
|
151
|
+
`
|
|
152
|
+
local key = KEYS[1]
|
|
153
|
+
local now = tonumber(ARGV[1])
|
|
154
|
+
local window_start = tonumber(ARGV[2])
|
|
155
|
+
local max_requests = tonumber(ARGV[3])
|
|
156
|
+
local window_ms = tonumber(ARGV[4])
|
|
157
|
+
|
|
158
|
+
-- Remove expired entries
|
|
159
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
|
|
160
|
+
|
|
161
|
+
-- Count current entries
|
|
162
|
+
local count = redis.call('ZCARD', key)
|
|
163
|
+
|
|
164
|
+
if count < max_requests then
|
|
165
|
+
-- Add new entry
|
|
166
|
+
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
|
|
167
|
+
redis.call('PEXPIRE', key, window_ms)
|
|
168
|
+
return {1, max_requests - count - 1, 0}
|
|
169
|
+
else
|
|
170
|
+
-- Get oldest entry to calculate retry-after
|
|
171
|
+
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
|
|
172
|
+
local retry_after = oldest[2] and (tonumber(oldest[2]) + window_ms - now) or window_ms
|
|
173
|
+
return {0, 0, retry_after}
|
|
174
|
+
end
|
|
175
|
+
`,
|
|
176
|
+
1, redisKey, now, windowStart, this.maxRequests, this.windowMs
|
|
177
|
+
) as [number, number, number];
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
allowed: result[0] === 1,
|
|
181
|
+
remaining: result[1],
|
|
182
|
+
resetAt: now + this.windowMs,
|
|
183
|
+
retryAfterMs: result[2],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Express Middleware Integration
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// src/middleware/rate-limit.ts
|
|
193
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
194
|
+
import { RedisRateLimiter } from '../rate-limiters/redis-sliding-window';
|
|
195
|
+
|
|
196
|
+
interface RateLimitConfig {
|
|
197
|
+
maxRequests: number;
|
|
198
|
+
windowMs: number;
|
|
199
|
+
keyGenerator?: (req: Request) => string;
|
|
200
|
+
skipFailedRequests?: boolean;
|
|
201
|
+
message?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function rateLimit(limiter: RedisRateLimiter, config: RateLimitConfig) {
|
|
205
|
+
const getKey = config.keyGenerator ?? ((req: Request) => {
|
|
206
|
+
const userId = (req as any).userId;
|
|
207
|
+
return userId ? `user:${userId}` : `ip:${req.ip}`;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
211
|
+
const key = getKey(req);
|
|
212
|
+
const result = await limiter.check(key);
|
|
213
|
+
|
|
214
|
+
// Always set rate limit headers
|
|
215
|
+
res.set({
|
|
216
|
+
'X-RateLimit-Limit': String(config.maxRequests),
|
|
217
|
+
'X-RateLimit-Remaining': String(result.remaining),
|
|
218
|
+
'X-RateLimit-Reset': String(Math.ceil(result.resetAt / 1000)),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!result.allowed) {
|
|
222
|
+
res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
|
|
223
|
+
return res.status(429).json({
|
|
224
|
+
error: config.message ?? 'Too many requests',
|
|
225
|
+
retryAfterMs: result.retryAfterMs,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
next();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage with different limits per endpoint
|
|
234
|
+
const redis = new Redis(process.env.REDIS_URL!);
|
|
235
|
+
|
|
236
|
+
app.use('/api/auth/login',
|
|
237
|
+
rateLimit(new RedisRateLimiter(redis, 5, 900_000, 'rl:login'), {
|
|
238
|
+
maxRequests: 5,
|
|
239
|
+
windowMs: 900_000, // 5 per 15 minutes
|
|
240
|
+
keyGenerator: (req) => `login:${req.ip}`,
|
|
241
|
+
message: 'Too many login attempts. Try again in 15 minutes.',
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
app.use('/api/',
|
|
246
|
+
rateLimit(new RedisRateLimiter(redis, 100, 60_000, 'rl:api'), {
|
|
247
|
+
maxRequests: 100,
|
|
248
|
+
windowMs: 60_000, // 100 per minute
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Tiered Rate Limiting
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// src/rate-limiters/tiered.ts
|
|
257
|
+
interface RateTier {
|
|
258
|
+
name: string;
|
|
259
|
+
maxRequests: number;
|
|
260
|
+
windowMs: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const tiers: Record<string, RateTier> = {
|
|
264
|
+
free: { name: 'free', maxRequests: 60, windowMs: 60_000 },
|
|
265
|
+
pro: { name: 'pro', maxRequests: 600, windowMs: 60_000 },
|
|
266
|
+
enterprise: { name: 'enterprise', maxRequests: 6000, windowMs: 60_000 },
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function getTierForUser(userId: string): RateTier {
|
|
270
|
+
const subscription = getUserSubscription(userId);
|
|
271
|
+
return tiers[subscription.plan] ?? tiers.free;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
app.use('/api/', async (req: Request, res: Response, next: NextFunction) => {
|
|
275
|
+
const userId = (req as any).userId;
|
|
276
|
+
const tier = getTierForUser(userId);
|
|
277
|
+
const limiter = new RedisRateLimiter(redis, tier.maxRequests, tier.windowMs, `rl:${tier.name}`);
|
|
278
|
+
|
|
279
|
+
const result = await limiter.check(userId);
|
|
280
|
+
|
|
281
|
+
res.set({
|
|
282
|
+
'X-RateLimit-Limit': String(tier.maxRequests),
|
|
283
|
+
'X-RateLimit-Remaining': String(result.remaining),
|
|
284
|
+
'X-RateLimit-Reset': String(Math.ceil(result.resetAt / 1000)),
|
|
285
|
+
'X-RateLimit-Tier': tier.name,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!result.allowed) {
|
|
289
|
+
res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
|
|
290
|
+
return res.status(429).json({
|
|
291
|
+
error: 'Rate limit exceeded',
|
|
292
|
+
tier: tier.name,
|
|
293
|
+
limit: tier.maxRequests,
|
|
294
|
+
upgradeUrl: '/pricing',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
next();
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Examples
|
|
303
|
+
|
|
304
|
+
| Algorithm | Burst Behavior | Complexity | Best For |
|
|
305
|
+
|-----------|---------------|------------|----------|
|
|
306
|
+
| Fixed window | Full burst at boundary | O(1) | Simple APIs, low traffic |
|
|
307
|
+
| Sliding window | Even distribution | O(n) | Accurate per-second limits |
|
|
308
|
+
| Token bucket | Allows controlled bursts | O(1) | APIs with bursty traffic |
|
|
309
|
+
| Leaky bucket | Smooths to constant rate | O(1) | Downstream protection |
|
|
310
|
+
|
|
311
|
+
## Checklist
|
|
312
|
+
- [ ] Rate limit headers (`X-RateLimit-*`, `Retry-After`) included in all responses
|
|
313
|
+
- [ ] `429` responses include retry-after time and human-readable message
|
|
314
|
+
- [ ] Redis-backed limiter used for multi-instance deployments
|
|
315
|
+
- [ ] Key generation uses user ID for authenticated, IP for anonymous
|
|
316
|
+
- [ ] Sensitive endpoints (login, password reset) have stricter limits
|
|
317
|
+
- [ ] Tiered limits aligned with pricing plans
|
|
318
|
+
- [ ] Lua script ensures atomic check-and-increment in Redis
|
|
319
|
+
- [ ] Rate limiter tested for boundary conditions and clock drift
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-components
|
|
3
|
+
description: Build maintainable React components using composition, hooks, memoization, and error boundaries.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Component Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building any React UI — new features, refactoring
|
|
11
|
+
existing components, or reviewing PRs. They keep components small, testable,
|
|
12
|
+
and resilient to change.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### 1. Composition Over Props Drilling
|
|
17
|
+
|
|
18
|
+
Pass components as children or render props instead of threading data through
|
|
19
|
+
five layers of props.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Bad — LayoutPage knows about User, Sidebar, and every child
|
|
23
|
+
<LayoutPage user={user} sidebarItems={items} headerTitle="Dashboard" />
|
|
24
|
+
|
|
25
|
+
// Good — parent composes, children are independent
|
|
26
|
+
<Layout>
|
|
27
|
+
<Layout.Header>
|
|
28
|
+
<h1>Dashboard</h1>
|
|
29
|
+
</Layout.Header>
|
|
30
|
+
<Layout.Sidebar>
|
|
31
|
+
<NavItems items={items} />
|
|
32
|
+
</Layout.Sidebar>
|
|
33
|
+
<Layout.Content>
|
|
34
|
+
<UserProfile user={user} />
|
|
35
|
+
</Layout.Content>
|
|
36
|
+
</Layout>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use compound components (dot notation) for tightly related UI groups.
|
|
40
|
+
|
|
41
|
+
### 2. Custom Hooks Extract Logic
|
|
42
|
+
|
|
43
|
+
Move stateful logic out of components into reusable hooks.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
function useDebounce<T>(value: T, delayMs: number): T {
|
|
47
|
+
const [debounced, setDebounced] = useState(value);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const timer = setTimeout(() => setDebounced(value), delayMs);
|
|
50
|
+
return () => clearTimeout(timer);
|
|
51
|
+
}, [value, delayMs]);
|
|
52
|
+
return debounced;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function SearchInput() {
|
|
56
|
+
const [query, setQuery] = useState('');
|
|
57
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
58
|
+
// fetch with debouncedQuery, component stays thin
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Rules: prefix with `use`, don't return JSX, test independently with
|
|
63
|
+
`renderHook` from Testing Library.
|
|
64
|
+
|
|
65
|
+
### 3. Memoization — When It Matters
|
|
66
|
+
|
|
67
|
+
`React.memo` prevents re-renders when props haven't changed. Use it for
|
|
68
|
+
expensive renders, not everywhere.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// Memoize: renders a large list, parent re-renders often
|
|
72
|
+
const UserList = memo(function UserList({ users }: { users: User[] }) {
|
|
73
|
+
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Stabilize callbacks passed to memoized children
|
|
77
|
+
function Parent() {
|
|
78
|
+
const handleClick = useCallback((id: string) => {
|
|
79
|
+
navigate(`/users/${id}`);
|
|
80
|
+
}, [navigate]);
|
|
81
|
+
|
|
82
|
+
return <UserList users={users} onClick={handleClick} />;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Skip memo for: leaf components, components that always get new props, or
|
|
87
|
+
anything that renders in < 1ms.
|
|
88
|
+
|
|
89
|
+
### 4. Error Boundaries
|
|
90
|
+
|
|
91
|
+
Catch render errors so one broken component doesn't crash the entire page.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
class ErrorBoundary extends Component<PropsWithChildren<{ fallback: ReactNode }>> {
|
|
95
|
+
state = { hasError: false, error: null as Error | null };
|
|
96
|
+
|
|
97
|
+
static getDerivedStateFromError(error: Error) {
|
|
98
|
+
return { hasError: true, error };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
102
|
+
reportError(error, info.componentStack);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
render() {
|
|
106
|
+
if (this.state.hasError) return this.props.fallback;
|
|
107
|
+
return this.props.children;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Usage — isolate feature boundaries
|
|
112
|
+
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
|
|
113
|
+
<Dashboard />
|
|
114
|
+
</ErrorBoundary>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Place boundaries around: route-level pages, third-party widgets, data-dependent
|
|
118
|
+
feature sections.
|
|
119
|
+
|
|
120
|
+
### 5. Suspense for Async UI
|
|
121
|
+
|
|
122
|
+
Use Suspense with lazy components and data fetching libraries that support it.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
const AnalyticsChart = lazy(() => import('./AnalyticsChart'));
|
|
126
|
+
|
|
127
|
+
function Dashboard() {
|
|
128
|
+
return (
|
|
129
|
+
<Suspense fallback={<ChartSkeleton />}>
|
|
130
|
+
<AnalyticsChart />
|
|
131
|
+
</Suspense>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Combine with error boundaries for the complete async pattern:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<ErrorBoundary fallback={<ErrorCard />}>
|
|
140
|
+
<Suspense fallback={<Skeleton />}>
|
|
141
|
+
<AsyncFeature />
|
|
142
|
+
</Suspense>
|
|
143
|
+
</ErrorBoundary>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 6. Prop Types That Document Intent
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
interface ButtonProps {
|
|
150
|
+
variant: 'primary' | 'secondary' | 'danger';
|
|
151
|
+
size?: 'sm' | 'md' | 'lg';
|
|
152
|
+
isLoading?: boolean;
|
|
153
|
+
children: ReactNode;
|
|
154
|
+
onClick: () => void;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use discriminated unions for mutually exclusive prop groups:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
type ModalProps =
|
|
162
|
+
| { mode: 'confirm'; onConfirm: () => void; onCancel: () => void }
|
|
163
|
+
| { mode: 'alert'; onDismiss: () => void };
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 7. Forwarding Refs and Polymorphism
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
170
|
+
function Input({ label, error, ...rest }, ref) {
|
|
171
|
+
return (
|
|
172
|
+
<div>
|
|
173
|
+
<label>{label}</label>
|
|
174
|
+
<input ref={ref} aria-invalid={!!error} {...rest} />
|
|
175
|
+
{error && <span role="alert">{error}</span>}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Examples
|
|
183
|
+
|
|
184
|
+
| Problem | Pattern | Result |
|
|
185
|
+
|---------|---------|--------|
|
|
186
|
+
| Props drilled 4+ levels | Composition / Context | Each component gets only what it needs |
|
|
187
|
+
| Logic duplicated across components | Custom hook | Single source, tested independently |
|
|
188
|
+
| Parent re-render cascades | `memo` + `useCallback` | Child skips unnecessary renders |
|
|
189
|
+
| One crash kills the page | Error boundary | Graceful fallback per section |
|
|
190
|
+
| Large bundle, slow initial load | `lazy` + `Suspense` | Code-split, skeleton loading |
|
|
191
|
+
|
|
192
|
+
## Checklist
|
|
193
|
+
|
|
194
|
+
- [ ] No component file exceeds 200 lines — extract hooks or sub-components
|
|
195
|
+
- [ ] Stateful logic lives in custom hooks, not inline in JSX
|
|
196
|
+
- [ ] Error boundaries wrap every route-level page and risky third-party widget
|
|
197
|
+
- [ ] `Suspense` with skeleton fallbacks wraps lazy-loaded and data-fetching components
|
|
198
|
+
- [ ] `React.memo` is applied only where profiling shows wasted re-renders
|
|
199
|
+
- [ ] Props use discriminated unions for mutually exclusive states
|
|
200
|
+
- [ ] Interactive elements have accessible names (`aria-label`, visible label, or `aria-labelledby`)
|
|
201
|
+
- [ ] No `any` in component props — every prop is explicitly typed
|