@girardmedia/bootspring 1.2.0 → 2.0.3
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/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
# Rate Limiting Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for implementing API rate limiting.
|
|
4
|
-
|
|
5
|
-
## In-Memory Rate Limiter
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/rate-limit.ts
|
|
9
|
-
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
|
|
10
|
-
|
|
11
|
-
export function rateLimit(
|
|
12
|
-
key: string,
|
|
13
|
-
limit: number,
|
|
14
|
-
windowMs: number
|
|
15
|
-
): { success: boolean; remaining: number; resetIn: number } {
|
|
16
|
-
const now = Date.now()
|
|
17
|
-
const record = rateLimitMap.get(key)
|
|
18
|
-
|
|
19
|
-
if (!record || now > record.resetTime) {
|
|
20
|
-
rateLimitMap.set(key, { count: 1, resetTime: now + windowMs })
|
|
21
|
-
return { success: true, remaining: limit - 1, resetIn: windowMs }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (record.count >= limit) {
|
|
25
|
-
return {
|
|
26
|
-
success: false,
|
|
27
|
-
remaining: 0,
|
|
28
|
-
resetIn: record.resetTime - now
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
record.count++
|
|
33
|
-
return {
|
|
34
|
-
success: true,
|
|
35
|
-
remaining: limit - record.count,
|
|
36
|
-
resetIn: record.resetTime - now
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Redis Rate Limiter
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
// lib/rate-limit-redis.ts
|
|
45
|
-
import { Redis } from '@upstash/redis'
|
|
46
|
-
|
|
47
|
-
const redis = new Redis({
|
|
48
|
-
url: process.env.UPSTASH_REDIS_URL!,
|
|
49
|
-
token: process.env.UPSTASH_REDIS_TOKEN!
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
export async function rateLimitRedis(
|
|
53
|
-
key: string,
|
|
54
|
-
limit: number,
|
|
55
|
-
windowSeconds: number
|
|
56
|
-
) {
|
|
57
|
-
const current = await redis.incr(key)
|
|
58
|
-
|
|
59
|
-
if (current === 1) {
|
|
60
|
-
await redis.expire(key, windowSeconds)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const ttl = await redis.ttl(key)
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
success: current <= limit,
|
|
67
|
-
remaining: Math.max(0, limit - current),
|
|
68
|
-
resetIn: ttl > 0 ? ttl * 1000 : windowSeconds * 1000
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Sliding Window Rate Limiter
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// lib/rate-limit-sliding.ts
|
|
77
|
-
import { Redis } from '@upstash/redis'
|
|
78
|
-
|
|
79
|
-
const redis = new Redis({
|
|
80
|
-
url: process.env.UPSTASH_REDIS_URL!,
|
|
81
|
-
token: process.env.UPSTASH_REDIS_TOKEN!
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
export async function slidingWindowRateLimit(
|
|
85
|
-
key: string,
|
|
86
|
-
limit: number,
|
|
87
|
-
windowMs: number
|
|
88
|
-
) {
|
|
89
|
-
const now = Date.now()
|
|
90
|
-
const windowStart = now - windowMs
|
|
91
|
-
|
|
92
|
-
// Remove old entries and add new one
|
|
93
|
-
const pipeline = redis.pipeline()
|
|
94
|
-
pipeline.zremrangebyscore(key, 0, windowStart)
|
|
95
|
-
pipeline.zadd(key, { score: now, member: `${now}-${Math.random()}` })
|
|
96
|
-
pipeline.zcard(key)
|
|
97
|
-
pipeline.expire(key, Math.ceil(windowMs / 1000))
|
|
98
|
-
|
|
99
|
-
const results = await pipeline.exec()
|
|
100
|
-
const count = results[2] as number
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
success: count <= limit,
|
|
104
|
-
remaining: Math.max(0, limit - count),
|
|
105
|
-
resetIn: windowMs
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Rate Limit Middleware
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
// middleware.ts
|
|
114
|
-
import { NextResponse } from 'next/server'
|
|
115
|
-
import type { NextRequest } from 'next/server'
|
|
116
|
-
import { rateLimitRedis } from '@/lib/rate-limit-redis'
|
|
117
|
-
|
|
118
|
-
const RATE_LIMITS = {
|
|
119
|
-
'/api/': { limit: 100, window: 60 }, // 100 req/min
|
|
120
|
-
'/api/auth/': { limit: 10, window: 60 }, // 10 req/min
|
|
121
|
-
'/api/ai/': { limit: 20, window: 60 } // 20 req/min
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function middleware(request: NextRequest) {
|
|
125
|
-
const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'unknown'
|
|
126
|
-
const path = request.nextUrl.pathname
|
|
127
|
-
|
|
128
|
-
// Find matching rate limit
|
|
129
|
-
let config = { limit: 100, window: 60 }
|
|
130
|
-
for (const [prefix, limits] of Object.entries(RATE_LIMITS)) {
|
|
131
|
-
if (path.startsWith(prefix)) {
|
|
132
|
-
config = limits
|
|
133
|
-
break
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const key = `rate-limit:${ip}:${path}`
|
|
138
|
-
const result = await rateLimitRedis(key, config.limit, config.window)
|
|
139
|
-
|
|
140
|
-
if (!result.success) {
|
|
141
|
-
return NextResponse.json(
|
|
142
|
-
{ error: 'Too many requests' },
|
|
143
|
-
{
|
|
144
|
-
status: 429,
|
|
145
|
-
headers: {
|
|
146
|
-
'X-RateLimit-Limit': config.limit.toString(),
|
|
147
|
-
'X-RateLimit-Remaining': '0',
|
|
148
|
-
'X-RateLimit-Reset': Math.ceil(Date.now() / 1000 + result.resetIn / 1000).toString(),
|
|
149
|
-
'Retry-After': Math.ceil(result.resetIn / 1000).toString()
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const response = NextResponse.next()
|
|
156
|
-
response.headers.set('X-RateLimit-Limit', config.limit.toString())
|
|
157
|
-
response.headers.set('X-RateLimit-Remaining', result.remaining.toString())
|
|
158
|
-
|
|
159
|
-
return response
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export const config = {
|
|
163
|
-
matcher: '/api/:path*'
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Per-User Rate Limiting
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
// lib/rate-limit-user.ts
|
|
171
|
-
import { auth } from '@/auth'
|
|
172
|
-
|
|
173
|
-
export async function userRateLimit(baseLimit: number) {
|
|
174
|
-
const session = await auth()
|
|
175
|
-
const userId = session?.user?.id
|
|
176
|
-
|
|
177
|
-
// Higher limits for authenticated users
|
|
178
|
-
const limit = userId ? baseLimit * 2 : baseLimit
|
|
179
|
-
|
|
180
|
-
// Use user ID or IP as key
|
|
181
|
-
const key = userId ?? (await getClientIP())
|
|
182
|
-
|
|
183
|
-
return rateLimitRedis(`user:${key}`, limit, 60)
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## Token Bucket Algorithm
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
// lib/token-bucket.ts
|
|
191
|
-
interface Bucket {
|
|
192
|
-
tokens: number
|
|
193
|
-
lastRefill: number
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const buckets = new Map<string, Bucket>()
|
|
197
|
-
|
|
198
|
-
export function tokenBucket(
|
|
199
|
-
key: string,
|
|
200
|
-
maxTokens: number,
|
|
201
|
-
refillRate: number // tokens per second
|
|
202
|
-
): boolean {
|
|
203
|
-
const now = Date.now()
|
|
204
|
-
let bucket = buckets.get(key)
|
|
205
|
-
|
|
206
|
-
if (!bucket) {
|
|
207
|
-
bucket = { tokens: maxTokens, lastRefill: now }
|
|
208
|
-
buckets.set(key, bucket)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Refill tokens
|
|
212
|
-
const timePassed = (now - bucket.lastRefill) / 1000
|
|
213
|
-
bucket.tokens = Math.min(maxTokens, bucket.tokens + timePassed * refillRate)
|
|
214
|
-
bucket.lastRefill = now
|
|
215
|
-
|
|
216
|
-
// Try to consume a token
|
|
217
|
-
if (bucket.tokens >= 1) {
|
|
218
|
-
bucket.tokens -= 1
|
|
219
|
-
return true
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return false
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## When to Use
|
|
227
|
-
|
|
228
|
-
- API protection
|
|
229
|
-
- DDoS mitigation
|
|
230
|
-
- Fair usage enforcement
|
|
231
|
-
- Cost control
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
# Next.js Route Handler Patterns
|
|
2
|
-
|
|
3
|
-
Battle-tested patterns for API routes in Next.js App Router.
|
|
4
|
-
|
|
5
|
-
## Basic CRUD Route Handler
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// app/api/posts/route.ts
|
|
9
|
-
import { prisma } from '@/lib/db'
|
|
10
|
-
import { auth } from '@/lib/auth'
|
|
11
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
12
|
-
import { z } from 'zod'
|
|
13
|
-
|
|
14
|
-
const CreatePostSchema = z.object({
|
|
15
|
-
title: z.string().min(1).max(200),
|
|
16
|
-
content: z.string().optional(),
|
|
17
|
-
published: z.boolean().default(false)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
// GET /api/posts
|
|
21
|
-
export async function GET(request: NextRequest) {
|
|
22
|
-
const searchParams = request.nextUrl.searchParams
|
|
23
|
-
const page = parseInt(searchParams.get('page') ?? '1')
|
|
24
|
-
const limit = parseInt(searchParams.get('limit') ?? '10')
|
|
25
|
-
|
|
26
|
-
const posts = await prisma.post.findMany({
|
|
27
|
-
where: { published: true },
|
|
28
|
-
orderBy: { createdAt: 'desc' },
|
|
29
|
-
skip: (page - 1) * limit,
|
|
30
|
-
take: limit,
|
|
31
|
-
include: { author: { select: { name: true } } }
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
return NextResponse.json({ posts, page, limit })
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// POST /api/posts
|
|
38
|
-
export async function POST(request: NextRequest) {
|
|
39
|
-
try {
|
|
40
|
-
const { userId } = await auth()
|
|
41
|
-
if (!userId) {
|
|
42
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const body = await request.json()
|
|
46
|
-
const validated = CreatePostSchema.parse(body)
|
|
47
|
-
|
|
48
|
-
const post = await prisma.post.create({
|
|
49
|
-
data: { ...validated, authorId: userId }
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return NextResponse.json(post, { status: 201 })
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (error instanceof z.ZodError) {
|
|
55
|
-
return NextResponse.json({ error: error.errors }, { status: 400 })
|
|
56
|
-
}
|
|
57
|
-
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Dynamic Route Handler
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// app/api/posts/[id]/route.ts
|
|
66
|
-
import { prisma } from '@/lib/db'
|
|
67
|
-
import { auth } from '@/lib/auth'
|
|
68
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
69
|
-
|
|
70
|
-
type Params = { params: Promise<{ id: string }> }
|
|
71
|
-
|
|
72
|
-
// GET /api/posts/:id
|
|
73
|
-
export async function GET(request: NextRequest, { params }: Params) {
|
|
74
|
-
const { id } = await params
|
|
75
|
-
|
|
76
|
-
const post = await prisma.post.findUnique({
|
|
77
|
-
where: { id },
|
|
78
|
-
include: { author: { select: { name: true, email: true } } }
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
if (!post) {
|
|
82
|
-
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return NextResponse.json(post)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// PATCH /api/posts/:id
|
|
89
|
-
export async function PATCH(request: NextRequest, { params }: Params) {
|
|
90
|
-
const { userId } = await auth()
|
|
91
|
-
if (!userId) {
|
|
92
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const { id } = await params
|
|
96
|
-
const body = await request.json()
|
|
97
|
-
|
|
98
|
-
const post = await prisma.post.findUnique({ where: { id } })
|
|
99
|
-
if (!post) {
|
|
100
|
-
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
101
|
-
}
|
|
102
|
-
if (post.authorId !== userId) {
|
|
103
|
-
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const updated = await prisma.post.update({
|
|
107
|
-
where: { id },
|
|
108
|
-
data: body
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
return NextResponse.json(updated)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// DELETE /api/posts/:id
|
|
115
|
-
export async function DELETE(request: NextRequest, { params }: Params) {
|
|
116
|
-
const { userId } = await auth()
|
|
117
|
-
if (!userId) {
|
|
118
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const { id } = await params
|
|
122
|
-
|
|
123
|
-
const post = await prisma.post.findUnique({ where: { id } })
|
|
124
|
-
if (!post || post.authorId !== userId) {
|
|
125
|
-
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
await prisma.post.delete({ where: { id } })
|
|
129
|
-
|
|
130
|
-
return new NextResponse(null, { status: 204 })
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Error Handling Wrapper
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
137
|
-
// lib/api-utils.ts
|
|
138
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
139
|
-
import { ZodError } from 'zod'
|
|
140
|
-
|
|
141
|
-
type Handler = (req: NextRequest, context?: any) => Promise<NextResponse>
|
|
142
|
-
|
|
143
|
-
export function withErrorHandling(handler: Handler): Handler {
|
|
144
|
-
return async (req, context) => {
|
|
145
|
-
try {
|
|
146
|
-
return await handler(req, context)
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.error('API Error:', error)
|
|
149
|
-
|
|
150
|
-
if (error instanceof ZodError) {
|
|
151
|
-
return NextResponse.json(
|
|
152
|
-
{ error: 'Validation failed', details: error.errors },
|
|
153
|
-
{ status: 400 }
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (error instanceof Error) {
|
|
158
|
-
if (error.message === 'UNAUTHORIZED') {
|
|
159
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
160
|
-
}
|
|
161
|
-
if (error.message === 'NOT_FOUND') {
|
|
162
|
-
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return NextResponse.json(
|
|
167
|
-
{ error: 'Internal server error' },
|
|
168
|
-
{ status: 500 }
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Rate Limiting Pattern
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
// lib/rate-limit.ts
|
|
179
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
180
|
-
|
|
181
|
-
const rateLimit = new Map<string, { count: number; resetTime: number }>()
|
|
182
|
-
|
|
183
|
-
export function checkRateLimit(
|
|
184
|
-
identifier: string,
|
|
185
|
-
limit: number = 10,
|
|
186
|
-
windowMs: number = 60000
|
|
187
|
-
): boolean {
|
|
188
|
-
const now = Date.now()
|
|
189
|
-
const record = rateLimit.get(identifier)
|
|
190
|
-
|
|
191
|
-
if (!record || now > record.resetTime) {
|
|
192
|
-
rateLimit.set(identifier, { count: 1, resetTime: now + windowMs })
|
|
193
|
-
return true
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (record.count >= limit) {
|
|
197
|
-
return false
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
record.count++
|
|
201
|
-
return true
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Usage in route
|
|
205
|
-
export async function POST(request: NextRequest) {
|
|
206
|
-
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
|
|
207
|
-
|
|
208
|
-
if (!checkRateLimit(ip, 10, 60000)) {
|
|
209
|
-
return NextResponse.json(
|
|
210
|
-
{ error: 'Too many requests' },
|
|
211
|
-
{ status: 429 }
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Continue with handler...
|
|
216
|
-
}
|
|
217
|
-
```
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
# Next.js Server Action Patterns
|
|
2
|
-
|
|
3
|
-
Battle-tested patterns for Server Actions in Next.js App Router.
|
|
4
|
-
|
|
5
|
-
## Basic Server Action
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// actions/posts.ts
|
|
9
|
-
'use server'
|
|
10
|
-
|
|
11
|
-
import { prisma } from '@/lib/db'
|
|
12
|
-
import { auth } from '@/lib/auth'
|
|
13
|
-
import { revalidatePath } from 'next/cache'
|
|
14
|
-
import { redirect } from 'next/navigation'
|
|
15
|
-
import { z } from 'zod'
|
|
16
|
-
|
|
17
|
-
const CreatePostSchema = z.object({
|
|
18
|
-
title: z.string().min(1, 'Title is required').max(200),
|
|
19
|
-
content: z.string().optional()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
export async function createPost(formData: FormData) {
|
|
23
|
-
const { userId } = await auth()
|
|
24
|
-
if (!userId) throw new Error('UNAUTHORIZED')
|
|
25
|
-
|
|
26
|
-
const validated = CreatePostSchema.parse({
|
|
27
|
-
title: formData.get('title'),
|
|
28
|
-
content: formData.get('content')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const post = await prisma.post.create({
|
|
32
|
-
data: { ...validated, authorId: userId }
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
revalidatePath('/posts')
|
|
36
|
-
redirect(`/posts/${post.id}`)
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Server Action with Return Value
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
// actions/posts.ts
|
|
44
|
-
'use server'
|
|
45
|
-
|
|
46
|
-
type ActionResult<T> =
|
|
47
|
-
| { success: true; data: T }
|
|
48
|
-
| { success: false; error: string }
|
|
49
|
-
|
|
50
|
-
export async function updatePost(
|
|
51
|
-
postId: string,
|
|
52
|
-
formData: FormData
|
|
53
|
-
): Promise<ActionResult<{ id: string }>> {
|
|
54
|
-
try {
|
|
55
|
-
const { userId } = await auth()
|
|
56
|
-
if (!userId) {
|
|
57
|
-
return { success: false, error: 'Not authenticated' }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const post = await prisma.post.findUnique({ where: { id: postId } })
|
|
61
|
-
if (!post || post.authorId !== userId) {
|
|
62
|
-
return { success: false, error: 'Not authorized' }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const title = formData.get('title') as string
|
|
66
|
-
if (!title || title.length < 1) {
|
|
67
|
-
return { success: false, error: 'Title is required' }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const updated = await prisma.post.update({
|
|
71
|
-
where: { id: postId },
|
|
72
|
-
data: { title }
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
revalidatePath(`/posts/${postId}`)
|
|
76
|
-
return { success: true, data: { id: updated.id } }
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error('Update post error:', error)
|
|
79
|
-
return { success: false, error: 'Failed to update post' }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Using with useActionState (React 19)
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
// components/create-post-form.tsx
|
|
88
|
-
'use client'
|
|
89
|
-
|
|
90
|
-
import { useActionState } from 'react'
|
|
91
|
-
import { createPost } from '@/actions/posts'
|
|
92
|
-
|
|
93
|
-
const initialState = { error: null as string | null }
|
|
94
|
-
|
|
95
|
-
export function CreatePostForm() {
|
|
96
|
-
const [state, formAction, isPending] = useActionState(
|
|
97
|
-
async (prevState: typeof initialState, formData: FormData) => {
|
|
98
|
-
const result = await createPost(formData)
|
|
99
|
-
if (!result.success) {
|
|
100
|
-
return { error: result.error }
|
|
101
|
-
}
|
|
102
|
-
return { error: null }
|
|
103
|
-
},
|
|
104
|
-
initialState
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<form action={formAction}>
|
|
109
|
-
{state.error && (
|
|
110
|
-
<div className="text-red-500 mb-4">{state.error}</div>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
<input
|
|
114
|
-
name="title"
|
|
115
|
-
placeholder="Post title"
|
|
116
|
-
required
|
|
117
|
-
disabled={isPending}
|
|
118
|
-
className="w-full p-2 border rounded"
|
|
119
|
-
/>
|
|
120
|
-
|
|
121
|
-
<textarea
|
|
122
|
-
name="content"
|
|
123
|
-
placeholder="Content (optional)"
|
|
124
|
-
disabled={isPending}
|
|
125
|
-
className="w-full p-2 border rounded mt-2"
|
|
126
|
-
/>
|
|
127
|
-
|
|
128
|
-
<button
|
|
129
|
-
type="submit"
|
|
130
|
-
disabled={isPending}
|
|
131
|
-
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
|
|
132
|
-
>
|
|
133
|
-
{isPending ? 'Creating...' : 'Create Post'}
|
|
134
|
-
</button>
|
|
135
|
-
</form>
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Delete Action with Confirmation
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// actions/posts.ts
|
|
144
|
-
'use server'
|
|
145
|
-
|
|
146
|
-
export async function deletePost(postId: string): Promise<ActionResult<null>> {
|
|
147
|
-
const { userId } = await auth()
|
|
148
|
-
if (!userId) {
|
|
149
|
-
return { success: false, error: 'Not authenticated' }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const post = await prisma.post.findUnique({ where: { id: postId } })
|
|
153
|
-
if (!post || post.authorId !== userId) {
|
|
154
|
-
return { success: false, error: 'Not authorized' }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
await prisma.post.delete({ where: { id: postId } })
|
|
158
|
-
|
|
159
|
-
revalidatePath('/posts')
|
|
160
|
-
return { success: true, data: null }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// components/delete-button.tsx
|
|
164
|
-
'use client'
|
|
165
|
-
|
|
166
|
-
import { deletePost } from '@/actions/posts'
|
|
167
|
-
import { useTransition } from 'react'
|
|
168
|
-
import { useRouter } from 'next/navigation'
|
|
169
|
-
|
|
170
|
-
export function DeleteButton({ postId }: { postId: string }) {
|
|
171
|
-
const [isPending, startTransition] = useTransition()
|
|
172
|
-
const router = useRouter()
|
|
173
|
-
|
|
174
|
-
const handleDelete = () => {
|
|
175
|
-
if (!confirm('Are you sure you want to delete this post?')) return
|
|
176
|
-
|
|
177
|
-
startTransition(async () => {
|
|
178
|
-
const result = await deletePost(postId)
|
|
179
|
-
if (result.success) {
|
|
180
|
-
router.push('/posts')
|
|
181
|
-
} else {
|
|
182
|
-
alert(result.error)
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<button
|
|
189
|
-
onClick={handleDelete}
|
|
190
|
-
disabled={isPending}
|
|
191
|
-
className="text-red-500 disabled:opacity-50"
|
|
192
|
-
>
|
|
193
|
-
{isPending ? 'Deleting...' : 'Delete'}
|
|
194
|
-
</button>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Optimistic Updates
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// components/like-button.tsx
|
|
203
|
-
'use client'
|
|
204
|
-
|
|
205
|
-
import { useOptimistic, useTransition } from 'react'
|
|
206
|
-
import { toggleLike } from '@/actions/posts'
|
|
207
|
-
|
|
208
|
-
export function LikeButton({ postId, initialLiked, initialCount }: {
|
|
209
|
-
postId: string
|
|
210
|
-
initialLiked: boolean
|
|
211
|
-
initialCount: number
|
|
212
|
-
}) {
|
|
213
|
-
const [isPending, startTransition] = useTransition()
|
|
214
|
-
const [optimistic, setOptimistic] = useOptimistic(
|
|
215
|
-
{ liked: initialLiked, count: initialCount },
|
|
216
|
-
(state, newLiked: boolean) => ({
|
|
217
|
-
liked: newLiked,
|
|
218
|
-
count: newLiked ? state.count + 1 : state.count - 1
|
|
219
|
-
})
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
const handleClick = () => {
|
|
223
|
-
startTransition(async () => {
|
|
224
|
-
setOptimistic(!optimistic.liked)
|
|
225
|
-
await toggleLike(postId)
|
|
226
|
-
})
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<button onClick={handleClick} disabled={isPending}>
|
|
231
|
-
{optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
|
|
232
|
-
</button>
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## When to Use Server Actions vs Route Handlers
|
|
238
|
-
|
|
239
|
-
**Use Server Actions for:**
|
|
240
|
-
- Form submissions
|
|
241
|
-
- Mutations from client components
|
|
242
|
-
- Actions that need revalidation
|
|
243
|
-
- Progressive enhancement (works without JS)
|
|
244
|
-
|
|
245
|
-
**Use Route Handlers for:**
|
|
246
|
-
- Webhooks from external services
|
|
247
|
-
- Public APIs consumed by other apps
|
|
248
|
-
- Long-running operations
|
|
249
|
-
- File uploads/downloads
|