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.
- package/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- package/setup.js +83 -0
package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md
ADDED
|
@@ -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
|
+
```
|