@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,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: express-patterns
|
|
3
|
+
description: Express.js patterns for middleware, error handling, validation, rate limiting, and graceful shutdown.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Express.js Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building Express 4/5 applications in Node.js. Use this
|
|
11
|
+
skill for structuring middleware pipelines, centralizing error handling, validating
|
|
12
|
+
request data, implementing rate limiting, and shutting down gracefully under load.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### Middleware Pipeline
|
|
17
|
+
|
|
18
|
+
Middleware executes in order of registration. Use `app.use()` for global middleware,
|
|
19
|
+
`router.use()` for route-scoped. Always call `next()` or send a response.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import express from 'express'
|
|
23
|
+
|
|
24
|
+
const app = express()
|
|
25
|
+
|
|
26
|
+
// Global middleware — order matters
|
|
27
|
+
app.use(express.json({ limit: '1mb' }))
|
|
28
|
+
app.use(requestId())
|
|
29
|
+
app.use(requestLogger())
|
|
30
|
+
app.use(cors(corsOptions))
|
|
31
|
+
|
|
32
|
+
// Route-scoped middleware
|
|
33
|
+
const authRouter = express.Router()
|
|
34
|
+
authRouter.use(authenticate) // applies to all routes in this router
|
|
35
|
+
authRouter.get('/me', getProfile)
|
|
36
|
+
authRouter.put('/me', validateBody(updateProfileSchema), updateProfile)
|
|
37
|
+
|
|
38
|
+
app.use('/api', authRouter)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Request ID Middleware
|
|
42
|
+
|
|
43
|
+
Attach a unique ID to every request for tracing across logs and downstream services.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { randomUUID } from 'crypto'
|
|
47
|
+
|
|
48
|
+
function requestId() {
|
|
49
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
50
|
+
const id = req.headers['x-request-id'] as string || randomUUID()
|
|
51
|
+
req.id = id
|
|
52
|
+
res.setHeader('X-Request-Id', id)
|
|
53
|
+
next()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Centralized Error Handling
|
|
59
|
+
|
|
60
|
+
Define an error-handling middleware (4 arguments) as the last `app.use`. Catch
|
|
61
|
+
async errors with a wrapper or Express 5's native async support.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Async wrapper for Express 4 (Express 5 handles this natively)
|
|
65
|
+
function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
|
|
66
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
67
|
+
fn(req, res, next).catch(next)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Domain error class
|
|
72
|
+
class AppError extends Error {
|
|
73
|
+
constructor(
|
|
74
|
+
public statusCode: number,
|
|
75
|
+
public code: string,
|
|
76
|
+
message: string,
|
|
77
|
+
) {
|
|
78
|
+
super(message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Central error handler — must be registered last
|
|
83
|
+
function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) {
|
|
84
|
+
if (err instanceof AppError) {
|
|
85
|
+
return res.status(err.statusCode).json({
|
|
86
|
+
error: { code: err.code, message: err.message },
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.error(`[${req.id}] Unhandled error:`, err)
|
|
91
|
+
res.status(500).json({
|
|
92
|
+
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
app.use(errorHandler)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Validation with Zod
|
|
100
|
+
|
|
101
|
+
Validate request bodies, query params, and path params at the middleware level.
|
|
102
|
+
Return structured 400 errors with field-level details.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { z, ZodSchema } from 'zod'
|
|
106
|
+
|
|
107
|
+
function validateBody(schema: ZodSchema) {
|
|
108
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
109
|
+
const result = schema.safeParse(req.body)
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
return res.status(400).json({
|
|
112
|
+
error: {
|
|
113
|
+
code: 'VALIDATION_ERROR',
|
|
114
|
+
details: result.error.issues.map(i => ({
|
|
115
|
+
field: i.path.join('.'),
|
|
116
|
+
message: i.message,
|
|
117
|
+
})),
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
req.body = result.data // parsed and typed
|
|
122
|
+
next()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const createProjectSchema = z.object({
|
|
127
|
+
name: z.string().min(1).max(100),
|
|
128
|
+
description: z.string().max(500).optional(),
|
|
129
|
+
budget: z.number().positive(),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
router.post('/projects', validateBody(createProjectSchema), asyncHandler(async (req, res) => {
|
|
133
|
+
const project = await projectService.create(req.body)
|
|
134
|
+
res.status(201).json(project)
|
|
135
|
+
}))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Rate Limiting
|
|
139
|
+
|
|
140
|
+
Use `express-rate-limit` for basic rate limiting. Apply stricter limits to
|
|
141
|
+
auth endpoints. Use Redis store in production for multi-instance deployments.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import rateLimit from 'express-rate-limit'
|
|
145
|
+
|
|
146
|
+
const globalLimiter = rateLimit({
|
|
147
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
148
|
+
max: 100,
|
|
149
|
+
standardHeaders: true,
|
|
150
|
+
legacyHeaders: false,
|
|
151
|
+
message: { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const authLimiter = rateLimit({
|
|
155
|
+
windowMs: 15 * 60 * 1000,
|
|
156
|
+
max: 5, // stricter for login attempts
|
|
157
|
+
skipSuccessfulRequests: true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
app.use(globalLimiter)
|
|
161
|
+
app.use('/api/auth/login', authLimiter)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Graceful Shutdown
|
|
165
|
+
|
|
166
|
+
Stop accepting new connections, finish in-flight requests, close database pools
|
|
167
|
+
and other resources, then exit. Handle both SIGTERM and SIGINT.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const server = app.listen(PORT, () => {
|
|
171
|
+
console.log(`Listening on port ${PORT}`)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
async function shutdown(signal: string) {
|
|
175
|
+
console.log(`${signal} received. Shutting down gracefully...`)
|
|
176
|
+
|
|
177
|
+
server.close(async () => {
|
|
178
|
+
console.log('HTTP server closed')
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await db.end() // close DB pool
|
|
182
|
+
await cache.quit() // close Redis
|
|
183
|
+
console.log('All connections closed')
|
|
184
|
+
process.exit(0)
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error('Error during shutdown:', err)
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Force exit after 30 seconds
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
console.error('Forced shutdown after timeout')
|
|
194
|
+
process.exit(1)
|
|
195
|
+
}, 30_000)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
199
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Router Organization
|
|
203
|
+
|
|
204
|
+
Group routes by domain in separate files. Mount on the main app with prefixes.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// routes/projects.ts
|
|
208
|
+
const router = express.Router()
|
|
209
|
+
router.get('/', asyncHandler(listProjects))
|
|
210
|
+
router.post('/', validateBody(createProjectSchema), asyncHandler(createProject))
|
|
211
|
+
router.get('/:id', asyncHandler(getProject))
|
|
212
|
+
export default router
|
|
213
|
+
|
|
214
|
+
// app.ts
|
|
215
|
+
import projectRoutes from './routes/projects'
|
|
216
|
+
app.use('/api/v1/projects', authenticate, projectRoutes)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Examples
|
|
220
|
+
|
|
221
|
+
**Pattern: Health check endpoint**
|
|
222
|
+
```typescript
|
|
223
|
+
app.get('/healthz', (req, res) => {
|
|
224
|
+
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() })
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Checklist
|
|
229
|
+
|
|
230
|
+
- [ ] Request ID middleware attached early in the pipeline
|
|
231
|
+
- [ ] `express.json()` with explicit size `limit`
|
|
232
|
+
- [ ] Async handler wrapper (Express 4) or native async support (Express 5)
|
|
233
|
+
- [ ] Single error-handling middleware registered last (`err, req, res, next`)
|
|
234
|
+
- [ ] `AppError` class with status code and error code for domain errors
|
|
235
|
+
- [ ] Zod or Joi validation middleware on every POST/PUT/PATCH route
|
|
236
|
+
- [ ] Rate limiting: global + stricter on auth endpoints
|
|
237
|
+
- [ ] Graceful shutdown handles SIGTERM/SIGINT, closes DB/cache connections
|
|
238
|
+
- [ ] Forced exit timeout (30s) prevents hanging processes
|
|
239
|
+
- [ ] Routes organized by domain in separate router files
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fastapi-patterns
|
|
3
|
+
description: FastAPI patterns for dependency injection, Pydantic models, middleware, background tasks, and WebSockets.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building async Python APIs with FastAPI. Use this skill
|
|
11
|
+
for structuring endpoints with dependency injection, validating requests with Pydantic,
|
|
12
|
+
adding middleware, running background tasks, and handling WebSocket connections.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### Dependency Injection
|
|
17
|
+
|
|
18
|
+
Use `Depends()` to inject services, database sessions, and auth into endpoints.
|
|
19
|
+
Dependencies can depend on other dependencies. Use `yield` dependencies for cleanup.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from fastapi import Depends, FastAPI
|
|
23
|
+
|
|
24
|
+
async def get_db():
|
|
25
|
+
db = SessionLocal()
|
|
26
|
+
try:
|
|
27
|
+
yield db
|
|
28
|
+
finally:
|
|
29
|
+
db.close()
|
|
30
|
+
|
|
31
|
+
async def get_current_user(
|
|
32
|
+
token: str = Depends(oauth2_scheme),
|
|
33
|
+
db: Session = Depends(get_db),
|
|
34
|
+
) -> User:
|
|
35
|
+
user = await verify_token(token, db)
|
|
36
|
+
if not user:
|
|
37
|
+
raise HTTPException(status_code=401, detail="Invalid token")
|
|
38
|
+
return user
|
|
39
|
+
|
|
40
|
+
@app.get("/me")
|
|
41
|
+
async def read_me(user: User = Depends(get_current_user)):
|
|
42
|
+
return user
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Pydantic Models
|
|
46
|
+
|
|
47
|
+
Separate request and response models. Use `model_config` for serialization settings.
|
|
48
|
+
Validate with `@field_validator` and `@model_validator`.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
52
|
+
|
|
53
|
+
class CreateProject(BaseModel):
|
|
54
|
+
name: str = Field(min_length=1, max_length=100)
|
|
55
|
+
description: str = Field(default="", max_length=500)
|
|
56
|
+
budget: float = Field(gt=0)
|
|
57
|
+
tags: list[str] = Field(default_factory=list, max_length=10)
|
|
58
|
+
|
|
59
|
+
@field_validator("tags")
|
|
60
|
+
@classmethod
|
|
61
|
+
def normalize_tags(cls, v: list[str]) -> list[str]:
|
|
62
|
+
return [tag.lower().strip() for tag in v if tag.strip()]
|
|
63
|
+
|
|
64
|
+
class ProjectResponse(BaseModel):
|
|
65
|
+
model_config = {"from_attributes": True}
|
|
66
|
+
|
|
67
|
+
id: int
|
|
68
|
+
name: str
|
|
69
|
+
description: str
|
|
70
|
+
budget: float
|
|
71
|
+
created_at: datetime
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Middleware
|
|
75
|
+
|
|
76
|
+
Use pure ASGI middleware for performance-critical paths. Use `@app.middleware("http")`
|
|
77
|
+
for simpler cases. Always call `call_next` and handle exceptions.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
81
|
+
|
|
82
|
+
class TimingMiddleware(BaseHTTPMiddleware):
|
|
83
|
+
async def dispatch(self, request: Request, call_next):
|
|
84
|
+
start = time.monotonic()
|
|
85
|
+
response = await call_next(request)
|
|
86
|
+
duration = time.monotonic() - start
|
|
87
|
+
response.headers["X-Duration-Ms"] = f"{duration * 1000:.1f}"
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
app.add_middleware(TimingMiddleware)
|
|
91
|
+
app.add_middleware(CORSMiddleware, allow_origins=["https://example.com"])
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Background Tasks
|
|
95
|
+
|
|
96
|
+
Use `BackgroundTasks` for fire-and-forget work after the response is sent. For
|
|
97
|
+
heavier work, use a task queue (Celery, arq, or Dramatiq).
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from fastapi import BackgroundTasks
|
|
101
|
+
|
|
102
|
+
async def send_welcome_email(email: str, name: str):
|
|
103
|
+
await email_client.send(to=email, subject=f"Welcome, {name}!")
|
|
104
|
+
|
|
105
|
+
@app.post("/users", status_code=201)
|
|
106
|
+
async def create_user(
|
|
107
|
+
body: CreateUser,
|
|
108
|
+
bg: BackgroundTasks,
|
|
109
|
+
db: Session = Depends(get_db),
|
|
110
|
+
):
|
|
111
|
+
user = await user_service.create(db, body)
|
|
112
|
+
bg.add_task(send_welcome_email, user.email, user.name)
|
|
113
|
+
return user
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### WebSockets
|
|
117
|
+
|
|
118
|
+
Accept connections explicitly. Use try/finally for cleanup. Handle
|
|
119
|
+
`WebSocketDisconnect` to avoid crashes on client disconnect.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
123
|
+
|
|
124
|
+
class ConnectionManager:
|
|
125
|
+
def __init__(self):
|
|
126
|
+
self.connections: dict[str, WebSocket] = {}
|
|
127
|
+
|
|
128
|
+
async def connect(self, ws: WebSocket, user_id: str):
|
|
129
|
+
await ws.accept()
|
|
130
|
+
self.connections[user_id] = ws
|
|
131
|
+
|
|
132
|
+
async def disconnect(self, user_id: str):
|
|
133
|
+
self.connections.pop(user_id, None)
|
|
134
|
+
|
|
135
|
+
async def broadcast(self, message: dict):
|
|
136
|
+
for ws in self.connections.values():
|
|
137
|
+
await ws.send_json(message)
|
|
138
|
+
|
|
139
|
+
manager = ConnectionManager()
|
|
140
|
+
|
|
141
|
+
@app.websocket("/ws/{user_id}")
|
|
142
|
+
async def websocket_endpoint(ws: WebSocket, user_id: str):
|
|
143
|
+
await manager.connect(ws, user_id)
|
|
144
|
+
try:
|
|
145
|
+
while True:
|
|
146
|
+
data = await ws.receive_json()
|
|
147
|
+
await handle_message(user_id, data)
|
|
148
|
+
except WebSocketDisconnect:
|
|
149
|
+
await manager.disconnect(user_id)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Router Organization
|
|
153
|
+
|
|
154
|
+
Split routes into routers by domain. Mount with prefixes and tags. Use
|
|
155
|
+
`include_router` in the main app.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# routers/projects.py
|
|
159
|
+
router = APIRouter(prefix="/projects", tags=["projects"])
|
|
160
|
+
|
|
161
|
+
@router.get("/", response_model=list[ProjectResponse])
|
|
162
|
+
async def list_projects(db: Session = Depends(get_db)):
|
|
163
|
+
return await project_service.list_all(db)
|
|
164
|
+
|
|
165
|
+
# main.py
|
|
166
|
+
from routers import projects, users, auth
|
|
167
|
+
app.include_router(projects.router)
|
|
168
|
+
app.include_router(users.router)
|
|
169
|
+
app.include_router(auth.router, prefix="/auth")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
**Pattern: Custom exception handler**
|
|
175
|
+
```python
|
|
176
|
+
class AppError(Exception):
|
|
177
|
+
def __init__(self, code: str, message: str, status: int = 400):
|
|
178
|
+
self.code = code
|
|
179
|
+
self.message = message
|
|
180
|
+
self.status = status
|
|
181
|
+
|
|
182
|
+
@app.exception_handler(AppError)
|
|
183
|
+
async def app_error_handler(request: Request, exc: AppError):
|
|
184
|
+
return JSONResponse(status_code=exc.status, content={"error": exc.code, "message": exc.message})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Checklist
|
|
188
|
+
|
|
189
|
+
- [ ] Separate Pydantic models for request body vs. response
|
|
190
|
+
- [ ] `Depends()` for DB sessions, auth, and services (never instantiate in handlers)
|
|
191
|
+
- [ ] Yield dependencies for resources that need cleanup (DB connections, files)
|
|
192
|
+
- [ ] `response_model` set on every endpoint for OpenAPI doc accuracy
|
|
193
|
+
- [ ] `status_code` explicitly set on POST (201), DELETE (204) endpoints
|
|
194
|
+
- [ ] Background tasks for post-response work; task queues for heavy processing
|
|
195
|
+
- [ ] Custom exception handlers for domain errors (not bare `HTTPException` everywhere)
|
|
196
|
+
- [ ] WebSocket handlers wrapped in try/except `WebSocketDisconnect`
|
|
197
|
+
- [ ] Routes organized into `APIRouter` with prefix and tags
|
|
198
|
+
- [ ] Middleware ordered correctly (CORS before auth, timing outermost)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feature-flags
|
|
3
|
+
description: Feature flag patterns with LaunchDarkly, Unleash, gradual rollout, A/B testing, and kill switches.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Feature Flags
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when you need to decouple deployment from release. Feature flags let you deploy code to production without exposing it to users, gradually roll out features to subsets of users, A/B test variations, and instantly kill a feature if it causes problems. Use flags for any change that carries risk or needs measurement.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Basic Feature Flag Implementation
|
|
14
|
+
|
|
15
|
+
Start simple before reaching for a third-party service:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
|
|
20
|
+
interface FlagConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
rolloutPercent?: number;
|
|
23
|
+
allowList?: string[];
|
|
24
|
+
variants?: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const flags: Record<string, FlagConfig> = {
|
|
28
|
+
"new-checkout-flow": {
|
|
29
|
+
enabled: true,
|
|
30
|
+
rolloutPercent: 25,
|
|
31
|
+
allowList: ["user_internal_team"],
|
|
32
|
+
},
|
|
33
|
+
"pricing-v2": {
|
|
34
|
+
enabled: true,
|
|
35
|
+
variants: { control: 50, variant_a: 25, variant_b: 25 },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function deterministicBucket(key: string, userId: string): number {
|
|
40
|
+
const hash = createHash("md5").update(`${key}:${userId}`).digest();
|
|
41
|
+
return hash.readUInt32BE(0) % 100;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isEnabled(flagKey: string, userId?: string): boolean {
|
|
45
|
+
const flag = flags[flagKey];
|
|
46
|
+
if (!flag?.enabled) return false;
|
|
47
|
+
if (userId && flag.allowList?.includes(userId)) return true;
|
|
48
|
+
if (flag.rolloutPercent !== undefined && userId) {
|
|
49
|
+
return deterministicBucket(flagKey, userId) < flag.rolloutPercent;
|
|
50
|
+
}
|
|
51
|
+
return flag.enabled;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### LaunchDarkly Integration
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import * as LaunchDarkly from "@launchdarkly/node-server-sdk";
|
|
59
|
+
|
|
60
|
+
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!);
|
|
61
|
+
await ldClient.waitForInitialization();
|
|
62
|
+
|
|
63
|
+
async function checkFlag(flagKey: string, user: { id: string; email: string }): Promise<boolean> {
|
|
64
|
+
const context: LaunchDarkly.LDContext = {
|
|
65
|
+
kind: "user",
|
|
66
|
+
key: user.id,
|
|
67
|
+
email: user.email,
|
|
68
|
+
};
|
|
69
|
+
return ldClient.variation(flagKey, context, false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// React client-side hook
|
|
73
|
+
import { useFlags } from "launchdarkly-react-client-sdk";
|
|
74
|
+
|
|
75
|
+
function CheckoutPage() {
|
|
76
|
+
const { newCheckoutFlow } = useFlags();
|
|
77
|
+
return newCheckoutFlow ? <NewCheckout /> : <LegacyCheckout />;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Unleash -- Open Source Alternative
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { initialize, isEnabled } from "unleash-client";
|
|
85
|
+
|
|
86
|
+
const unleash = initialize({
|
|
87
|
+
url: "https://unleash.example.com/api",
|
|
88
|
+
appName: "my-app",
|
|
89
|
+
customHeaders: { Authorization: process.env.UNLEASH_API_TOKEN! },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (isEnabled("new-checkout-flow")) {
|
|
93
|
+
renderNewCheckout();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// With user context for gradual rollout
|
|
97
|
+
const context = { userId: user.id, properties: { plan: user.plan } };
|
|
98
|
+
if (isEnabled("premium-feature", context)) {
|
|
99
|
+
renderPremiumFeature();
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Gradual Rollout Strategy
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Phase 1: Internal team only
|
|
107
|
+
{ enabled: true, allowList: ["team_engineering"] }
|
|
108
|
+
|
|
109
|
+
// Phase 2: 5% of users -- monitor error rates
|
|
110
|
+
{ enabled: true, rolloutPercent: 5 }
|
|
111
|
+
|
|
112
|
+
// Phase 3: 25% -- check business metrics
|
|
113
|
+
{ enabled: true, rolloutPercent: 25 }
|
|
114
|
+
|
|
115
|
+
// Phase 4: 100% -- fully rolled out
|
|
116
|
+
{ enabled: true, rolloutPercent: 100 }
|
|
117
|
+
|
|
118
|
+
// Phase 5: Remove flag -- clean up the code path entirely
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### A/B Testing with Variants
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
function getVariant(flagKey: string, userId: string, variants: Record<string, number>): string {
|
|
125
|
+
const bucket = deterministicBucket(flagKey, userId);
|
|
126
|
+
let cumulative = 0;
|
|
127
|
+
for (const [variant, weight] of Object.entries(variants)) {
|
|
128
|
+
cumulative += weight;
|
|
129
|
+
if (bucket < cumulative) return variant;
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(variants)[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const variant = getVariant("pricing-page", user.id, {
|
|
135
|
+
control: 50,
|
|
136
|
+
annual_first: 25,
|
|
137
|
+
comparison_table: 25,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
analytics.track("pricing_page_viewed", { variant, userId: user.id });
|
|
141
|
+
|
|
142
|
+
switch (variant) {
|
|
143
|
+
case "control": return <PricingOriginal />;
|
|
144
|
+
case "annual_first": return <PricingAnnualFirst />;
|
|
145
|
+
case "comparison_table": return <PricingComparison />;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Kill Switch Pattern
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
function killSwitchMiddleware(flagKey: string) {
|
|
153
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
154
|
+
if (!isEnabled(flagKey)) {
|
|
155
|
+
return res.status(503).json({
|
|
156
|
+
error: "This feature is temporarily unavailable",
|
|
157
|
+
retryAfter: 300,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
next();
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
app.post("/api/payments", killSwitchMiddleware("payments-enabled"), paymentHandler);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Flag Cleanup -- Prevent Technical Debt
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface FlagMetadata {
|
|
171
|
+
createdAt: Date;
|
|
172
|
+
owner: string;
|
|
173
|
+
jiraTicket: string;
|
|
174
|
+
expiresAt: Date;
|
|
175
|
+
type: "release" | "experiment" | "ops" | "permission";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function auditStaleFlags(flags: Record<string, FlagMetadata>): string[] {
|
|
179
|
+
const now = new Date();
|
|
180
|
+
return Object.entries(flags)
|
|
181
|
+
.filter(([_, meta]) => meta.type === "release" && meta.expiresAt < now)
|
|
182
|
+
.map(([key]) => key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Run in CI: fail if stale flags exist
|
|
186
|
+
const stale = auditStaleFlags(flagRegistry);
|
|
187
|
+
if (stale.length > 0) {
|
|
188
|
+
console.error("Stale feature flags need cleanup:", stale);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Examples
|
|
194
|
+
|
|
195
|
+
| Pattern | When | Result |
|
|
196
|
+
|---------|------|--------|
|
|
197
|
+
| Boolean flag | Simple on/off feature | Deploy safely, enable later |
|
|
198
|
+
| Gradual rollout | Risky changes | Limit blast radius to N% |
|
|
199
|
+
| A/B testing | Optimize conversion | Data-driven decisions |
|
|
200
|
+
| Kill switch | Payment, auth flows | Instant disable in incidents |
|
|
201
|
+
| Allow list | Beta features | Internal testing first |
|
|
202
|
+
| Flag cleanup | Post-rollout | Prevent code path debt |
|
|
203
|
+
|
|
204
|
+
## Checklist
|
|
205
|
+
- [ ] All risky features behind a feature flag
|
|
206
|
+
- [ ] Gradual rollout starts at 5%, not 50%
|
|
207
|
+
- [ ] Kill switches exist for critical paths (payments, auth)
|
|
208
|
+
- [ ] A/B variants tracked in analytics for measurement
|
|
209
|
+
- [ ] Flag defaults are safe (off) if flag service unreachable
|
|
210
|
+
- [ ] Stale flags (> 90 days) cleaned up with explicit expiry
|
|
211
|
+
- [ ] Flag evaluation consistent per user (deterministic hash)
|
|
212
|
+
- [ ] Flag changes auditable with who/when/why
|