@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,443 +0,0 @@
|
|
|
1
|
-
# API Versioning Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for versioning REST APIs.
|
|
4
|
-
|
|
5
|
-
## URL-Based Versioning
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// app/api/v1/users/route.ts
|
|
9
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
10
|
-
import { prisma } from '@/lib/db'
|
|
11
|
-
|
|
12
|
-
// V1 API - returns basic user data
|
|
13
|
-
export async function GET() {
|
|
14
|
-
const users = await prisma.user.findMany({
|
|
15
|
-
select: {
|
|
16
|
-
id: true,
|
|
17
|
-
name: true,
|
|
18
|
-
email: true
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
return NextResponse.json(users)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// app/api/v2/users/route.ts
|
|
26
|
-
// V2 API - returns extended user data with different structure
|
|
27
|
-
export async function GET() {
|
|
28
|
-
const users = await prisma.user.findMany({
|
|
29
|
-
select: {
|
|
30
|
-
id: true,
|
|
31
|
-
name: true,
|
|
32
|
-
email: true,
|
|
33
|
-
createdAt: true,
|
|
34
|
-
profile: {
|
|
35
|
-
select: {
|
|
36
|
-
avatar: true,
|
|
37
|
-
bio: true
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// V2 uses different response format
|
|
44
|
-
return NextResponse.json({
|
|
45
|
-
data: users,
|
|
46
|
-
meta: {
|
|
47
|
-
total: users.length,
|
|
48
|
-
version: 'v2'
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Shared Version Logic
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
// lib/api/versions.ts
|
|
58
|
-
import { NextResponse } from 'next/server'
|
|
59
|
-
|
|
60
|
-
export type ApiVersion = 'v1' | 'v2' | 'v3'
|
|
61
|
-
|
|
62
|
-
interface VersionedResponse<T> {
|
|
63
|
-
v1: (data: T) => object
|
|
64
|
-
v2: (data: T) => object
|
|
65
|
-
v3?: (data: T) => object
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function formatResponse<T>(
|
|
69
|
-
version: ApiVersion,
|
|
70
|
-
data: T,
|
|
71
|
-
formatters: VersionedResponse<T>
|
|
72
|
-
) {
|
|
73
|
-
const formatter = formatters[version]
|
|
74
|
-
if (!formatter) {
|
|
75
|
-
throw new Error(`Unsupported API version: ${version}`)
|
|
76
|
-
}
|
|
77
|
-
return formatter(data)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Usage example
|
|
81
|
-
// lib/api/users/formatters.ts
|
|
82
|
-
import { User } from '@prisma/client'
|
|
83
|
-
|
|
84
|
-
export const userFormatters = {
|
|
85
|
-
v1: (user: User) => ({
|
|
86
|
-
id: user.id,
|
|
87
|
-
name: user.name,
|
|
88
|
-
email: user.email
|
|
89
|
-
}),
|
|
90
|
-
|
|
91
|
-
v2: (user: User & { profile?: { avatar?: string } }) => ({
|
|
92
|
-
id: user.id,
|
|
93
|
-
displayName: user.name, // renamed field
|
|
94
|
-
email: user.email,
|
|
95
|
-
avatar: user.profile?.avatar ?? null,
|
|
96
|
-
createdAt: user.createdAt.toISOString()
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Header-Based Versioning
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
// middleware.ts
|
|
105
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
106
|
-
|
|
107
|
-
export function middleware(request: NextRequest) {
|
|
108
|
-
const version = request.headers.get('API-Version') ?? 'v1'
|
|
109
|
-
const validVersions = ['v1', 'v2', 'v3']
|
|
110
|
-
|
|
111
|
-
if (!validVersions.includes(version)) {
|
|
112
|
-
return NextResponse.json(
|
|
113
|
-
{ error: `Invalid API version: ${version}` },
|
|
114
|
-
{ status: 400 }
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Add version to request headers for route handlers
|
|
119
|
-
const requestHeaders = new Headers(request.headers)
|
|
120
|
-
requestHeaders.set('x-api-version', version)
|
|
121
|
-
|
|
122
|
-
return NextResponse.next({
|
|
123
|
-
request: { headers: requestHeaders }
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export const config = {
|
|
128
|
-
matcher: '/api/:path*'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// app/api/users/route.ts
|
|
132
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
133
|
-
import { formatResponse } from '@/lib/api/versions'
|
|
134
|
-
import { userFormatters } from '@/lib/api/users/formatters'
|
|
135
|
-
|
|
136
|
-
export async function GET(request: NextRequest) {
|
|
137
|
-
const version = request.headers.get('x-api-version') as 'v1' | 'v2'
|
|
138
|
-
|
|
139
|
-
const users = await prisma.user.findMany({
|
|
140
|
-
include: version === 'v2' ? { profile: true } : undefined
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const formatted = users.map(user =>
|
|
144
|
-
formatResponse(version, user, userFormatters)
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
const response = NextResponse.json(formatted)
|
|
148
|
-
response.headers.set('API-Version', version)
|
|
149
|
-
|
|
150
|
-
return response
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Content Negotiation Versioning
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
// lib/api/content-negotiation.ts
|
|
158
|
-
import { NextRequest } from 'next/server'
|
|
159
|
-
|
|
160
|
-
export function parseAcceptHeader(request: NextRequest): {
|
|
161
|
-
version: string
|
|
162
|
-
format: string
|
|
163
|
-
} {
|
|
164
|
-
const accept = request.headers.get('Accept') ?? 'application/json'
|
|
165
|
-
|
|
166
|
-
// Parse: application/vnd.myapi.v2+json
|
|
167
|
-
const match = accept.match(/application\/vnd\.myapi\.v(\d+)\+(\w+)/)
|
|
168
|
-
|
|
169
|
-
if (match) {
|
|
170
|
-
return {
|
|
171
|
-
version: `v${match[1]}`,
|
|
172
|
-
format: match[2]
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { version: 'v1', format: 'json' }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// app/api/users/route.ts
|
|
180
|
-
export async function GET(request: NextRequest) {
|
|
181
|
-
const { version, format } = parseAcceptHeader(request)
|
|
182
|
-
|
|
183
|
-
const users = await prisma.user.findMany()
|
|
184
|
-
|
|
185
|
-
const formatted = users.map(user =>
|
|
186
|
-
formatResponse(version as 'v1' | 'v2', user, userFormatters)
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
const contentType = `application/vnd.myapi.${version}+${format}`
|
|
190
|
-
|
|
191
|
-
return new Response(JSON.stringify(formatted), {
|
|
192
|
-
headers: {
|
|
193
|
-
'Content-Type': contentType
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Version Deprecation
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// lib/api/deprecation.ts
|
|
203
|
-
import { NextResponse } from 'next/server'
|
|
204
|
-
|
|
205
|
-
interface DeprecationInfo {
|
|
206
|
-
deprecated: boolean
|
|
207
|
-
sunset?: string // ISO date when version will be removed
|
|
208
|
-
successor?: string // Recommended version to migrate to
|
|
209
|
-
message?: string
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const versionStatus: Record<string, DeprecationInfo> = {
|
|
213
|
-
v1: {
|
|
214
|
-
deprecated: true,
|
|
215
|
-
sunset: '2024-12-31',
|
|
216
|
-
successor: 'v2',
|
|
217
|
-
message: 'Please migrate to v2 API before December 31, 2024'
|
|
218
|
-
},
|
|
219
|
-
v2: { deprecated: false },
|
|
220
|
-
v3: { deprecated: false }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function addDeprecationHeaders(
|
|
224
|
-
response: NextResponse,
|
|
225
|
-
version: string
|
|
226
|
-
): NextResponse {
|
|
227
|
-
const info = versionStatus[version]
|
|
228
|
-
|
|
229
|
-
if (info?.deprecated) {
|
|
230
|
-
response.headers.set('Deprecation', 'true')
|
|
231
|
-
if (info.sunset) {
|
|
232
|
-
response.headers.set('Sunset', info.sunset)
|
|
233
|
-
}
|
|
234
|
-
if (info.message) {
|
|
235
|
-
response.headers.set('X-Deprecation-Notice', info.message)
|
|
236
|
-
}
|
|
237
|
-
if (info.successor) {
|
|
238
|
-
response.headers.set(
|
|
239
|
-
'Link',
|
|
240
|
-
`</api/${info.successor}>; rel="successor-version"`
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return response
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Usage in route handler
|
|
249
|
-
export async function GET(request: NextRequest) {
|
|
250
|
-
const version = request.headers.get('x-api-version') ?? 'v1'
|
|
251
|
-
|
|
252
|
-
const data = await fetchData()
|
|
253
|
-
|
|
254
|
-
let response = NextResponse.json(data)
|
|
255
|
-
response = addDeprecationHeaders(response, version)
|
|
256
|
-
|
|
257
|
-
return response
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
## Version-Specific Middleware
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
// lib/api/version-middleware.ts
|
|
265
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
266
|
-
|
|
267
|
-
type Handler = (req: NextRequest) => Promise<NextResponse>
|
|
268
|
-
type VersionedHandlers = Record<string, Handler>
|
|
269
|
-
|
|
270
|
-
export function createVersionedHandler(handlers: VersionedHandlers) {
|
|
271
|
-
return async (request: NextRequest) => {
|
|
272
|
-
const version = request.headers.get('x-api-version') ?? 'v1'
|
|
273
|
-
|
|
274
|
-
const handler = handlers[version]
|
|
275
|
-
if (!handler) {
|
|
276
|
-
return NextResponse.json(
|
|
277
|
-
{
|
|
278
|
-
error: 'Unsupported API version',
|
|
279
|
-
supportedVersions: Object.keys(handlers)
|
|
280
|
-
},
|
|
281
|
-
{ status: 400 }
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return handler(request)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// app/api/products/route.ts
|
|
290
|
-
import { createVersionedHandler } from '@/lib/api/version-middleware'
|
|
291
|
-
|
|
292
|
-
const v1Handler = async (request: NextRequest) => {
|
|
293
|
-
const products = await prisma.product.findMany()
|
|
294
|
-
return NextResponse.json(products)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const v2Handler = async (request: NextRequest) => {
|
|
298
|
-
const products = await prisma.product.findMany({
|
|
299
|
-
include: { variants: true, reviews: true }
|
|
300
|
-
})
|
|
301
|
-
return NextResponse.json({
|
|
302
|
-
data: products,
|
|
303
|
-
included: { totalCount: products.length }
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export const GET = createVersionedHandler({
|
|
308
|
-
v1: v1Handler,
|
|
309
|
-
v2: v2Handler
|
|
310
|
-
})
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## API Documentation per Version
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
// lib/api/openapi.ts
|
|
317
|
-
import { OpenAPIObject } from 'openapi3-ts/oas31'
|
|
318
|
-
|
|
319
|
-
export const openApiSpecs: Record<string, OpenAPIObject> = {
|
|
320
|
-
v1: {
|
|
321
|
-
openapi: '3.1.0',
|
|
322
|
-
info: {
|
|
323
|
-
title: 'My API',
|
|
324
|
-
version: '1.0.0',
|
|
325
|
-
description: 'API v1 (deprecated)'
|
|
326
|
-
},
|
|
327
|
-
paths: {
|
|
328
|
-
'/api/v1/users': {
|
|
329
|
-
get: {
|
|
330
|
-
summary: 'List users',
|
|
331
|
-
responses: {
|
|
332
|
-
'200': {
|
|
333
|
-
description: 'List of users',
|
|
334
|
-
content: {
|
|
335
|
-
'application/json': {
|
|
336
|
-
schema: {
|
|
337
|
-
type: 'array',
|
|
338
|
-
items: { $ref: '#/components/schemas/UserV1' }
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
},
|
|
347
|
-
components: {
|
|
348
|
-
schemas: {
|
|
349
|
-
UserV1: {
|
|
350
|
-
type: 'object',
|
|
351
|
-
properties: {
|
|
352
|
-
id: { type: 'string' },
|
|
353
|
-
name: { type: 'string' },
|
|
354
|
-
email: { type: 'string' }
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
v2: {
|
|
361
|
-
openapi: '3.1.0',
|
|
362
|
-
info: {
|
|
363
|
-
title: 'My API',
|
|
364
|
-
version: '2.0.0',
|
|
365
|
-
description: 'API v2 (current)'
|
|
366
|
-
},
|
|
367
|
-
paths: {
|
|
368
|
-
'/api/v2/users': {
|
|
369
|
-
get: {
|
|
370
|
-
summary: 'List users',
|
|
371
|
-
responses: {
|
|
372
|
-
'200': {
|
|
373
|
-
description: 'List of users with metadata',
|
|
374
|
-
content: {
|
|
375
|
-
'application/json': {
|
|
376
|
-
schema: { $ref: '#/components/schemas/UsersResponseV2' }
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
components: {
|
|
385
|
-
schemas: {
|
|
386
|
-
UserV2: {
|
|
387
|
-
type: 'object',
|
|
388
|
-
properties: {
|
|
389
|
-
id: { type: 'string' },
|
|
390
|
-
displayName: { type: 'string' },
|
|
391
|
-
email: { type: 'string' },
|
|
392
|
-
avatar: { type: 'string', nullable: true },
|
|
393
|
-
createdAt: { type: 'string', format: 'date-time' }
|
|
394
|
-
}
|
|
395
|
-
},
|
|
396
|
-
UsersResponseV2: {
|
|
397
|
-
type: 'object',
|
|
398
|
-
properties: {
|
|
399
|
-
data: {
|
|
400
|
-
type: 'array',
|
|
401
|
-
items: { $ref: '#/components/schemas/UserV2' }
|
|
402
|
-
},
|
|
403
|
-
meta: {
|
|
404
|
-
type: 'object',
|
|
405
|
-
properties: {
|
|
406
|
-
total: { type: 'number' },
|
|
407
|
-
version: { type: 'string' }
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// app/api/docs/[version]/route.ts
|
|
418
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
419
|
-
import { openApiSpecs } from '@/lib/api/openapi'
|
|
420
|
-
|
|
421
|
-
export async function GET(
|
|
422
|
-
request: NextRequest,
|
|
423
|
-
{ params }: { params: { version: string } }
|
|
424
|
-
) {
|
|
425
|
-
const spec = openApiSpecs[params.version]
|
|
426
|
-
|
|
427
|
-
if (!spec) {
|
|
428
|
-
return NextResponse.json(
|
|
429
|
-
{ error: 'Version not found' },
|
|
430
|
-
{ status: 404 }
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return NextResponse.json(spec)
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
## When to Use
|
|
439
|
-
|
|
440
|
-
- Breaking API changes
|
|
441
|
-
- Multiple client versions
|
|
442
|
-
- Gradual API evolution
|
|
443
|
-
- Long-term API support
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
# Webhook Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for handling incoming webhooks.
|
|
4
|
-
|
|
5
|
-
## Basic Webhook Handler
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// app/api/webhooks/stripe/route.ts
|
|
9
|
-
import { headers } from 'next/headers'
|
|
10
|
-
import Stripe from 'stripe'
|
|
11
|
-
|
|
12
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
13
|
-
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
|
|
14
|
-
|
|
15
|
-
export async function POST(request: Request) {
|
|
16
|
-
const body = await request.text()
|
|
17
|
-
const signature = headers().get('stripe-signature')!
|
|
18
|
-
|
|
19
|
-
let event: Stripe.Event
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.error('Webhook signature verification failed')
|
|
25
|
-
return Response.json({ error: 'Invalid signature' }, { status: 400 })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
await handleEvent(event)
|
|
30
|
-
return Response.json({ received: true })
|
|
31
|
-
} catch (err) {
|
|
32
|
-
console.error('Webhook handler error:', err)
|
|
33
|
-
return Response.json({ error: 'Handler failed' }, { status: 500 })
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function handleEvent(event: Stripe.Event) {
|
|
38
|
-
switch (event.type) {
|
|
39
|
-
case 'checkout.session.completed':
|
|
40
|
-
await handleCheckoutComplete(event.data.object)
|
|
41
|
-
break
|
|
42
|
-
case 'customer.subscription.updated':
|
|
43
|
-
await handleSubscriptionUpdate(event.data.object)
|
|
44
|
-
break
|
|
45
|
-
case 'invoice.payment_failed':
|
|
46
|
-
await handlePaymentFailed(event.data.object)
|
|
47
|
-
break
|
|
48
|
-
default:
|
|
49
|
-
console.log(`Unhandled event type: ${event.type}`)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Idempotent Webhook Processing
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
// lib/webhooks.ts
|
|
58
|
-
import { prisma } from '@/lib/db'
|
|
59
|
-
|
|
60
|
-
export async function processWebhookOnce(
|
|
61
|
-
eventId: string,
|
|
62
|
-
handler: () => Promise<void>
|
|
63
|
-
) {
|
|
64
|
-
// Check if already processed
|
|
65
|
-
const existing = await prisma.webhookEvent.findUnique({
|
|
66
|
-
where: { eventId }
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
if (existing?.processedAt) {
|
|
70
|
-
console.log(`Event ${eventId} already processed`)
|
|
71
|
-
return { skipped: true }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Create or update event record
|
|
75
|
-
await prisma.webhookEvent.upsert({
|
|
76
|
-
where: { eventId },
|
|
77
|
-
create: { eventId, receivedAt: new Date() },
|
|
78
|
-
update: {}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
await handler()
|
|
83
|
-
|
|
84
|
-
// Mark as processed
|
|
85
|
-
await prisma.webhookEvent.update({
|
|
86
|
-
where: { eventId },
|
|
87
|
-
data: { processedAt: new Date() }
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
return { success: true }
|
|
91
|
-
} catch (error) {
|
|
92
|
-
// Record failure
|
|
93
|
-
await prisma.webhookEvent.update({
|
|
94
|
-
where: { eventId },
|
|
95
|
-
data: {
|
|
96
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
97
|
-
failedAt: new Date()
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
throw error
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Usage
|
|
106
|
-
async function handleEvent(event: Stripe.Event) {
|
|
107
|
-
await processWebhookOnce(event.id, async () => {
|
|
108
|
-
// Handle event...
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Webhook Queue Processing
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
// lib/webhook-queue.ts
|
|
117
|
-
import { prisma } from '@/lib/db'
|
|
118
|
-
|
|
119
|
-
export async function queueWebhook(
|
|
120
|
-
type: string,
|
|
121
|
-
payload: unknown,
|
|
122
|
-
eventId: string
|
|
123
|
-
) {
|
|
124
|
-
await prisma.webhookQueue.create({
|
|
125
|
-
data: {
|
|
126
|
-
eventId,
|
|
127
|
-
type,
|
|
128
|
-
payload: JSON.stringify(payload),
|
|
129
|
-
status: 'PENDING'
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export async function processWebhookQueue() {
|
|
135
|
-
const pending = await prisma.webhookQueue.findMany({
|
|
136
|
-
where: { status: 'PENDING' },
|
|
137
|
-
orderBy: { createdAt: 'asc' },
|
|
138
|
-
take: 10
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
for (const webhook of pending) {
|
|
142
|
-
try {
|
|
143
|
-
await prisma.webhookQueue.update({
|
|
144
|
-
where: { id: webhook.id },
|
|
145
|
-
data: { status: 'PROCESSING' }
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const payload = JSON.parse(webhook.payload)
|
|
149
|
-
await processWebhookByType(webhook.type, payload)
|
|
150
|
-
|
|
151
|
-
await prisma.webhookQueue.update({
|
|
152
|
-
where: { id: webhook.id },
|
|
153
|
-
data: { status: 'COMPLETED', processedAt: new Date() }
|
|
154
|
-
})
|
|
155
|
-
} catch (error) {
|
|
156
|
-
await prisma.webhookQueue.update({
|
|
157
|
-
where: { id: webhook.id },
|
|
158
|
-
data: {
|
|
159
|
-
status: 'FAILED',
|
|
160
|
-
attempts: { increment: 1 },
|
|
161
|
-
lastError: error instanceof Error ? error.message : 'Unknown'
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## Webhook Retry Logic
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
// lib/webhook-retry.ts
|
|
173
|
-
const MAX_RETRIES = 3
|
|
174
|
-
const RETRY_DELAYS = [1000, 5000, 30000] // 1s, 5s, 30s
|
|
175
|
-
|
|
176
|
-
export async function withRetry<T>(
|
|
177
|
-
fn: () => Promise<T>,
|
|
178
|
-
eventId: string
|
|
179
|
-
): Promise<T> {
|
|
180
|
-
let lastError: Error
|
|
181
|
-
|
|
182
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
183
|
-
try {
|
|
184
|
-
return await fn()
|
|
185
|
-
} catch (error) {
|
|
186
|
-
lastError = error instanceof Error ? error : new Error('Unknown error')
|
|
187
|
-
|
|
188
|
-
if (attempt < MAX_RETRIES) {
|
|
189
|
-
console.log(`Retry ${attempt + 1}/${MAX_RETRIES} for ${eventId}`)
|
|
190
|
-
await sleep(RETRY_DELAYS[attempt])
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
throw lastError!
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function sleep(ms: number) {
|
|
199
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Generic Webhook Verification
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
// lib/webhooks/verify.ts
|
|
207
|
-
import { createHmac, timingSafeEqual } from 'crypto'
|
|
208
|
-
|
|
209
|
-
export function verifyWebhookSignature(
|
|
210
|
-
payload: string,
|
|
211
|
-
signature: string,
|
|
212
|
-
secret: string,
|
|
213
|
-
algorithm = 'sha256'
|
|
214
|
-
): boolean {
|
|
215
|
-
const expected = createHmac(algorithm, secret)
|
|
216
|
-
.update(payload)
|
|
217
|
-
.digest('hex')
|
|
218
|
-
|
|
219
|
-
const signatureBuffer = Buffer.from(signature)
|
|
220
|
-
const expectedBuffer = Buffer.from(expected)
|
|
221
|
-
|
|
222
|
-
if (signatureBuffer.length !== expectedBuffer.length) {
|
|
223
|
-
return false
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return timingSafeEqual(signatureBuffer, expectedBuffer)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Usage
|
|
230
|
-
export async function POST(request: Request) {
|
|
231
|
-
const body = await request.text()
|
|
232
|
-
const signature = headers().get('x-signature')!
|
|
233
|
-
|
|
234
|
-
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET!)) {
|
|
235
|
-
return Response.json({ error: 'Invalid signature' }, { status: 401 })
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Process webhook...
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## When to Use
|
|
243
|
-
|
|
244
|
-
- Payment providers (Stripe, PayPal)
|
|
245
|
-
- Third-party integrations
|
|
246
|
-
- Event-driven architectures
|
|
247
|
-
- Async processing
|