codeforge-dev 1.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.
Files changed (131) hide show
  1. package/.devcontainer/.env +22 -0
  2. package/.devcontainer/CHANGELOG.md +197 -0
  3. package/.devcontainer/CLAUDE.md +117 -0
  4. package/.devcontainer/README.md +222 -0
  5. package/.devcontainer/config/main-system-prompt.md +502 -0
  6. package/.devcontainer/config/settings.json +47 -0
  7. package/.devcontainer/devcontainer.json +94 -0
  8. package/.devcontainer/features/README.md +113 -0
  9. package/.devcontainer/features/agent-browser/README.md +65 -0
  10. package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
  11. package/.devcontainer/features/agent-browser/install.sh +79 -0
  12. package/.devcontainer/features/ast-grep/README.md +24 -0
  13. package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
  14. package/.devcontainer/features/ast-grep/install.sh +51 -0
  15. package/.devcontainer/features/ccstatusline/README.md +296 -0
  16. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
  17. package/.devcontainer/features/ccstatusline/install.sh +290 -0
  18. package/.devcontainer/features/ccusage/README.md +205 -0
  19. package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
  20. package/.devcontainer/features/ccusage/install.sh +132 -0
  21. package/.devcontainer/features/claude-code/README.md +498 -0
  22. package/.devcontainer/features/claude-code/config/settings.json +36 -0
  23. package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
  24. package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
  25. package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
  26. package/.devcontainer/features/claude-code/install.sh +466 -0
  27. package/.devcontainer/features/claude-monitor/README.md +74 -0
  28. package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
  29. package/.devcontainer/features/claude-monitor/install.sh +99 -0
  30. package/.devcontainer/features/lsp-servers/README.md +85 -0
  31. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
  32. package/.devcontainer/features/lsp-servers/install.sh +116 -0
  33. package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
  34. package/.devcontainer/features/mcp-qdrant/README.md +474 -0
  35. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
  36. package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
  37. package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
  38. package/.devcontainer/features/mcp-reasoner/README.md +177 -0
  39. package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
  40. package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
  41. package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
  42. package/.devcontainer/features/notify-hook/README.md +86 -0
  43. package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
  44. package/.devcontainer/features/notify-hook/install.sh +38 -0
  45. package/.devcontainer/features/splitrail/README.md +140 -0
  46. package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
  47. package/.devcontainer/features/splitrail/install.sh +129 -0
  48. package/.devcontainer/features/tree-sitter/README.md +138 -0
  49. package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
  50. package/.devcontainer/features/tree-sitter/install.sh +173 -0
  51. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
  52. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
  53. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
  54. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
  55. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
  56. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
  57. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
  58. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
  60. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
  61. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
  62. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
  63. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
  64. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
  65. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
  66. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
  67. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
  68. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
  69. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
  70. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
  71. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
  72. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
  73. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
  74. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
  75. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
  76. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
  77. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
  78. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
  79. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
  80. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
  81. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
  82. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
  83. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
  84. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
  85. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
  86. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
  87. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
  88. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
  89. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
  90. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
  91. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
  92. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
  93. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
  94. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
  95. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
  96. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
  97. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
  98. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
  99. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
  100. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
  101. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
  102. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
  103. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
  104. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
  106. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
  107. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
  108. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
  109. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
  110. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
  111. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
  112. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
  113. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
  114. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
  115. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
  116. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
  117. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
  118. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
  119. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
  120. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
  121. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
  122. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
  123. package/.devcontainer/scripts/setup-aliases.sh +80 -0
  124. package/.devcontainer/scripts/setup-config.sh +28 -0
  125. package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
  126. package/.devcontainer/scripts/setup-plugins.sh +80 -0
  127. package/.devcontainer/scripts/setup.sh +58 -0
  128. package/LICENSE.txt +674 -0
  129. package/README.md +267 -0
  130. package/package.json +44 -0
  131. package/setup.js +83 -0
@@ -0,0 +1,344 @@
1
+ ---
2
+ name: fastapi
3
+ description: >-
4
+ This skill should be used when the user asks to "build a FastAPI app",
5
+ "create a REST API with FastAPI", "add SSE streaming to FastAPI",
6
+ "use dependency injection in FastAPI", "define Pydantic models for an API",
7
+ "stream LLM responses with FastAPI", "add middleware to FastAPI",
8
+ "handle background tasks in FastAPI", or discusses FastAPI routing,
9
+ Pydantic v2 models, dependency injection, server-sent events,
10
+ or ASGI middleware.
11
+ version: 0.1.0
12
+ ---
13
+
14
+ # FastAPI Development
15
+
16
+ ## Mental Model
17
+
18
+ FastAPI is a type-driven API framework where **path function signatures define the entire contract**. The function's parameters declare what the endpoint accepts (path params, query params, request body, dependencies); the return type annotation and `response_model` declare what it produces. Pydantic validates all input and output automatically -- there is no separate validation layer to configure.
19
+
20
+ Dependency injection wires shared resources (database sessions, auth checks, config) into handlers without global state. Dependencies compose and nest, forming a directed graph resolved at request time.
21
+
22
+ FastAPI runs on ASGI (Starlette), making every handler natively async-capable. This matters most for streaming -- SSE and chunked responses use async generators that yield data as it becomes available, keeping connections open without blocking worker threads.
23
+
24
+ Assume FastAPI 0.100+ with Pydantic v2 for all new code. When modifying an existing codebase using Pydantic v1 patterns, ask whether to migrate or preserve the existing style.
25
+
26
+ ---
27
+
28
+ ## Routing and Path Operations
29
+
30
+ Declare endpoints with HTTP method decorators on a FastAPI app or APIRouter. The decorator configures the operation; the function signature defines the contract:
31
+
32
+ ```python
33
+ from fastapi import FastAPI, APIRouter, status
34
+ from pydantic import BaseModel
35
+
36
+ app = FastAPI()
37
+ router = APIRouter(prefix="/items", tags=["items"])
38
+
39
+ class ItemCreate(BaseModel):
40
+ name: str
41
+ price: float
42
+
43
+ class ItemResponse(BaseModel):
44
+ id: int
45
+ name: str
46
+ price: float
47
+
48
+ @router.post("/", status_code=status.HTTP_201_CREATED, response_model=ItemResponse)
49
+ async def create_item(item: ItemCreate):
50
+ record = await save_to_db(item)
51
+ return record
52
+
53
+ @router.get("/{item_id}")
54
+ async def get_item(item_id: int, include_deleted: bool = False):
55
+ return await fetch_item(item_id, include_deleted)
56
+
57
+ app.include_router(router)
58
+ ```
59
+
60
+ Path parameters are extracted from the URL pattern and validated against the type annotation. Query parameters are any function parameters not found in the path. A parameter typed as a Pydantic model is parsed from the request body.
61
+
62
+ Use `APIRouter` to organize endpoints by domain. Each router gets its own prefix and tags, then mounts onto the app with `include_router`. Keep one router per domain module.
63
+
64
+ > **Deep dive:** See `references/routing-and-dependencies.md` for APIRouter organization, path operation configuration, WebSocket endpoints, nested/overridden dependencies, and testing patterns.
65
+
66
+ ---
67
+
68
+ ## Dependency Injection
69
+
70
+ Dependencies are callables (functions or classes) declared with `Depends()`. FastAPI resolves them per-request, injecting the result into the handler parameter:
71
+
72
+ ```python
73
+ from typing import Annotated
74
+ from fastapi import Depends
75
+
76
+ async def get_db():
77
+ db = SessionLocal()
78
+ try:
79
+ yield db
80
+ finally:
81
+ await db.close()
82
+
83
+ DB = Annotated[AsyncSession, Depends(get_db)]
84
+
85
+ @router.get("/items/{item_id}")
86
+ async def get_item(item_id: int, db: DB):
87
+ return await db.get(Item, item_id)
88
+ ```
89
+
90
+ Generator dependencies (using `yield`) provide setup/teardown semantics -- the code before `yield` runs at request start, after `yield` at request end. This replaces middleware for resource lifecycle management.
91
+
92
+ Dependencies can depend on other dependencies, forming a graph. FastAPI resolves each unique dependency once per request (cached by default). Use `Annotated` type aliases to avoid repeating `Depends()` declarations across handlers.
93
+
94
+ ---
95
+
96
+ ## Request and Response Models
97
+
98
+ Pydantic v2 models define the shape of request bodies, response payloads, and query parameter groups. Separate input and output schemas to control what clients send versus what they receive:
99
+
100
+ ```python
101
+ from pydantic import BaseModel, Field, field_validator
102
+ from datetime import datetime
103
+
104
+ class UserCreate(BaseModel):
105
+ email: str
106
+ password: str = Field(min_length=8)
107
+ display_name: str | None = None
108
+
109
+ @field_validator("email")
110
+ @classmethod
111
+ def validate_email(cls, v: str) -> str:
112
+ if "@" not in v:
113
+ raise ValueError("invalid email format")
114
+ return v.lower()
115
+
116
+ class UserResponse(BaseModel):
117
+ model_config = {"from_attributes": True}
118
+
119
+ id: int
120
+ email: str
121
+ display_name: str | None
122
+ created_at: datetime
123
+ ```
124
+
125
+ Set `from_attributes = True` in model config to enable construction from ORM objects (replaces Pydantic v1's `orm_mode`). Use `Field()` for constraints (min/max length, regex, ge/le). Use `field_validator` for custom validation logic.
126
+
127
+ For polymorphic responses, use discriminated unions with a literal type field:
128
+
129
+ ```python
130
+ from typing import Literal, Union
131
+ from pydantic import BaseModel
132
+
133
+ class TextMessage(BaseModel):
134
+ type: Literal["text"] = "text"
135
+ content: str
136
+
137
+ class ImageMessage(BaseModel):
138
+ type: Literal["image"] = "image"
139
+ url: str
140
+
141
+ Message = Union[TextMessage, ImageMessage]
142
+ ```
143
+
144
+ > **Deep dive:** See `references/pydantic-models.md` for computed fields, model inheritance, custom JSON encoders, discriminated union patterns, and BaseSettings for configuration.
145
+
146
+ ---
147
+
148
+ ## Error Handling
149
+
150
+ Raise `HTTPException` for expected error conditions. FastAPI converts it to a JSON response with the specified status code:
151
+
152
+ ```python
153
+ from fastapi import HTTPException, status
154
+
155
+ @router.get("/items/{item_id}")
156
+ async def get_item(item_id: int):
157
+ item = await fetch_item(item_id)
158
+ if not item:
159
+ raise HTTPException(
160
+ status_code=status.HTTP_404_NOT_FOUND,
161
+ detail="Item not found",
162
+ )
163
+ return item
164
+ ```
165
+
166
+ Register custom exception handlers for domain exceptions and validation errors:
167
+
168
+ ```python
169
+ from fastapi.exceptions import RequestValidationError
170
+ from fastapi.responses import JSONResponse
171
+
172
+ @app.exception_handler(RequestValidationError)
173
+ async def validation_handler(request, exc):
174
+ return JSONResponse(status_code=422, content={"errors": exc.errors()})
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Server-Sent Events
180
+
181
+ SSE provides a persistent HTTP connection for server-to-client streaming. FastAPI handles SSE through `sse-starlette`, which wraps an async generator into a compliant event stream with proper `text/event-stream` headers, keep-alive, and reconnection support.
182
+
183
+ ### Basic SSE Pattern
184
+
185
+ ```python
186
+ from sse_starlette.sse import EventSourceResponse
187
+
188
+ async def event_generator():
189
+ while True:
190
+ event = await get_next_event()
191
+ if event is None:
192
+ break
193
+ yield {"event": "update", "data": event.json(), "id": str(event.id)}
194
+
195
+ @app.get("/events")
196
+ async def stream_events():
197
+ return EventSourceResponse(event_generator())
198
+ ```
199
+
200
+ Each yielded dict maps to SSE fields: `data` (required), `event` (event type), `id` (last-event ID for reconnection), and `retry` (reconnection interval in ms). Yield a plain string to send a data-only event.
201
+
202
+ ### LLM Streaming Pattern
203
+
204
+ Stream token-by-token responses from an LLM, forwarding chunks as they arrive:
205
+
206
+ ```python
207
+ import json
208
+
209
+ async def stream_llm_response(prompt: str):
210
+ response = await llm_client.chat.completions.create(
211
+ model="claude-sonnet-4-20250514",
212
+ messages=[{"role": "user", "content": prompt}],
213
+ stream=True,
214
+ )
215
+ async for chunk in response:
216
+ delta = chunk.choices[0].delta
217
+ if delta.content:
218
+ yield {"event": "token", "data": delta.content}
219
+ yield {"event": "done", "data": ""}
220
+
221
+ @app.post("/chat")
222
+ async def chat(prompt: str):
223
+ return EventSourceResponse(stream_llm_response(prompt))
224
+ ```
225
+
226
+ ### Reconnection and Backpressure
227
+
228
+ Clients reconnect automatically using the `Last-Event-ID` header. Accept this header in the generator to resume from the correct position:
229
+
230
+ ```python
231
+ from fastapi import Request
232
+
233
+ async def resumable_generator(request: Request):
234
+ last_id = request.headers.get("last-event-id")
235
+ offset = int(last_id) + 1 if last_id else 0
236
+ async for event in fetch_events_from(offset):
237
+ if await request.is_disconnected():
238
+ break
239
+ yield {"data": event.json(), "id": str(event.id)}
240
+
241
+ @app.get("/events")
242
+ async def stream(request: Request):
243
+ return EventSourceResponse(resumable_generator(request))
244
+ ```
245
+
246
+ Check `request.is_disconnected()` periodically to detect dropped clients and stop generating events. For high-throughput scenarios, use a bounded `asyncio.Queue` to apply backpressure -- producers block when the queue is full rather than accumulating unbounded memory.
247
+
248
+ > **Deep dive:** See `references/sse-and-streaming.md` for the full SSE protocol, sse-starlette configuration, heartbeat keep-alive, disconnect detection patterns, bounded queue backpressure, LLM streaming with tool calls, and testing SSE endpoints.
249
+
250
+ ---
251
+
252
+ ## Middleware and Lifespan
253
+
254
+ ### HTTP Middleware
255
+
256
+ Middleware wraps the entire request/response cycle. Use `@app.middleware("http")` for simple cases or subclass `BaseHTTPMiddleware` for complex logic:
257
+
258
+ ```python
259
+ import time
260
+
261
+ @app.middleware("http")
262
+ async def add_timing_header(request, call_next):
263
+ start = time.perf_counter()
264
+ response = await call_next(request)
265
+ response.headers["X-Process-Time"] = str(time.perf_counter() - start)
266
+ return response
267
+ ```
268
+
269
+ Add CORS support with the built-in middleware:
270
+
271
+ ```python
272
+ from fastapi.middleware.cors import CORSMiddleware
273
+
274
+ app.add_middleware(
275
+ CORSMiddleware,
276
+ allow_origins=["https://example.com"],
277
+ allow_methods=["*"],
278
+ allow_headers=["*"],
279
+ )
280
+ ```
281
+
282
+ ### Lifespan
283
+
284
+ The lifespan context manager replaces `on_startup`/`on_shutdown` events. Resources created during startup are available to all handlers through the `app.state` object:
285
+
286
+ ```python
287
+ from contextlib import asynccontextmanager
288
+
289
+ @asynccontextmanager
290
+ async def lifespan(app):
291
+ pool = await create_db_pool()
292
+ app.state.db_pool = pool
293
+ yield
294
+ await pool.close()
295
+
296
+ app = FastAPI(lifespan=lifespan)
297
+ ```
298
+
299
+ > **Deep dive:** See `references/middleware-and-lifespan.md` for custom middleware patterns, GZip compression, trusted host middleware, lifespan resource sharing, and exception handling in middleware.
300
+
301
+ ---
302
+
303
+ ## Background Tasks
304
+
305
+ Declare a `BackgroundTasks` parameter to schedule work that runs after the response is sent. Background tasks share the same process -- they are not distributed workers:
306
+
307
+ ```python
308
+ from fastapi import BackgroundTasks
309
+
310
+ async def send_notification(email: str, message: str):
311
+ await email_service.send(email, message)
312
+
313
+ @router.post("/orders")
314
+ async def create_order(order: OrderCreate, tasks: BackgroundTasks):
315
+ record = await save_order(order)
316
+ tasks.add_task(send_notification, order.email, "Order confirmed")
317
+ return record
318
+ ```
319
+
320
+ Background tasks are fire-and-forget. For work requiring reliability guarantees (retries, dead-letter queues), use a task queue like Celery or arq instead.
321
+
322
+ ---
323
+
324
+ ## Ambiguity Policy
325
+
326
+ These defaults apply when the user does not specify a preference. State the assumption when making a choice so the user can override:
327
+
328
+ - **Async vs sync handlers:** Default to `async def`. Use plain `def` only when calling blocking libraries that lack async support.
329
+ - **Pydantic version:** Default to Pydantic v2. Do not use v1 field definitions (`Field(...)` with `schema_extra`) or v1 validators (`@validator`).
330
+ - **Server:** Default to uvicorn for development, uvicorn with `--workers` for production.
331
+ - **SSE library:** Default to `sse-starlette` over raw `StreamingResponse` for SSE. Use `StreamingResponse` only for non-SSE streaming (file downloads, binary data).
332
+ - **Dependency style:** Default to `Annotated[Type, Depends()]` over bare `Depends()` in function signatures.
333
+ - **Project structure:** Default to one router per domain module with a central `app.include_router()` registration.
334
+
335
+ ---
336
+
337
+ ## Reference Files
338
+
339
+ | File | Contents |
340
+ |------|----------|
341
+ | `references/routing-and-dependencies.md` | APIRouter organization, path operation config, nested/overridden dependencies, WebSocket basics, testing endpoints |
342
+ | `references/pydantic-models.md` | Computed fields, model inheritance, custom encoders, discriminated unions, BaseSettings configuration |
343
+ | `references/sse-and-streaming.md` | Full SSE protocol, sse-starlette API, reconnection with Last-Event-ID, backpressure with bounded queues, heartbeats, disconnect detection, LLM streaming with tool calls, testing SSE |
344
+ | `references/middleware-and-lifespan.md` | Custom middleware patterns, CORS configuration, GZip, trusted hosts, lifespan resource management, exception handling in middleware |
@@ -0,0 +1,254 @@
1
+ # Middleware and Lifespan -- Deep Dive
2
+
3
+ ## 1. Custom Middleware Patterns
4
+
5
+ ### Function-Based Middleware
6
+
7
+ The simplest middleware form intercepts every request and response:
8
+
9
+ ```python
10
+ import time
11
+ from fastapi import Request
12
+
13
+ @app.middleware("http")
14
+ async def timing_middleware(request: Request, call_next):
15
+ start = time.perf_counter()
16
+ response = await call_next(request)
17
+ duration = time.perf_counter() - start
18
+ response.headers["X-Process-Time"] = f"{duration:.4f}"
19
+ return response
20
+ ```
21
+
22
+ ### Class-Based Middleware
23
+
24
+ For middleware with configuration or shared state, subclass `BaseHTTPMiddleware`:
25
+
26
+ ```python
27
+ from starlette.middleware.base import BaseHTTPMiddleware
28
+
29
+ class RequestIDMiddleware(BaseHTTPMiddleware):
30
+ async def dispatch(self, request: Request, call_next):
31
+ request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
32
+ request.state.request_id = request_id
33
+ response = await call_next(request)
34
+ response.headers["X-Request-ID"] = request_id
35
+ return response
36
+
37
+ app.add_middleware(RequestIDMiddleware)
38
+ ```
39
+
40
+ ### Pure ASGI Middleware
41
+
42
+ For performance-critical middleware or when `BaseHTTPMiddleware` limitations apply (streaming response issues), write raw ASGI middleware:
43
+
44
+ ```python
45
+ class RawTimingMiddleware:
46
+ def __init__(self, app):
47
+ self.app = app
48
+
49
+ async def __call__(self, scope, receive, send):
50
+ if scope["type"] != "http":
51
+ await self.app(scope, receive, send)
52
+ return
53
+
54
+ start = time.perf_counter()
55
+
56
+ async def send_wrapper(message):
57
+ if message["type"] == "http.response.start":
58
+ headers = dict(message.get("headers", []))
59
+ duration = time.perf_counter() - start
60
+ headers[b"x-process-time"] = str(duration).encode()
61
+ message["headers"] = list(headers.items())
62
+ await send(message)
63
+
64
+ await self.app(scope, receive, send_wrapper)
65
+
66
+ app.add_middleware(RawTimingMiddleware)
67
+ ```
68
+
69
+ Raw ASGI middleware avoids the `BaseHTTPMiddleware` limitation where the entire response body is consumed before the middleware can modify headers. This matters for streaming responses.
70
+
71
+ ---
72
+
73
+ ## 2. CORS Configuration
74
+
75
+ ### Development Setup
76
+
77
+ ```python
78
+ from fastapi.middleware.cors import CORSMiddleware
79
+
80
+ app.add_middleware(
81
+ CORSMiddleware,
82
+ allow_origins=["http://localhost:3000", "http://localhost:5173"],
83
+ allow_credentials=True,
84
+ allow_methods=["*"],
85
+ allow_headers=["*"],
86
+ )
87
+ ```
88
+
89
+ ### Production Setup
90
+
91
+ Restrict origins to known domains. Avoid `allow_origins=["*"]` when `allow_credentials=True` -- the CORS specification forbids this combination:
92
+
93
+ ```python
94
+ app.add_middleware(
95
+ CORSMiddleware,
96
+ allow_origins=["https://app.example.com", "https://admin.example.com"],
97
+ allow_credentials=True,
98
+ allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
99
+ allow_headers=["Authorization", "Content-Type"],
100
+ expose_headers=["X-Request-ID", "X-Process-Time"],
101
+ max_age=600, # preflight cache duration in seconds
102
+ )
103
+ ```
104
+
105
+ ### CORS with SSE
106
+
107
+ SSE connections use `GET` requests with `text/event-stream` accept headers. CORS applies normally. Ensure the SSE endpoint's origin is in `allow_origins` and that `expose_headers` includes any custom headers the client needs to read.
108
+
109
+ ---
110
+
111
+ ## 3. GZip Compression
112
+
113
+ ```python
114
+ from fastapi.middleware.gzip import GZipMiddleware
115
+
116
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
117
+ ```
118
+
119
+ The `minimum_size` parameter (in bytes) prevents compressing small responses where the overhead exceeds the benefit. Default to 500-1000 bytes.
120
+
121
+ GZip middleware does not compress streaming responses (`StreamingResponse`, `EventSourceResponse`). SSE connections are already efficient for small, frequent messages.
122
+
123
+ ---
124
+
125
+ ## 4. Trusted Host Middleware
126
+
127
+ Prevent host header attacks by restricting accepted hostnames:
128
+
129
+ ```python
130
+ from starlette.middleware.trustedhost import TrustedHostMiddleware
131
+
132
+ app.add_middleware(
133
+ TrustedHostMiddleware,
134
+ allowed_hosts=["example.com", "*.example.com"],
135
+ )
136
+ ```
137
+
138
+ Requests with unrecognized `Host` headers receive a 400 response. Include `localhost` during development or use a separate middleware configuration per environment.
139
+
140
+ ---
141
+
142
+ ## 5. Middleware Ordering
143
+
144
+ Middleware executes in reverse registration order (last registered runs first, closest to the application). Register in this order for typical applications:
145
+
146
+ ```python
147
+ # Outermost (runs first on request, last on response)
148
+ app.add_middleware(TrustedHostMiddleware, allowed_hosts=["example.com"])
149
+ app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"])
150
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
151
+ app.add_middleware(RequestIDMiddleware)
152
+ # Innermost (runs last on request, first on response)
153
+ ```
154
+
155
+ The CORS middleware must run before any middleware that might reject the request, so it can handle preflight `OPTIONS` requests. GZip should run after CORS to compress all responses including CORS headers.
156
+
157
+ ---
158
+
159
+ ## 6. Lifespan Resource Management
160
+
161
+ The lifespan context manager replaces deprecated `on_startup` and `on_shutdown` events. Resources created during startup are shared across all requests:
162
+
163
+ ```python
164
+ from contextlib import asynccontextmanager
165
+ from fastapi import FastAPI
166
+
167
+ @asynccontextmanager
168
+ async def lifespan(app: FastAPI):
169
+ # Startup: create shared resources
170
+ app.state.db_pool = await create_pool(DATABASE_URL)
171
+ app.state.redis = await aioredis.from_url(REDIS_URL)
172
+ app.state.http_client = httpx.AsyncClient()
173
+
174
+ yield # Application runs
175
+
176
+ # Shutdown: clean up resources
177
+ await app.state.http_client.aclose()
178
+ await app.state.redis.close()
179
+ await app.state.db_pool.close()
180
+
181
+ app = FastAPI(lifespan=lifespan)
182
+ ```
183
+
184
+ ### Accessing Lifespan Resources
185
+
186
+ Access shared resources through `request.app.state` in handlers or dependencies:
187
+
188
+ ```python
189
+ async def get_db_pool(request: Request):
190
+ return request.app.state.db_pool
191
+
192
+ @app.get("/items")
193
+ async def list_items(pool = Depends(get_db_pool)):
194
+ async with pool.acquire() as conn:
195
+ return await conn.fetch("SELECT * FROM items")
196
+ ```
197
+
198
+ ### Multiple Resource Groups
199
+
200
+ For complex applications, compose lifespan from multiple context managers:
201
+
202
+ ```python
203
+ from contextlib import asynccontextmanager, AsyncExitStack
204
+
205
+ @asynccontextmanager
206
+ async def lifespan(app: FastAPI):
207
+ async with AsyncExitStack() as stack:
208
+ app.state.db = await stack.enter_async_context(create_db_pool())
209
+ app.state.cache = await stack.enter_async_context(create_cache())
210
+ app.state.bus = await stack.enter_async_context(create_event_bus())
211
+ yield
212
+ ```
213
+
214
+ `AsyncExitStack` ensures all resources are cleaned up in reverse order, even if one cleanup raises an exception.
215
+
216
+ ---
217
+
218
+ ## 7. Exception Handling in Middleware
219
+
220
+ ### Catching Exceptions in Middleware
221
+
222
+ Middleware can catch and transform exceptions before they reach the default handler:
223
+
224
+ ```python
225
+ @app.middleware("http")
226
+ async def error_handling_middleware(request: Request, call_next):
227
+ try:
228
+ response = await call_next(request)
229
+ return response
230
+ except Exception as exc:
231
+ logger.exception("Unhandled error", extra={"path": request.url.path})
232
+ return JSONResponse(
233
+ status_code=500,
234
+ content={"detail": "Internal server error"},
235
+ )
236
+ ```
237
+
238
+ ### Exception Handlers vs Middleware
239
+
240
+ Use exception handlers (`@app.exception_handler`) for converting known exception types to HTTP responses. Use middleware for cross-cutting concerns (logging, timing, request modification) that apply to all requests regardless of outcome.
241
+
242
+ ```python
243
+ class RateLimitExceeded(Exception):
244
+ def __init__(self, retry_after: int):
245
+ self.retry_after = retry_after
246
+
247
+ @app.exception_handler(RateLimitExceeded)
248
+ async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
249
+ return JSONResponse(
250
+ status_code=429,
251
+ content={"detail": "Rate limit exceeded"},
252
+ headers={"Retry-After": str(exc.retry_after)},
253
+ )
254
+ ```