@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,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: serverless-patterns
|
|
3
|
+
description: Serverless patterns for cold starts, function composition, event triggers, DynamoDB, and step functions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Serverless Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building on AWS Lambda, Azure Functions, or Google Cloud
|
|
11
|
+
Functions. Use this skill for minimizing cold starts, composing functions into
|
|
12
|
+
workflows, handling event triggers from queues and streams, designing DynamoDB
|
|
13
|
+
tables, and orchestrating multi-step processes with Step Functions.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Cold Start Mitigation
|
|
18
|
+
|
|
19
|
+
Cold starts add latency on first invocation. Minimize them by keeping bundles
|
|
20
|
+
small, initializing outside the handler, and using provisioned concurrency for
|
|
21
|
+
latency-sensitive paths.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// Initialize OUTSIDE the handler — runs once per container
|
|
25
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
26
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
|
|
27
|
+
|
|
28
|
+
const client = new DynamoDBClient({})
|
|
29
|
+
const ddb = DynamoDBDocumentClient.from(client)
|
|
30
|
+
|
|
31
|
+
// Lazy initialization for rarely-used resources
|
|
32
|
+
let s3Client: S3Client | null = null
|
|
33
|
+
function getS3() {
|
|
34
|
+
if (!s3Client) s3Client = new S3Client({})
|
|
35
|
+
return s3Client
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The handler itself should be fast
|
|
39
|
+
export async function handler(event: APIGatewayProxyEvent) {
|
|
40
|
+
const body = JSON.parse(event.body ?? '{}')
|
|
41
|
+
const result = await processRequest(ddb, body)
|
|
42
|
+
return {
|
|
43
|
+
statusCode: 200,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify(result),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Bundle size tips:**
|
|
51
|
+
- Use `esbuild` or `tsup` to tree-shake and bundle
|
|
52
|
+
- Import only needed clients: `@aws-sdk/client-dynamodb` not `aws-sdk`
|
|
53
|
+
- Keep the deployment package under 5MB zipped
|
|
54
|
+
- Exclude dev dependencies and test files
|
|
55
|
+
|
|
56
|
+
### Event Triggers
|
|
57
|
+
|
|
58
|
+
Lambda functions respond to events from SQS, SNS, S3, EventBridge, DynamoDB
|
|
59
|
+
Streams, and API Gateway. Handle each event type with its own handler pattern.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// SQS batch handler with partial failure reporting
|
|
63
|
+
import type { SQSEvent, SQSBatchResponse } from 'aws-lambda'
|
|
64
|
+
|
|
65
|
+
export async function handler(event: SQSEvent): Promise<SQSBatchResponse> {
|
|
66
|
+
const failures: string[] = []
|
|
67
|
+
|
|
68
|
+
await Promise.allSettled(
|
|
69
|
+
event.Records.map(async (record) => {
|
|
70
|
+
try {
|
|
71
|
+
const body = JSON.parse(record.body)
|
|
72
|
+
await processMessage(body)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`Failed to process ${record.messageId}:`, error)
|
|
75
|
+
failures.push(record.messageId)
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
batchItemFailures: failures.map(id => ({ itemIdentifier: id })),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// S3 trigger
|
|
86
|
+
export async function handleUpload(event: S3Event) {
|
|
87
|
+
for (const record of event.Records) {
|
|
88
|
+
const bucket = record.s3.bucket.name
|
|
89
|
+
const key = decodeURIComponent(record.s3.object.key)
|
|
90
|
+
await processFile(bucket, key)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### DynamoDB Single-Table Design
|
|
96
|
+
|
|
97
|
+
Model multiple entity types in one table using composite keys. Use GSIs for
|
|
98
|
+
access patterns. Avoid scans; every query should hit an index.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Single table: PK and SK encode entity type and ID
|
|
102
|
+
// Entity | PK | SK | GSI1PK | GSI1SK
|
|
103
|
+
// User | USER#123 | PROFILE | EMAIL#a@b.com | USER#123
|
|
104
|
+
// Order | USER#123 | ORDER#456 | ORDER#456 | STATUS#active
|
|
105
|
+
// OrderItem | ORDER#456 | ITEM#789 | — | —
|
|
106
|
+
|
|
107
|
+
interface TableItem {
|
|
108
|
+
PK: string
|
|
109
|
+
SK: string
|
|
110
|
+
GSI1PK?: string
|
|
111
|
+
GSI1SK?: string
|
|
112
|
+
type: string // discriminator
|
|
113
|
+
[key: string]: unknown
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get user by ID
|
|
117
|
+
const user = await ddb.get({ TableName: TABLE, Key: { PK: `USER#${id}`, SK: 'PROFILE' } })
|
|
118
|
+
|
|
119
|
+
// Get all orders for user
|
|
120
|
+
const orders = await ddb.query({
|
|
121
|
+
TableName: TABLE,
|
|
122
|
+
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
|
|
123
|
+
ExpressionAttributeValues: { ':pk': `USER#${userId}`, ':sk': 'ORDER#' },
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Get order by order ID (via GSI)
|
|
127
|
+
const order = await ddb.query({
|
|
128
|
+
TableName: TABLE,
|
|
129
|
+
IndexName: 'GSI1',
|
|
130
|
+
KeyConditionExpression: 'GSI1PK = :pk',
|
|
131
|
+
ExpressionAttributeValues: { ':pk': `ORDER#${orderId}` },
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Step Functions for Orchestration
|
|
136
|
+
|
|
137
|
+
Use AWS Step Functions to orchestrate multi-step workflows. Define states as a
|
|
138
|
+
state machine. Handle retries, parallelism, and error handling declaratively.
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"StartAt": "ValidateInput",
|
|
143
|
+
"States": {
|
|
144
|
+
"ValidateInput": {
|
|
145
|
+
"Type": "Task",
|
|
146
|
+
"Resource": "arn:aws:lambda:us-east-1:123:function:validate",
|
|
147
|
+
"Next": "ProcessParallel",
|
|
148
|
+
"Catch": [{ "ErrorEquals": ["ValidationError"], "Next": "HandleError" }]
|
|
149
|
+
},
|
|
150
|
+
"ProcessParallel": {
|
|
151
|
+
"Type": "Parallel",
|
|
152
|
+
"Branches": [
|
|
153
|
+
{ "StartAt": "GenerateReport", "States": { "GenerateReport": { "Type": "Task", "Resource": "arn:aws:lambda:...:report", "End": true } } },
|
|
154
|
+
{ "StartAt": "SendNotification", "States": { "SendNotification": { "Type": "Task", "Resource": "arn:aws:lambda:...:notify", "End": true } } }
|
|
155
|
+
],
|
|
156
|
+
"Next": "Complete"
|
|
157
|
+
},
|
|
158
|
+
"Complete": { "Type": "Succeed" },
|
|
159
|
+
"HandleError": { "Type": "Fail", "Error": "ProcessingFailed" }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Function Composition Patterns
|
|
165
|
+
|
|
166
|
+
Use SNS/SQS for fan-out, EventBridge for event routing, and Step Functions for
|
|
167
|
+
orchestration. Avoid direct Lambda-to-Lambda invocation.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
API Gateway -> Lambda (validate) -> SQS -> Lambda (process) -> DynamoDB
|
|
171
|
+
\-> Lambda (audit) -> CloudWatch
|
|
172
|
+
|
|
173
|
+
EventBridge Rule: source=orders, detail-type=OrderCreated
|
|
174
|
+
-> Target 1: Lambda (send-confirmation)
|
|
175
|
+
-> Target 2: Lambda (update-analytics)
|
|
176
|
+
-> Target 3: SQS (inventory-queue)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Environment and Configuration
|
|
180
|
+
|
|
181
|
+
Use SSM Parameter Store or Secrets Manager for configuration. Cache values
|
|
182
|
+
in the Lambda container to avoid repeated API calls.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm'
|
|
186
|
+
|
|
187
|
+
const ssm = new SSMClient({})
|
|
188
|
+
const paramCache = new Map<string, { value: string; expiry: number }>()
|
|
189
|
+
|
|
190
|
+
async function getParam(name: string, ttl = 300_000): Promise<string> {
|
|
191
|
+
const cached = paramCache.get(name)
|
|
192
|
+
if (cached && cached.expiry > Date.now()) return cached.value
|
|
193
|
+
|
|
194
|
+
const result = await ssm.send(new GetParameterCommand({ Name: name, WithDecryption: true }))
|
|
195
|
+
const value = result.Parameter!.Value!
|
|
196
|
+
paramCache.set(name, { value, expiry: Date.now() + ttl })
|
|
197
|
+
return value
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Examples
|
|
202
|
+
|
|
203
|
+
**Pattern: Idempotent handler with DynamoDB conditional write**
|
|
204
|
+
```typescript
|
|
205
|
+
await ddb.put({
|
|
206
|
+
TableName: TABLE,
|
|
207
|
+
Item: { PK: `IDEM#${idempotencyKey}`, result, TTL: Math.floor(Date.now() / 1000) + 86400 },
|
|
208
|
+
ConditionExpression: 'attribute_not_exists(PK)',
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Checklist
|
|
213
|
+
|
|
214
|
+
- [ ] SDK clients initialized outside handler (reused across invocations)
|
|
215
|
+
- [ ] Bundle size under 5MB zipped; tree-shaken with esbuild
|
|
216
|
+
- [ ] Provisioned concurrency on latency-sensitive functions
|
|
217
|
+
- [ ] SQS handlers return `batchItemFailures` for partial failure
|
|
218
|
+
- [ ] DynamoDB single-table design with composite keys and GSIs
|
|
219
|
+
- [ ] No table scans; every access pattern uses a key or index
|
|
220
|
+
- [ ] Step Functions for multi-step orchestration (not Lambda-to-Lambda calls)
|
|
221
|
+
- [ ] SSM/Secrets Manager for config, cached in-container with TTL
|
|
222
|
+
- [ ] Idempotency keys on all mutating operations
|
|
223
|
+
- [ ] Dead letter queues on all SQS queues and async Lambda invocations
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sql-optimization
|
|
3
|
+
description: SQL optimization patterns with EXPLAIN ANALYZE, index strategies, query rewriting, CTEs, and window functions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SQL Optimization
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when queries are slow, tables are growing, or you are designing a new schema. SQL optimization starts with measuring (EXPLAIN ANALYZE), then choosing the right index, then restructuring the query. Fix the query first before adding application-level caching.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### EXPLAIN ANALYZE -- Always Measure First
|
|
14
|
+
|
|
15
|
+
Never guess. Run EXPLAIN ANALYZE to see actual execution time and row counts:
|
|
16
|
+
|
|
17
|
+
```sql
|
|
18
|
+
EXPLAIN ANALYZE
|
|
19
|
+
SELECT o.id, o.total, u.email
|
|
20
|
+
FROM orders o
|
|
21
|
+
JOIN users u ON u.id = o.user_id
|
|
22
|
+
WHERE o.status = 'pending'
|
|
23
|
+
AND o.created_at > NOW() - INTERVAL '7 days'
|
|
24
|
+
ORDER BY o.created_at DESC
|
|
25
|
+
LIMIT 20;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Key things to look for:
|
|
29
|
+
- **Seq Scan** on large tables -- needs an index
|
|
30
|
+
- **Rows** actual vs estimated -- stale statistics need `ANALYZE`
|
|
31
|
+
- **Sort** with high cost -- add an index matching the ORDER BY
|
|
32
|
+
- **Nested Loop** with high row count -- consider hash or merge join
|
|
33
|
+
|
|
34
|
+
### Index Strategies
|
|
35
|
+
|
|
36
|
+
```sql
|
|
37
|
+
-- Composite index: leftmost columns must match WHERE clause
|
|
38
|
+
CREATE INDEX idx_orders_status_created ON orders (status, created_at DESC);
|
|
39
|
+
|
|
40
|
+
-- Partial index: only index rows you actually query
|
|
41
|
+
CREATE INDEX idx_orders_pending ON orders (created_at DESC)
|
|
42
|
+
WHERE status = 'pending';
|
|
43
|
+
|
|
44
|
+
-- Covering index: includes all selected columns, avoids table lookup
|
|
45
|
+
CREATE INDEX idx_orders_cover ON orders (status, created_at DESC)
|
|
46
|
+
INCLUDE (total, user_id);
|
|
47
|
+
|
|
48
|
+
-- Expression index: index computed values
|
|
49
|
+
CREATE INDEX idx_users_lower_email ON users (LOWER(email));
|
|
50
|
+
|
|
51
|
+
-- Always use CONCURRENTLY in production
|
|
52
|
+
CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Index rules of thumb:
|
|
56
|
+
- Columns in WHERE, JOIN ON, ORDER BY are index candidates
|
|
57
|
+
- High-cardinality columns first in composite indexes
|
|
58
|
+
- Partial indexes when you filter on a constant value
|
|
59
|
+
- Do not index columns that change frequently
|
|
60
|
+
|
|
61
|
+
### CTEs -- Readable Subquery Composition
|
|
62
|
+
|
|
63
|
+
```sql
|
|
64
|
+
WITH monthly_revenue AS (
|
|
65
|
+
SELECT DATE_TRUNC('month', created_at) AS month,
|
|
66
|
+
SUM(total) AS revenue
|
|
67
|
+
FROM orders
|
|
68
|
+
WHERE status = 'paid'
|
|
69
|
+
GROUP BY 1
|
|
70
|
+
),
|
|
71
|
+
growth AS (
|
|
72
|
+
SELECT month,
|
|
73
|
+
revenue,
|
|
74
|
+
LAG(revenue) OVER (ORDER BY month) AS prev_revenue
|
|
75
|
+
FROM monthly_revenue
|
|
76
|
+
)
|
|
77
|
+
SELECT month,
|
|
78
|
+
revenue,
|
|
79
|
+
ROUND((revenue - prev_revenue) / NULLIF(prev_revenue, 0) * 100, 1) AS growth_pct
|
|
80
|
+
FROM growth
|
|
81
|
+
WHERE prev_revenue IS NOT NULL
|
|
82
|
+
ORDER BY month;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
In PostgreSQL 12+, CTEs are inlined by default. Use `MATERIALIZED` only when you need to force a single evaluation.
|
|
86
|
+
|
|
87
|
+
### Window Functions -- Avoid Self-Joins
|
|
88
|
+
|
|
89
|
+
```sql
|
|
90
|
+
-- Rank users by order count
|
|
91
|
+
SELECT user_id,
|
|
92
|
+
COUNT(*) AS order_count,
|
|
93
|
+
RANK() OVER (ORDER BY COUNT(*) DESC) AS rank
|
|
94
|
+
FROM orders
|
|
95
|
+
GROUP BY user_id;
|
|
96
|
+
|
|
97
|
+
-- Running total
|
|
98
|
+
SELECT id, amount,
|
|
99
|
+
SUM(amount) OVER (ORDER BY created_at ROWS UNBOUNDED PRECEDING) AS running_total
|
|
100
|
+
FROM transactions;
|
|
101
|
+
|
|
102
|
+
-- Get previous and next values without self-join
|
|
103
|
+
SELECT id, amount,
|
|
104
|
+
LAG(amount) OVER (ORDER BY created_at) AS prev_amount,
|
|
105
|
+
LEAD(amount) OVER (ORDER BY created_at) AS next_amount
|
|
106
|
+
FROM transactions;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Partitioning -- Scale Large Tables
|
|
110
|
+
|
|
111
|
+
```sql
|
|
112
|
+
CREATE TABLE events (
|
|
113
|
+
id BIGSERIAL,
|
|
114
|
+
event_type TEXT NOT NULL,
|
|
115
|
+
payload JSONB,
|
|
116
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
117
|
+
) PARTITION BY RANGE (created_at);
|
|
118
|
+
|
|
119
|
+
CREATE TABLE events_2026_01 PARTITION OF events
|
|
120
|
+
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
|
|
121
|
+
CREATE TABLE events_2026_02 PARTITION OF events
|
|
122
|
+
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Always include the partition key in WHERE clauses for partition pruning.
|
|
126
|
+
|
|
127
|
+
### Common Anti-Patterns
|
|
128
|
+
|
|
129
|
+
| Anti-Pattern | Fix |
|
|
130
|
+
|-------------|-----|
|
|
131
|
+
| `SELECT *` | List only needed columns |
|
|
132
|
+
| `WHERE function(col) = value` | Use expression index or rewrite |
|
|
133
|
+
| `LIKE '%term%'` | Use `pg_trgm` GIN index or full-text search |
|
|
134
|
+
| `NOT IN (subquery)` with NULLs | Use `NOT EXISTS` instead |
|
|
135
|
+
| Missing `LIMIT` on dashboards | Always paginate (keyset preferred) |
|
|
136
|
+
| `ORDER BY RANDOM()` | Use `TABLESAMPLE` or precomputed column |
|
|
137
|
+
|
|
138
|
+
## Examples
|
|
139
|
+
|
|
140
|
+
| Scenario | Before | After |
|
|
141
|
+
|----------|--------|-------|
|
|
142
|
+
| Filter + sort | Seq Scan + Sort (2.3s) | Index Scan (4ms) |
|
|
143
|
+
| Aggregate + rank | Self-join (800ms) | Window function (50ms) |
|
|
144
|
+
| Time-range on 500M rows | Full scan (45s) | Partition pruning (200ms) |
|
|
145
|
+
|
|
146
|
+
## Checklist
|
|
147
|
+
- [ ] `EXPLAIN ANALYZE` run on every query touching > 10K rows
|
|
148
|
+
- [ ] All foreign keys have indexes
|
|
149
|
+
- [ ] Composite indexes match common WHERE + ORDER BY patterns
|
|
150
|
+
- [ ] Partial indexes exist for status-filtered queries
|
|
151
|
+
- [ ] No `SELECT *` in application code
|
|
152
|
+
- [ ] Window functions replace self-joins and correlated subqueries
|
|
153
|
+
- [ ] Tables over 100M rows are partitioned
|
|
154
|
+
- [ ] `CREATE INDEX` always uses `CONCURRENTLY` in production
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: state-management
|
|
3
|
+
description: Manage React state effectively — Zustand for client, TanStack Query for server, URL state, and optimistic updates.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# State Management
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Choose the right state tool for the job. Most React apps misuse global state
|
|
11
|
+
stores for data that belongs in the URL, the server, or a local component.
|
|
12
|
+
The rule: keep state as close as possible to where it's used, and use the
|
|
13
|
+
simplest tool that works.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### 1. Decision Framework
|
|
18
|
+
|
|
19
|
+
| State type | Where it lives | Tool |
|
|
20
|
+
|-----------|----------------|------|
|
|
21
|
+
| Component UI (open/closed, hover) | Component `useState` | React |
|
|
22
|
+
| Form input | Component `useState` or form library | React / react-hook-form |
|
|
23
|
+
| URL state (filters, pagination, tabs) | URL search params | `useSearchParams` |
|
|
24
|
+
| Client-only global (theme, sidebar) | Global store | Zustand |
|
|
25
|
+
| Server data (users, orders) | Server cache | TanStack Query |
|
|
26
|
+
| Auth session | Context + server | Context + cookies |
|
|
27
|
+
|
|
28
|
+
### 2. Zustand for Client State
|
|
29
|
+
|
|
30
|
+
Minimal, no boilerplate, works outside React components.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { create } from 'zustand';
|
|
34
|
+
import { persist } from 'zustand/middleware';
|
|
35
|
+
|
|
36
|
+
interface UIStore {
|
|
37
|
+
sidebarOpen: boolean;
|
|
38
|
+
toggleSidebar: () => void;
|
|
39
|
+
theme: 'light' | 'dark';
|
|
40
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useUIStore = create<UIStore>()(
|
|
44
|
+
persist(
|
|
45
|
+
(set) => ({
|
|
46
|
+
sidebarOpen: true,
|
|
47
|
+
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
|
48
|
+
theme: 'light',
|
|
49
|
+
setTheme: (theme) => set({ theme }),
|
|
50
|
+
}),
|
|
51
|
+
{ name: 'ui-store' }, // persists to localStorage
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Usage in components:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
function Sidebar() {
|
|
60
|
+
const open = useUIStore((s) => s.sidebarOpen); // only re-renders on this slice
|
|
61
|
+
if (!open) return null;
|
|
62
|
+
return <nav>...</nav>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ThemeToggle() {
|
|
66
|
+
const [theme, setTheme] = useUIStore((s) => [s.theme, s.setTheme]);
|
|
67
|
+
return (
|
|
68
|
+
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
69
|
+
{theme === 'light' ? 'Dark mode' : 'Light mode'}
|
|
70
|
+
</button>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Select individual fields to avoid unnecessary re-renders.
|
|
76
|
+
|
|
77
|
+
### 3. React Context — When Zustand is Overkill
|
|
78
|
+
|
|
79
|
+
For state scoped to a subtree, not the whole app.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
interface ModalContextValue {
|
|
83
|
+
isOpen: boolean;
|
|
84
|
+
open: (content: ReactNode) => void;
|
|
85
|
+
close: () => void;
|
|
86
|
+
content: ReactNode | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ModalContext = createContext<ModalContextValue | null>(null);
|
|
90
|
+
|
|
91
|
+
export function ModalProvider({ children }: { children: ReactNode }) {
|
|
92
|
+
const [state, setState] = useState<{ isOpen: boolean; content: ReactNode | null }>({
|
|
93
|
+
isOpen: false,
|
|
94
|
+
content: null,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const value = useMemo(() => ({
|
|
98
|
+
...state,
|
|
99
|
+
open: (content: ReactNode) => setState({ isOpen: true, content }),
|
|
100
|
+
close: () => setState({ isOpen: false, content: null }),
|
|
101
|
+
}), [state]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ModalContext.Provider value={value}>
|
|
105
|
+
{children}
|
|
106
|
+
{state.isOpen && <ModalOverlay>{state.content}</ModalOverlay>}
|
|
107
|
+
</ModalContext.Provider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useModal() {
|
|
112
|
+
const ctx = useContext(ModalContext);
|
|
113
|
+
if (!ctx) throw new Error('useModal must be used within ModalProvider');
|
|
114
|
+
return ctx;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 4. URL State for Shareable UI
|
|
119
|
+
|
|
120
|
+
Filters, pagination, search, and tabs should live in the URL so users can
|
|
121
|
+
bookmark and share.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { useSearchParams } from 'react-router-dom';
|
|
125
|
+
|
|
126
|
+
function OrderList() {
|
|
127
|
+
const [params, setParams] = useSearchParams();
|
|
128
|
+
const status = params.get('status') ?? 'all';
|
|
129
|
+
const page = Number(params.get('page') ?? '1');
|
|
130
|
+
const search = params.get('q') ?? '';
|
|
131
|
+
|
|
132
|
+
function setFilter(key: string, value: string) {
|
|
133
|
+
setParams((prev) => {
|
|
134
|
+
const next = new URLSearchParams(prev);
|
|
135
|
+
if (value) next.set(key, value);
|
|
136
|
+
else next.delete(key);
|
|
137
|
+
next.set('page', '1'); // reset to page 1 on filter change
|
|
138
|
+
return next;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<FilterBar
|
|
145
|
+
status={status}
|
|
146
|
+
onStatusChange={(v) => setFilter('status', v)}
|
|
147
|
+
search={search}
|
|
148
|
+
onSearchChange={(v) => setFilter('q', v)}
|
|
149
|
+
/>
|
|
150
|
+
<OrderTable status={status} page={page} search={search} />
|
|
151
|
+
<Pagination page={page} onChange={(p) => setFilter('page', String(p))} />
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5. TanStack Query for Server State
|
|
158
|
+
|
|
159
|
+
Server data is not client state. It has its own lifecycle: loading, fresh,
|
|
160
|
+
stale, error, background refresh.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
164
|
+
|
|
165
|
+
// Fetch
|
|
166
|
+
function useOrders(filters: OrderFilters) {
|
|
167
|
+
return useQuery({
|
|
168
|
+
queryKey: ['orders', filters],
|
|
169
|
+
queryFn: () => api.orders.list(filters),
|
|
170
|
+
staleTime: 30_000, // fresh for 30s
|
|
171
|
+
gcTime: 5 * 60_000, // keep in cache 5min
|
|
172
|
+
placeholderData: (prev) => prev, // show old data while refetching
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Mutate with cache invalidation
|
|
177
|
+
function useDeleteOrder() {
|
|
178
|
+
const queryClient = useQueryClient();
|
|
179
|
+
|
|
180
|
+
return useMutation({
|
|
181
|
+
mutationFn: (id: string) => api.orders.delete(id),
|
|
182
|
+
onSuccess: () => {
|
|
183
|
+
queryClient.invalidateQueries({ queryKey: ['orders'] });
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 6. Optimistic Updates
|
|
190
|
+
|
|
191
|
+
Update the UI immediately, roll back if the server rejects.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
function useToggleFavorite() {
|
|
195
|
+
const queryClient = useQueryClient();
|
|
196
|
+
|
|
197
|
+
return useMutation({
|
|
198
|
+
mutationFn: (id: string) => api.favorites.toggle(id),
|
|
199
|
+
onMutate: async (id) => {
|
|
200
|
+
await queryClient.cancelQueries({ queryKey: ['favorites'] });
|
|
201
|
+
|
|
202
|
+
const previous = queryClient.getQueryData<string[]>(['favorites']);
|
|
203
|
+
|
|
204
|
+
queryClient.setQueryData<string[]>(['favorites'], (old = []) =>
|
|
205
|
+
old.includes(id) ? old.filter((x) => x !== id) : [...old, id],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return { previous };
|
|
209
|
+
},
|
|
210
|
+
onError: (_err, _id, context) => {
|
|
211
|
+
// Roll back on failure
|
|
212
|
+
queryClient.setQueryData(['favorites'], context?.previous);
|
|
213
|
+
},
|
|
214
|
+
onSettled: () => {
|
|
215
|
+
queryClient.invalidateQueries({ queryKey: ['favorites'] });
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 7. Derived State — Compute, Don't Store
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
// Bad — derived state stored separately
|
|
225
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
226
|
+
const [total, setTotal] = useState(0);
|
|
227
|
+
// total drifts if you forget to update it
|
|
228
|
+
|
|
229
|
+
// Good — compute from source
|
|
230
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
231
|
+
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Examples
|
|
235
|
+
|
|
236
|
+
| Scenario | Approach |
|
|
237
|
+
|----------|----------|
|
|
238
|
+
| Dark mode toggle | Zustand with `persist` middleware |
|
|
239
|
+
| Data table with filters | URL search params + TanStack Query |
|
|
240
|
+
| Shopping cart | Zustand (client state with persistence) |
|
|
241
|
+
| User profile data | TanStack Query (`useQuery`) |
|
|
242
|
+
| Like button | Optimistic mutation (`useMutation` + `onMutate`) |
|
|
243
|
+
| Modal open/close | Local `useState` or Context |
|
|
244
|
+
|
|
245
|
+
## Checklist
|
|
246
|
+
|
|
247
|
+
- [ ] Server data uses TanStack Query, not `useState` + `useEffect`
|
|
248
|
+
- [ ] Filters, pagination, and search live in URL params
|
|
249
|
+
- [ ] Zustand selectors pick individual fields to minimize re-renders
|
|
250
|
+
- [ ] No derived state is stored — it's computed with `useMemo`
|
|
251
|
+
- [ ] Optimistic updates include rollback logic in `onError`
|
|
252
|
+
- [ ] Context is used for subtree state (modals, forms), Zustand for app-wide
|
|
253
|
+
- [ ] `staleTime` is configured per query to avoid unnecessary refetches
|
|
254
|
+
- [ ] Loading, error, and empty states are handled for every server query
|