mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-10 12:14:57 +03:00
678 lines
21 KiB
Markdown
678 lines
21 KiB
Markdown
# Backend Frameworks — FastAPI Patterns
|
|
|
|
|
|
# FastAPI
|
|
|
|
## When to Use
|
|
|
|
- Building REST APIs with Python
|
|
- Async web applications
|
|
- OpenAPI/Swagger documentation needed
|
|
- Python microservices
|
|
- WebSocket real-time applications
|
|
|
|
## When NOT to Use
|
|
|
|
- Django projects — use the `django` skill instead
|
|
- JavaScript/Node.js backends (Express, NestJS) — this skill is Python-only
|
|
- Non-API applications such as CLI tools, desktop apps, or batch processing scripts
|
|
|
|
---
|
|
|
|
## Core Patterns
|
|
|
|
### 1. Project Structure
|
|
|
|
Recommended layout for medium-large FastAPI applications:
|
|
|
|
```
|
|
project/
|
|
├── app/
|
|
│ ├── __init__.py
|
|
│ ├── main.py # FastAPI app creation, startup/shutdown
|
|
│ ├── config.py # Settings via pydantic-settings
|
|
│ ├── dependencies.py # Shared dependencies
|
|
│ ├── exceptions.py # Custom exception handlers
|
|
│ ├── middleware.py # Custom middleware
|
|
│ ├── api/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── router.py # Root router aggregating all sub-routers
|
|
│ │ ├── v1/
|
|
│ │ │ ├── __init__.py
|
|
│ │ │ ├── users.py # /api/v1/users endpoints
|
|
│ │ │ ├── items.py # /api/v1/items endpoints
|
|
│ │ │ └── auth.py # /api/v1/auth endpoints
|
|
│ │ └── v2/ # Future API version
|
|
│ ├── models/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── user.py # SQLAlchemy / SQLModel ORM models
|
|
│ │ └── item.py
|
|
│ ├── schemas/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── user.py # Pydantic request/response schemas
|
|
│ │ └── item.py
|
|
│ ├── services/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── user_service.py # Business logic layer
|
|
│ │ └── item_service.py
|
|
│ ├── repositories/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── user_repo.py # Data access layer
|
|
│ │ └── item_repo.py
|
|
│ ├── core/
|
|
│ │ ├── __init__.py
|
|
│ │ ├── database.py # DB engine, session factory
|
|
│ │ └── security.py # JWT, hashing, auth utils
|
|
│ └── tests/
|
|
│ ├── __init__.py
|
|
│ ├── conftest.py # Fixtures: test client, test DB
|
|
│ ├── test_users.py
|
|
│ └── test_items.py
|
|
├── alembic/ # Database migrations
|
|
│ ├── env.py
|
|
│ └── versions/
|
|
├── alembic.ini
|
|
├── pyproject.toml
|
|
├── Dockerfile
|
|
└── .env
|
|
```
|
|
|
|
**Key conventions:**
|
|
- Separate `schemas/` (Pydantic) from `models/` (ORM) to keep concerns clean
|
|
- Use `services/` for business logic, `repositories/` for data access
|
|
- Version API routes under `api/v1/`, `api/v2/` for backward compatibility
|
|
- Keep `main.py` thin — it only wires things together
|
|
|
|
```python
|
|
# app/main.py
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI
|
|
from app.api.router import api_router
|
|
from app.config import settings
|
|
from app.core.database import engine
|
|
from app.middleware import add_middleware
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup: create tables, warm caches, connect to services
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield
|
|
# Shutdown: close connections, flush buffers
|
|
await engine.dispose()
|
|
|
|
app = FastAPI(
|
|
title=settings.PROJECT_NAME,
|
|
version=settings.VERSION,
|
|
lifespan=lifespan,
|
|
)
|
|
add_middleware(app)
|
|
app.include_router(api_router, prefix="/api")
|
|
```
|
|
|
|
### 2. Route Patterns
|
|
|
|
#### APIRouter with tags, prefixes, and dependencies
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends, Query, Path, Body, HTTPException, status
|
|
from app.schemas.user import UserCreate, UserResponse, UserUpdate, UserList
|
|
from app.dependencies import get_current_user
|
|
|
|
router = APIRouter(
|
|
prefix="/users",
|
|
tags=["users"],
|
|
dependencies=[Depends(get_current_user)], # Applied to all routes
|
|
responses={401: {"description": "Not authenticated"}},
|
|
)
|
|
|
|
# Path parameters with validation
|
|
@router.get("/{user_id}", response_model=UserResponse)
|
|
async def get_user(
|
|
user_id: int = Path(..., gt=0, description="The ID of the user to retrieve"),
|
|
):
|
|
user = await user_service.get(user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return user
|
|
|
|
# Query parameters with defaults and validation
|
|
@router.get("/", response_model=UserList)
|
|
async def list_users(
|
|
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
|
limit: int = Query(20, ge=1, le=100, description="Max records to return"),
|
|
search: str | None = Query(None, min_length=1, max_length=100),
|
|
sort_by: str = Query("created_at", pattern="^(created_at|name|email)$"),
|
|
):
|
|
users = await user_service.list(skip=skip, limit=limit, search=search)
|
|
return users
|
|
|
|
# Request body with status codes
|
|
@router.post(
|
|
"/",
|
|
response_model=UserResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create a new user",
|
|
description="Creates a user account and sends a welcome email.",
|
|
)
|
|
async def create_user(user: UserCreate = Body(...)):
|
|
return await user_service.create(user)
|
|
|
|
# Multiple response models for different status codes
|
|
@router.put("/{user_id}", response_model=UserResponse, responses={
|
|
404: {"description": "User not found"},
|
|
409: {"description": "Email already taken"},
|
|
})
|
|
async def update_user(user_id: int, user: UserUpdate):
|
|
return await user_service.update(user_id, user)
|
|
|
|
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_user(user_id: int):
|
|
await user_service.delete(user_id)
|
|
```
|
|
|
|
#### Router aggregation
|
|
|
|
```python
|
|
# app/api/router.py
|
|
from fastapi import APIRouter
|
|
from app.api.v1 import users, items, auth
|
|
|
|
api_router = APIRouter()
|
|
api_router.include_router(auth.router, prefix="/v1")
|
|
api_router.include_router(users.router, prefix="/v1")
|
|
api_router.include_router(items.router, prefix="/v1")
|
|
```
|
|
|
|
### 3. Dependency Injection
|
|
|
|
#### Basic dependency with Depends()
|
|
|
|
```python
|
|
from fastapi import Depends, Header, HTTPException
|
|
|
|
async def verify_api_key(x_api_key: str = Header(...)):
|
|
if x_api_key != settings.API_KEY:
|
|
raise HTTPException(status_code=403, detail="Invalid API key")
|
|
return x_api_key
|
|
```
|
|
|
|
#### Nested dependencies
|
|
|
|
```python
|
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
async with async_session_maker() as session:
|
|
yield session # yield dependency — cleanup runs after response
|
|
|
|
async def get_user_repo(db: AsyncSession = Depends(get_db)) -> UserRepository:
|
|
return UserRepository(db)
|
|
|
|
async def get_user_service(
|
|
repo: UserRepository = Depends(get_user_repo),
|
|
) -> UserService:
|
|
return UserService(repo)
|
|
|
|
@router.get("/users")
|
|
async def list_users(service: UserService = Depends(get_user_service)):
|
|
return await service.list_all()
|
|
```
|
|
|
|
#### Yield dependencies for cleanup
|
|
|
|
```python
|
|
async def get_redis() -> AsyncGenerator[Redis, None]:
|
|
redis = await aioredis.from_url(settings.REDIS_URL)
|
|
try:
|
|
yield redis
|
|
finally:
|
|
await redis.close() # Always runs, even on exceptions
|
|
|
|
async def get_http_client() -> AsyncGenerator[httpx.AsyncClient, None]:
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
yield client
|
|
```
|
|
|
|
#### Request-scoped dependencies with caching
|
|
|
|
```python
|
|
# FastAPI caches dependency results per-request by default.
|
|
# The same db session is reused if multiple deps request it.
|
|
|
|
@router.get("/dashboard")
|
|
async def dashboard(
|
|
user_service: UserService = Depends(get_user_service),
|
|
item_service: ItemService = Depends(get_item_service),
|
|
# Both services share the same db session from get_db()
|
|
):
|
|
users = await user_service.count()
|
|
items = await item_service.count()
|
|
return {"users": users, "items": items}
|
|
|
|
# To disable caching (get fresh instance each time):
|
|
@router.get("/example")
|
|
async def example(
|
|
db1: AsyncSession = Depends(get_db),
|
|
db2: AsyncSession = Depends(get_db, use_cache=False),
|
|
# db1 and db2 are different sessions
|
|
):
|
|
pass
|
|
```
|
|
|
|
#### Class-based dependencies
|
|
|
|
```python
|
|
class Pagination:
|
|
def __init__(
|
|
self,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(20, ge=1, le=100),
|
|
):
|
|
self.skip = skip
|
|
self.limit = limit
|
|
|
|
@router.get("/items")
|
|
async def list_items(pagination: Pagination = Depends()):
|
|
# pagination.skip, pagination.limit
|
|
pass
|
|
```
|
|
|
|
### 4. Middleware
|
|
|
|
#### Custom middleware
|
|
|
|
```python
|
|
import time
|
|
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
class TimingMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
start = time.perf_counter()
|
|
response = await call_next(request)
|
|
duration = time.perf_counter() - start
|
|
response.headers["X-Process-Time"] = f"{duration:.4f}"
|
|
return response
|
|
```
|
|
|
|
#### Pure ASGI middleware (higher performance)
|
|
|
|
```python
|
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
|
|
class RequestIDMiddleware:
|
|
def __init__(self, app: ASGIApp):
|
|
self.app = app
|
|
|
|
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
if scope["type"] == "http":
|
|
request_id = uuid.uuid4().hex
|
|
scope.setdefault("state", {})["request_id"] = request_id
|
|
|
|
async def send_with_header(message):
|
|
if message["type"] == "http.response.start":
|
|
headers = dict(message.get("headers", []))
|
|
headers[b"x-request-id"] = request_id.encode()
|
|
message["headers"] = list(headers.items())
|
|
await send(message)
|
|
|
|
await self.app(scope, receive, send_with_header)
|
|
else:
|
|
await self.app(scope, receive, send)
|
|
```
|
|
|
|
#### Standard middleware configuration
|
|
|
|
```python
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
|
|
def add_middleware(app: FastAPI):
|
|
# Order matters: first added = outermost (runs first on request, last on response)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.CORS_ORIGINS, # ["https://example.com"]
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.add_middleware(
|
|
TrustedHostMiddleware,
|
|
allowed_hosts=settings.ALLOWED_HOSTS, # ["example.com", "*.example.com"]
|
|
)
|
|
|
|
app.add_middleware(GZipMiddleware, minimum_size=500) # Compress responses > 500 bytes
|
|
|
|
app.add_middleware(TimingMiddleware)
|
|
```
|
|
|
|
### 5. Background Tasks
|
|
|
|
#### Simple background tasks
|
|
|
|
```python
|
|
from fastapi import BackgroundTasks
|
|
|
|
async def send_welcome_email(email: str, name: str):
|
|
# This runs after the response is sent
|
|
await email_service.send(
|
|
to=email,
|
|
subject="Welcome!",
|
|
body=f"Hello {name}, welcome to our platform.",
|
|
)
|
|
|
|
async def log_activity(user_id: int, action: str):
|
|
await activity_repo.create(user_id=user_id, action=action)
|
|
|
|
@router.post("/users", status_code=201)
|
|
async def create_user(
|
|
user: UserCreate,
|
|
background_tasks: BackgroundTasks,
|
|
):
|
|
new_user = await user_service.create(user)
|
|
# Queue multiple background tasks
|
|
background_tasks.add_task(send_welcome_email, new_user.email, new_user.name)
|
|
background_tasks.add_task(log_activity, new_user.id, "account_created")
|
|
return new_user
|
|
```
|
|
|
|
#### Long-running tasks with task queues
|
|
|
|
For tasks that take more than a few seconds, use a proper task queue:
|
|
|
|
```python
|
|
from celery import Celery
|
|
|
|
celery_app = Celery("worker", broker=settings.CELERY_BROKER_URL)
|
|
|
|
@celery_app.task
|
|
def generate_report(report_id: int):
|
|
# Long-running: query data, build PDF, upload to S3
|
|
...
|
|
|
|
@router.post("/reports", status_code=202)
|
|
async def request_report(params: ReportRequest):
|
|
report = await report_service.create(params)
|
|
generate_report.delay(report.id) # Dispatch to Celery worker
|
|
return {"report_id": report.id, "status": "processing"}
|
|
|
|
@router.get("/reports/{report_id}/status")
|
|
async def report_status(report_id: int):
|
|
report = await report_service.get(report_id)
|
|
return {"status": report.status, "url": report.download_url}
|
|
```
|
|
|
|
### 6. WebSocket
|
|
|
|
#### WebSocket endpoint with connection management
|
|
|
|
```python
|
|
from fastapi import WebSocket, WebSocketDisconnect
|
|
|
|
class ConnectionManager:
|
|
def __init__(self):
|
|
self.active_connections: dict[str, list[WebSocket]] = {}
|
|
|
|
async def connect(self, websocket: WebSocket, room: str):
|
|
await websocket.accept()
|
|
self.active_connections.setdefault(room, []).append(websocket)
|
|
|
|
def disconnect(self, websocket: WebSocket, room: str):
|
|
self.active_connections.get(room, []).remove(websocket)
|
|
|
|
async def broadcast(self, message: str, room: str):
|
|
for connection in self.active_connections.get(room, []):
|
|
try:
|
|
await connection.send_text(message)
|
|
except WebSocketDisconnect:
|
|
self.disconnect(connection, room)
|
|
|
|
manager = ConnectionManager()
|
|
|
|
@app.websocket("/ws/{room}")
|
|
async def websocket_endpoint(websocket: WebSocket, room: str):
|
|
await manager.connect(websocket, room)
|
|
try:
|
|
while True:
|
|
data = await websocket.receive_text()
|
|
await manager.broadcast(f"Message: {data}", room)
|
|
except WebSocketDisconnect:
|
|
manager.disconnect(websocket, room)
|
|
await manager.broadcast(f"User left the room", room)
|
|
```
|
|
|
|
#### WebSocket with authentication
|
|
|
|
```python
|
|
@app.websocket("/ws/private")
|
|
async def private_ws(websocket: WebSocket, token: str = Query(...)):
|
|
try:
|
|
user = verify_token(token)
|
|
except InvalidToken:
|
|
await websocket.close(code=4001, reason="Invalid token")
|
|
return
|
|
|
|
await websocket.accept()
|
|
try:
|
|
while True:
|
|
data = await websocket.receive_json()
|
|
response = await process_message(user, data)
|
|
await websocket.send_json(response)
|
|
except WebSocketDisconnect:
|
|
pass
|
|
```
|
|
|
|
### 7. File Handling
|
|
|
|
#### Upload files
|
|
|
|
```python
|
|
from fastapi import UploadFile, File
|
|
|
|
@router.post("/upload")
|
|
async def upload_file(
|
|
file: UploadFile = File(..., description="File to upload"),
|
|
):
|
|
# Validate file type and size
|
|
if file.content_type not in ["image/png", "image/jpeg"]:
|
|
raise HTTPException(400, "Only PNG and JPEG images are allowed")
|
|
|
|
if file.size and file.size > 5 * 1024 * 1024: # 5 MB
|
|
raise HTTPException(400, "File too large (max 5 MB)")
|
|
|
|
contents = await file.read()
|
|
path = f"uploads/{uuid.uuid4()}_{file.filename}"
|
|
async with aiofiles.open(path, "wb") as f:
|
|
await f.write(contents)
|
|
|
|
return {"filename": file.filename, "path": path, "size": len(contents)}
|
|
|
|
# Multiple file upload
|
|
@router.post("/upload-multiple")
|
|
async def upload_multiple(files: list[UploadFile] = File(...)):
|
|
results = []
|
|
for file in files:
|
|
contents = await file.read()
|
|
results.append({"filename": file.filename, "size": len(contents)})
|
|
return results
|
|
```
|
|
|
|
#### Streaming responses
|
|
|
|
```python
|
|
from fastapi.responses import StreamingResponse
|
|
import csv
|
|
import io
|
|
|
|
@router.get("/export/users")
|
|
async def export_users():
|
|
async def generate_csv():
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(["id", "name", "email"])
|
|
yield output.getvalue()
|
|
output.seek(0)
|
|
output.truncate(0)
|
|
|
|
async for user in user_service.stream_all():
|
|
writer.writerow([user.id, user.name, user.email])
|
|
yield output.getvalue()
|
|
output.seek(0)
|
|
output.truncate(0)
|
|
|
|
return StreamingResponse(
|
|
generate_csv(),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=users.csv"},
|
|
)
|
|
```
|
|
|
|
#### Static files
|
|
|
|
```python
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
```
|
|
|
|
### 8. Testing
|
|
|
|
#### TestClient for synchronous tests
|
|
|
|
```python
|
|
from fastapi.testclient import TestClient
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
def test_create_user():
|
|
response = client.post("/api/v1/users", json={
|
|
"email": "test@example.com",
|
|
"name": "Test User",
|
|
})
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["email"] == "test@example.com"
|
|
|
|
def test_get_user_not_found():
|
|
response = client.get("/api/v1/users/99999")
|
|
assert response.status_code == 404
|
|
```
|
|
|
|
#### Async testing with httpx
|
|
|
|
```python
|
|
import pytest
|
|
from httpx import AsyncClient, ASGITransport
|
|
from app.main import app
|
|
|
|
@pytest.fixture
|
|
async def async_client():
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
yield client
|
|
|
|
@pytest.mark.anyio
|
|
async def test_list_users(async_client: AsyncClient):
|
|
response = await async_client.get("/api/v1/users")
|
|
assert response.status_code == 200
|
|
assert isinstance(response.json(), list)
|
|
```
|
|
|
|
#### Overriding dependencies for tests
|
|
|
|
```python
|
|
from app.dependencies import get_db, get_current_user
|
|
from app.models.user import User
|
|
|
|
# Mock database session
|
|
async def override_get_db():
|
|
async with test_session_maker() as session:
|
|
yield session
|
|
|
|
# Mock authenticated user
|
|
async def override_get_current_user():
|
|
return User(id=1, email="test@example.com", name="Test")
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
app.dependency_overrides[get_current_user] = override_get_current_user
|
|
|
|
# In conftest.py — clean up after tests
|
|
@pytest.fixture(autouse=True)
|
|
def clear_overrides():
|
|
yield
|
|
app.dependency_overrides.clear()
|
|
```
|
|
|
|
#### Testing WebSocket endpoints
|
|
|
|
```python
|
|
def test_websocket():
|
|
with client.websocket_connect("/ws/test-room") as ws:
|
|
ws.send_text("hello")
|
|
data = ws.receive_text()
|
|
assert "hello" in data
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Use Pydantic models for all request/response validation** — never pass raw dicts through your API boundary. Define separate `Create`, `Update`, and `Response` schemas for each resource.
|
|
|
|
2. **Organize routes with APIRouter** — group related endpoints by resource and version. Apply shared dependencies at the router level, not on each individual route.
|
|
|
|
3. **Separate business logic from routes** — route functions should only handle HTTP concerns (parsing request, returning response). Delegate logic to service classes injected via `Depends()`.
|
|
|
|
4. **Use the lifespan context manager** — replace deprecated `on_event("startup")` and `on_event("shutdown")` with the `lifespan` async context manager for resource setup and teardown.
|
|
|
|
5. **Return proper HTTP status codes** — 201 for creation, 204 for deletion, 202 for accepted-but-not-done, 409 for conflicts. Use `status_code` parameter on route decorators.
|
|
|
|
6. **Add OpenAPI metadata** — provide `summary`, `description`, `tags`, and `responses` on routes. Set `title`, `version`, and `description` on the FastAPI app. This generates high-quality auto-docs.
|
|
|
|
7. **Use async all the way down** — if your route is `async def`, every I/O call inside it must also be async. Mixing sync blocking calls (e.g., `requests.get()`) in an async route will block the event loop.
|
|
|
|
8. **Configure settings with pydantic-settings** — load config from environment variables with validation and type coercion. Never hardcode secrets or connection strings.
|
|
|
|
```python
|
|
from pydantic_settings import BaseSettings
|
|
|
|
class Settings(BaseSettings):
|
|
DATABASE_URL: str
|
|
API_KEY: str
|
|
DEBUG: bool = False
|
|
|
|
model_config = {"env_file": ".env"}
|
|
|
|
settings = Settings()
|
|
```
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
1. **Blocking I/O in async routes** — calling `requests.get()`, `time.sleep()`, or synchronous DB drivers inside `async def` routes starves the event loop. Use `httpx`, `asyncio.sleep()`, and async database drivers instead. If you must call sync code, use `run_in_executor`.
|
|
|
|
2. **Missing response_model** — without `response_model`, FastAPI returns whatever you return, potentially leaking internal fields (passwords, internal IDs). Always define a Pydantic response schema.
|
|
|
|
3. **Forgetting to await coroutines** — calling `await db.execute(query)` vs `db.execute(query)` is easy to miss. The latter returns a coroutine object instead of results. Enable linting rules that catch unawaited coroutines.
|
|
|
|
4. **Circular imports between models and schemas** — when schemas reference ORM models and vice versa, you get import cycles. Fix by using `TYPE_CHECKING` imports or by keeping schemas and models in separate modules that do not import each other.
|
|
|
|
5. **Not handling Pydantic validation errors** — FastAPI returns 422 by default, but the error format may confuse API consumers. Add a custom exception handler to reshape validation error responses to match your API's error format.
|
|
|
|
6. **Sharing mutable state across requests without locks** — global mutable variables (lists, dicts) accessed from async routes can cause race conditions. Use async-safe structures or dependency-injected per-request state.
|
|
|
|
---
|
|
|
|
## Related Skills
|
|
|
|
- `python` — Python language patterns and best practices
|
|
- `openapi` — OpenAPI specification and documentation standards
|
|
- `postgresql` — Database integration with async SQLAlchemy
|
|
- `pytest` — Testing FastAPI applications with pytest and httpx
|
|
- `authentication` — JWT, OAuth2, and session patterns for FastAPI endpoints
|
|
- `logging` — Structured logging for FastAPI applications
|