技能详情(站内镜像,无评论)
许可证:MIT-0
MIT-0 ·免费使用、修改和重新分发。无需归因。
版本:v1.0.0
统计:⭐ 0 · 371 · 0 current installs · 0 all-time installs
⭐ 0
安装量(当前) 0
🛡 VirusTotal :良性 · OpenClaw :良性
Package:1kalin/afrexai-fastapi-production
安全扫描(ClawHub)
- VirusTotal :良性
- OpenClaw :良性
OpenClaw 评估
This is an instruction-only FastAPI production engineering guide that is internally consistent with its stated purpose and does not request unusual credentials, binaries, or install steps.
目的
The name and description (build, deploy, scale FastAPI apps) match the content of SKILL.md and README: architecture, config patterns, Pydantic usage, auth, observability, CI/CD, and Docker. There are no unrelated requirements (no external credentials or unrelated binaries requested).
说明范围
SKILL.md is a comprehensive textual methodology and code examples for designing and auditing FastAPI projects. It implicitly expects the agent to operate on the user's project (e.g., 'audit my FastAPI project'), which is coherent for an auditing/assistant skill, but it does not give explicit runtime steps for reading or transmitting host files. Users should expect the agent to read their source files if they ask it to audit or modify a repo; t…
安装机制
No install spec or code files are present (instruction-only). Nothing is downloaded or written to disk by the skill itself, which minimizes install-time risk.
证书
The skill does not declare required environment variables or credentials. SKILL.md shows patterns that applications will need secrets (e.g., database_url, jwt_secret), but the skill itself does not request those. This is proportionate for a documentation/assistant skill — however, generated project templates or runtime testing by the agent may require the user to supply secrets outside the skill.
持久
always is false and model invocation is allowed (default); the skill does not request persistent system presence or modification of other skills. There is no installation that changes agent-global configuration.
综合结论
This skill is an instruction-only production guide and appears coherent with its purpose. Before using it: 1) Be prepared that asking the agent to 'audit' or 'modify' your project will require it to read your repository files — don't expose secrets or credentials you don't want inspected. 2) When following its templates, keep real secrets (DATABASE_URL, JWT_SECRET) out of source; use your own secret management and .env files. 3) The README lin…
安装(复制给龙虾 AI)
将下方整段复制到龙虾中文库对话中,由龙虾按 SKILL.md 完成安装。
请把本段交给龙虾中文库(龙虾 AI)执行:为本机安装 OpenClaw 技能「FastAPI Production Engineering」。简介:Build, deploy, and scale production-grade FastAPI apps with async, type-safe de…。
请 fetch 以下地址读取 SKILL.md 并按文档完成安装:https://raw.githubusercontent.com/openclaw/skills/refs/heads/main/skills/1kalin/afrexai-fastapi-production/SKILL.md
(来源:yingzhi8.cn 技能库)
SKILL.md
# FastAPI Production Engineering
Complete methodology for building, deploying, and scaling production FastAPI applications. Not a tutorial — a production operating system.
## Quick Health Check (/16)
Score 2 points each. Total < 8 = critical work needed.
| Signal | Healthy | Unhealthy |
|--------|---------|-----------|
| Type safety | Pydantic v2 models everywhere | `dict` returns, no validation |
| Error handling | Structured error hierarchy | Bare `HTTPException` strings |
| Auth | JWT + dependency injection | Manual token parsing |
| Testing | 80%+ coverage, async tests | No tests or sync-only |
| Database | Async ORM, migrations | Raw SQL, no migrations |
| Observability | Structured logging + tracing | `print()` debugging |
| Deployment | Multi-stage Docker, health checks | `uvicorn main:app` on bare metal |
| Documentation | Auto-generated, accurate OpenAPI | Default `/docs` untouched |
## Phase 1: Project Architecture
### Recommended Structure
```
src/
├── app/
│ ├── __init__.py
│ ├── main.py # App factory
│ ├── config.py # Pydantic Settings
│ ├── dependencies.py # Shared DI
│ ├── middleware.py # Custom middleware
│ ├── features/
│ │ ├── users/
│ │ │ ├── __init__.py
│ │ │ ├── router.py # Endpoints
│ │ │ ├── schemas.py # Pydantic models
│ │ │ ├── service.py # Business logic
│ │ │ ├── repository.py # Data access
│ │ │ ├── models.py # SQLAlchemy/SQLModel
│ │ │ ├── dependencies.py
│ │ │ └── exceptions.py
│ │ ├── auth/
│ │ ├── orders/
│ │ └── ...
│ ├── core/
│ │ ├── database.py # Engine, session factory
│ │ ├── security.py # JWT, hashing
│ │ ├── errors.py # Error hierarchy
│ │ └── logging.py # Structlog config
│ └── shared/
│ ├── pagination.py
│ ├── filters.py
│ └── responses.py
├── migrations/ # Alembic
├── tests/
│ ├── conftest.py
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── pyproject.toml
├── Dockerfile
└── docker-compose.yml
```
### 7 Architecture Rules
1. **Feature-based modules** — group by domain, not by layer
2. **Router → Service → Repository** — strict layering, no skipping
3. **Dependency injection everywhere** — use `Depends()` for testability
4. **Pydantic models at boundaries** — validate all input AND output
5. **No business logic in routers** — routers are thin, services are thick
6. **Config via environment** — Pydantic Settings with `.env` support
7. **Async by default** — use async def for all I/O-bound operations
### Framework Selection Context
```yaml
# When to choose FastAPI over alternatives
fastapi_is_best_when:
- "You need auto-generated OpenAPI docs"
- "Team knows Python type hints"
- "API-first (no server-rendered HTML as primary)"
- "High concurrency with async I/O"
- "Microservice or API gateway"
consider_alternatives:
django: "Full-featured web app with admin, ORM, auth batteries"
flask: "Simple app, team prefers explicit over magic"
litestar: "Need WebSocket-heavy or more opinionated framework"
hono_or_express: "Team prefers TypeScript"
```
## Phase 2: Configuration & Environment
### Pydantic Settings Pattern
```python
from pydantic_settings import BaseSettings
from pydantic import SecretStr, field_validator
from functools import lru_cache
class Settings(BaseSettings):
# App
app_name: str = "MyAPI"
debug: bool = False
environment: str = "production" # development | staging | production
# Server
host: str = "0.0.0.0"
port: int = 8000
workers: int = 4
# Database
database_url: SecretStr # Required — no default
db_pool_size: int = 20
db_max_overflow: int = 10
db_pool_timeout: int = 30
# Auth
jwt_secret: SecretStr # Required
jwt_algorithm: str = "HS256"
jwt_expire_minutes: int = 30
# Redis
redis_url: str = "redis://localhost:6379/0"
# CORS
cors_origins: list[str] = ["http://localhost:3000"]
@field_validator("environment")
@classmethod
def validate_environment(cls, v: str) -> str:
allowed = {"development", "staging", "production"}
if v not in allowed:
raise ValueError(f"environment must be one of {allowed}")
return v
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
@lru_cache
def get_settings() -> Settings:
return Settings()
```
### 5 Configuration Rules
1. **Never hardcode secrets** — use `SecretStr` for sensitive values
2. **Fail fast** — required fields have no defaults; app won't start without them
3. **Validate at startup** — use `@field_validator` for constraint checking
4. **Cache settings** — `@lru_cache` ensures single parse
5. **Type everything** — no `str` for structured values; use enums, Literal types
## Phase 3: Pydantic v2 Mastery
### Schema Design Patterns
```python
from pydantic import BaseModel, Field, ConfigDict
from datetime import datetime
from uuid import UUID
# Base with common config
class AppSchema(BaseModel):
model_config = ConfigDict(
from_attributes=True, # ORM mode
str_strip_whitespace=True, # Auto-strip
validate_default=True, # Validate defaults too
)
# Input schemas (what the API accepts)
class UserCreate(AppSchema):
email: str = Field(..., pattern=r"^[w.-]+@[w.-]+.w+$")
name: str = Field(..., min_length=1, max_length=100)
password: str = Field(..., min_length=8, max_length=128)
class UserUpdate(AppSchema):
name: str | None = Field(None, min_length=1, max_length=100)
email: str | None = Field(None, pattern=r"^[w.-]+@[w.-]+.w+$")
# Output schemas (what the API returns)
class UserResponse(AppSchema):
id: UUID
email: str
name: str
created_at: datetime
# Note: password is NEVER in response schema
# List response with pagination
class PaginatedResponse[T](AppSchema):
items: list[T]
total: int
page: int
page_size: int
has_next: bool
```
### 8 Pydantic Rules
1. **Separate Create/Update/Response schemas** — never reuse input as output
2. **Never expose internal fields** — no passwords, internal IDs, or debug info in responses
3. **Use Field() for constraints** — min/max length, regex patterns, gt/lt for numbers
4. **Enable `from_attributes=True`** — for ORM model → schema conversion
5. **Use generics for wrappers** — `PaginatedResponse[T]`, `ApiResponse[T]`
6. **Validate at boundaries** — request body, query params, path params, headers
7. **Use computed fields** — `@computed_field` for derived values
8. **Document with examples** — `model_config = {"json_schema_extra": {"examples": [...]}}`
## Phase 4: Error Handling Architecture
### Structured Error Hierarchy
```python
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.status import (
HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND,
HTTP_409_CONFLICT, HTTP_422_UNPROCESSABLE_ENTITY,
HTTP_429_TOO_MANY_REQUESTS, HTTP_500_INTERNAL_SERVER_ERROR,
)
class AppError(Exception):
"""Base application error."""
def __init__(
self,
message: str,
code: str,
status_code: int = HTTP_500_INTERNAL_SERVER_ERROR,
details: dict | None = None,
):
self.message = message
self.code = code
self.status_code = status_code
self.details = details or {}
super().__init__(message)
class NotFoundError(AppError):
def __init__(self, resource: str, identifier: str | int):
super().__init__(
message=f"{resource} not found: {identifier}",
code="NOT_FOUND",
status_code=HTTP_404_NOT_FOUND,
details={"resource": resource, "identifier": str(identifier)},
)
class ConflictError(AppError):
def __init__(self, message: str, field: str | None = None):
super().__init__(
message=message, code="CONFLICT",
status_code=HTTP_409_CONFLICT,
details={"field": field} if field else {},
)
class AuthenticationError(AppError):
def __init__(self, message: str = "Invalid credentials"):
super().__init__(message=message, code="UNAUTHORIZED", status_code=HTTP_401_UNAUTHORIZED)
class AuthorizationError(AppError):
def __init__(self, message: str = "Insufficient permissions"):
super().__init__(message=message, code="FORBIDDEN", status_code=HTTP_403_FORBIDDEN)
class ValidationError(AppError):
def __init__(self, message: str, errors: list[dict] | None = None):
super().__init__(
message=message, code="VALIDATION_ERROR",
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
details={"errors": errors or []},
)
class RateLimitError(AppError):
def __init__(self, retry_after: int = 60):
super().__init__(
message="Rate limit exceeded", code="RATE_LIMITED",
status_code=HTTP_429_TOO_MANY_REQUESTS,
details={"retry_after": retry_after},
)
# Global error handler
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"details": exc.details,
}
},
)
# Register in app factory
# app.add_exception_handler(AppError, app_error_handler)
```
### 6 Error Handling Rules
1. **Never return bare strings** — always structured `{"error": {"code", "message", "details"}}`
2. **Use domain-specific errors** — `NotFoundError("User", user_id)` not `HTTPException(404)`
3. **Global handler catches all** — register `AppError` handler in app factory
4. **Log server errors, don't expose** — 5xx returns generic message, logs full traceback
5. **Include actionable details** — which field failed, what's allowed, retry-after for rate limits
6. **Never leak internals** — no stack traces, SQL queries, or file paths in responses
## Phase 5: Authentication & Authorization
### JWT + Dependency Injection Pattern
```python
from fastapi import Depends, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from datetime import datetime, timedelta, timezone
security = HTTPBearer()
def create_access_token(user_id: str, roles: list[str], settings: Settings) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
payload = {
"sub": user_id,
"roles": roles,
"exp": expire,
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, settings.jwt_secret.get_secret_value(), algorithm=settings.jwt_algorithm)
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
settings: Settings = Depends(get_settings),
db: AsyncSession = Depends(get_db),
) -> User:
try:
payload = jwt.decode(
credentials.credentials,
settings.jwt_secret.get_secret_value(),
algorithms=[settings.jwt_algorithm],
)
user_id = payload.get("sub")
if not user_id:
raise AuthenticationError("Invalid token payload")
except JWTError:
raise AuthenticationError("Invalid or expired token")
user = await db.get(User, user_id)
if not user:
raise AuthenticationError("User not found")
return user
# Role-based authorization
def require_role(*roles: str):
async def checker(user: User = Depends(get_current_user)) -> User:
if not any(r in user.roles for r in roles):
raise AuthorizationError(f"Requires one of: {', '.join(roles)}")
return user
return checker
# Usage in router
@router.get("/admin/users")
async def list_users(
admin: User = Depends(require_role("admin", "superadmin")),
service: UserService = Depends(get_user_service),
):
return await service.list_all()
```
### 10-Point Security Checklist
| # | Check | Priority |
|---|-------|----------|
| 1 | JWT secret ≥ 256 bits, from env | P0 |
| 2 | Token expiry ≤ 30 min for access, ≤ 7 days refresh | P0 |
| 3 | Password hashed with bcrypt/argon2 | P0 |
| 4 | CORS configured per environment | P0 |
| 5 | Rate limiting on auth endpoints | P0 |
| 6 | HTTPS enforced (redirect HTTP) | P0 |
| 7 | Security headers (HSTS, CSP, X-Frame) | P1 |
| 8 | Input validation on ALL endpoints | P1 |
| 9 | SQL injection prevented (parameterized queries) | P0 |
| 10 | Dependency scanning (safety/pip-audit) | P1 |
## Phase 6: Database Patterns
### Async SQLAlchemy + Repository Pattern
```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import select, func
from uuid import uuid4, UUID
from datetime import datetime, timezone
# Engine setup
engine = create_async_engine(
settings.database_url.get_secret_value(),
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
pool_timeout=settings.db_pool_timeout,
pool_pre_ping=True, # Check connection health
echo=settings.debug,
)
SessionFactory = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with SessionFactory() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# Base model with common fields
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
# Repository pattern
class BaseRepository[T]:
def __init__(self, session: AsyncSession, model: type[T]):
self.session = session
self.model = model
async def get_by_id(self, id: UUID) -> T | None:
return await self.session.get(self.model, id)
async def get_or_raise(self, id: UUID) -> T:
entity = await self.get_by_id(id)
if not entity:
raise NotFoundError(self.model.__name__, str(id))
return entity
async def list(
self, *, offset: int = 0, limit: int = 20, **filters
) -> tuple[list[T], int]:
query = select(self.model)
count_query = select(func.count()).select_from(self.model)
for field, value in filters.items():
if value is not None:
query = query.where(getattr(self.model, field) == value)
count_query = count_query.where(getattr(self.model, field) == value)
total = await self.session.scalar(count_query) or 0
result = await self.session.execute(
query.offset(offset).limit(limit).order_by(self.model.created_at.desc())
)
return list(result.scalars().all()), total
async def create(self, entity: T) -> T:
self.session.add(entity)
await self.session.flush()
return entity
async def delete(self, entity: T) -> None:
await self.session.delete(entity)
```
### ORM Selection Guide
| ORM | Best For | Async | Type Safety | Learning Curve |
|-----|----------|-------|-------------|----------------|
| **SQLAlchemy 2.0** | Complex queries, enterprise | ✅ | ✅ Mapped[] | Medium |
| **SQLModel** | Simple CRUD, Pydantic sync | ✅ | ✅ | Low |
| **Tortoise** | Django-like feel | ✅ | Partial | Low |
| **Piccolo** | Modern, migrations built-in | ✅ | ✅ | Low |
**Recommendation:** SQLAlchemy 2.0 for production. SQLModel for prototypes.
### Migration Strategy (Alembic)
```bash
# Setup
alembic init migrations
# Edit alembic.ini: sqlalchemy.url = from env
# Generate migration
alembic revision --autogenerate -m "add users table"
# Apply
alembic upgrade head
# Rollback
alembic downgrade -1
```
**Migration Rules:**
1. Always review autogenerated migrations before applying
2. Never edit applied migrations — create new ones
3. Test migrations in staging before production
4. Include `downgrade()` for every `upgrade()`
5. Use `batch_alter_table` for SQLite compatibility
## Phase 7: Testing Strategy
### Test Pyramid
| Level | Coverage Target | Tools | Focus |
|-------|----------------|-------|-------|
| Unit | 80%+ | pytest, unittest.mock | Service logic, validators |
| Integration | Key paths | pytest-asyncio, testcontainers | DB queries, external APIs |
| E2E | Critical flows | httpx.AsyncClient | Full request→response |
| Contract | API boundaries | schemathesis | OpenAPI compliance |
### Test Patterns
```python
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import create_app
@pytest.fixture
async def app():
app = create_app()
yield app
@pytest.fixture
async def client(app):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def auth_client(client, test_user):
token = create_access_token(test_user.id, test_user.roles)
client.headers["Authorization"] = f"Bearer {token}"
return client
# E2E test
@pytest.mark.asyncio
async def test_create_user(client: AsyncClient):
response = await client.post("/api/users", json={
"email": "test@example.com",
"name": "Test User",
"password": "securepass123",
})
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert "password" not in data # Never expose
# Unit test (service layer)
@pytest.mark.asyncio
async def test_user_service_duplicate_email(user_service, mock_repo):
mock_repo.get_by_email.return_value = existing_user
with pytest.raises(ConflictError, match="Email already registered"):
await user_service.create(UserCreate(email="taken@example.com", ...))
# Parametrized validation
@pytest.mark.parametrize("email,expected", [
("valid@example.com", True),
("invalid", False),
("", False),
("a@b.c", True),
])
def test_email_validation(email, expected):
if expected:
UserCreate(email=email, name="Test", password="12345678")
else:
with pytest.raises(ValidationError):
UserCreate(email=email, name="Test", password="12345678")
```
### 7 Testing Rules
1. **Test services, not routers** — business logic lives in services
2. **Use fixtures for DI override** — swap real DB with test DB via `app.dependency_overrides`
3. **One assertion per test** — clear what broke when it fails
4. **Test error paths** — 40% of tests should be sad-path
5. **Use factories for test data** — `UserFactory.create()` not manual dict construction
6. **Async tests need `@pytest.mark.asyncio`** — or set `asyncio_mode = "auto"` in config
7. **Run tests in CI** — block merge if tests fail
## Phase 8: Structured Logging & Observability
### Structlog Setup
```python
import structlog
from uuid import uuid4
from starlette.middleware.base import BaseHTTPMiddleware
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.stdlib.LoggerFactory(),
)
logger = structlog.get_logger()
# Request ID middleware
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid4()))
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(
request_id=request_id,
method=request.method,
path=request.url.path,
)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
logger.info(
"request_completed",
status_code=response.status_code,
)
return response
```
### Health Check Endpoints
```python
@router.get("/health")
async def health():
"""Liveness probe — is the process running?"""
return {"status": "ok"}
@router.get("/ready")
async def ready(db: AsyncSession = Depends(get_db)):
"""Readiness probe — can we serve traffic?"""
checks = {}
try:
await db.execute(text("SELECT 1"))
checks["database"] = "ok"
except Exception:
checks["database"] = "error"
all_ok = all(v == "ok" for v in checks.values())
return JSONResponse(
status_code=200 if all_ok else 503,
content={"status": "ok" if all_ok else "degraded", "checks": checks},
)
```
## Phase 9: Performance Optimization
### Priority Stack
| # | Technique | Impact | Effort |
|---|-----------|--------|--------|
| 1 | Async database queries | High | Low |
| 2 | Connection pooling (tuned) | High | Low |
| 3 | Response caching (Redis) | High | Medium |
| 4 | Background tasks for heavy work | High | Low |
| 5 | Pagination on all list endpoints | Medium | Low |
| 6 | Select only needed columns | Medium | Low |
| 7 | Eager loading (joinedload) | Medium | Medium |
| 8 | Rate limiting | Medium | Low |
### Background Tasks
```python
from fastapi import BackgroundTasks
@router.post("/users", status_code=201)
async def create_user(
user_in: UserCreate,
background_tasks: BackgroundTasks,
service: UserService = Depends(get_user_service),
):
user = await service.create(user_in)
background_tasks.add_task(send_welcome_email, user.email, user.name)
return user
```
### Caching Pattern
```python
from redis.asyncio import Redis
import json
class CacheService:
def __init__(self, redis: Redis):
self.redis = redis
async def get_or_set(self, key: str, factory, ttl: int = 300):
cached = await self.redis.get(key)
if cached:
return json.loads(cached)
result = await factory()
await self.redis.setex(key, ttl, json.dumps(result, default=str))
return result
async def invalidate(self, pattern: str):
keys = await self.redis.keys(pattern)
if keys:
await self.redis.delete(*keys)
```
## Phase 10: Production Deployment
### Multi-Stage Dockerfile
```dockerfile
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-editable
# Production stage
FROM python:3.12-slim
WORKDIR /app
RUN adduser --disabled-password --no-create-home appuser
COPY --from=builder /app/.venv /app/.venv
COPY src/ ./src/
COPY migrations/ ./migrations/
COPY alembic.ini ./
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD ["python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]
CMD ["uvicorn", "src.app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
```
### App Factory Pattern
```python
from fastapi import FastAPI
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logger.info("starting_up", environment=settings.environment)
await init_db()
yield
# Shutdown
logger.info("shutting_down")
await engine.dispose()
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(
title=settings.app_name,
lifespan=lifespan,
docs_url="/docs" if settings.debug else None,
redoc_url=None,
)
# Middleware (order matters — last added = first executed)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(RequestIDMiddleware)
# Error handlers
app.add_exception_handler(AppError, app_error_handler)
# Routers
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
app.include_router(users_router, prefix="/api/users", tags=["users"])
app.include_router(health_router, tags=["health"])
return app
app = create_app()
```
### GitHub Actions CI
```yaml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install uv && uv sync
- run: uv run ruff check .
- run: uv run mypy src/
- run: uv run pytest --cov=src --cov-report=xml -x
env:
DATABASE_URL: postgresql+asyncpg://postgres:test@localhost:5432/testdb
JWT_SECRET: test-secret-key-at-least-32-chars
```
### Production Checklist
**P0 — Mandatory:**
- [ ] All secrets from environment variables (SecretStr)
- [ ] HTTPS enforced
- [ ] CORS configured per environment
- [ ] Rate limiting on auth endpoints
- [ ] Input validation on all endpoints
- [ ] Structured error responses (no stack traces)
- [ ] Health + readiness endpoints
- [ ] Database connection pooling
- [ ] Migrations run before deploy
- [ ] Structured logging (JSON)
- [ ] Tests passing in CI
**P1 — Recommended:**
- [ ] OpenTelemetry tracing
- [ ] Prometheus metrics endpoint
- [ ] Background task queue (Celery/ARQ)
- [ ] Redis caching layer
- [ ] API versioning strategy
- [ ] Request/response logging
- [ ] Dependency security scanning
- [ ] Performance benchmarks established
## Phase 11: Advanced Patterns
### Middleware Stack Order
```python
# Applied bottom-to-top (last added = first executed)
app.add_middleware(GZipMiddleware, minimum_size=1000) # 5. Compress
app.add_middleware(CORSMiddleware, ...) # 4. CORS
app.add_middleware(RequestIDMiddleware) # 3. Request ID
app.add_middleware(RateLimitMiddleware) # 2. Rate limit
app.add_middleware(TrustedHostMiddleware, allowed=["*"]) # 1. Host check
```
### Pagination with Cursor-Based Option
```python
from fastapi import Query
class PaginationParams:
def __init__(
self,
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
):
self.offset = (page - 1) * page_size
self.limit = page_size
self.page = page
self.page_size = page_size
@router.get("/users", response_model=PaginatedResponse[UserResponse])
async def list_users(
pagination: PaginationParams = Depends(),
service: UserService = Depends(get_user_service),
):
items, total = await service.list(
offset=pagination.offset, limit=pagination.limit
)
return PaginatedResponse(
items=items, total=total,
page=pagination.page, page_size=pagination.page_size,
has_next=(pagination.offset + pagination.limit) < total,
)
```
### WebSocket Pattern
```python
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.connections: dict[str, WebSocket] = {}
async def connect(self, user_id: str, ws: WebSocket):
await ws.accept()
self.connections[user_id] = ws
def disconnect(self, user_id: str):
self.connections.pop(user_id, None)
async def send(self, user_id: str, message: dict):
if ws := self.connections.get(user_id):
await ws.send_json(message)
manager = ConnectionManager()
@router.websocket("/ws/{user_id}")
async def websocket_endpoint(websocket: WebSocket, user_id: str):
await manager.connect(user_id, websocket)
try:
while True:
data = await websocket.receive_json()
# Process message
except WebSocketDisconnect:
manager.disconnect(user_id)
```
### File Upload Pattern
```python
from fastapi import UploadFile, File
@router.post("/upload")
async def upload_file(
file: UploadFile = File(..., description="File to upload"),
user: User = Depends(get_current_user),
):
# Validate
if file.size and file.size > 10 * 1024 * 1024: # 10MB
raise ValidationError("File too large (max 10MB)")
allowed_types = {"image/jpeg", "image/png", "application/pdf"}
if file.content_type not in allowed_types:
raise ValidationError(f"File type not allowed: {file.content_type}")
# Save
contents = await file.read()
path = f"uploads/{user.id}/{file.filename}"
# Save to S3/local storage...
return {"filename": file.filename, "size": len(contents)}
```
## Phase 12: Common Mistakes
| # | Mistake | Fix |
|---|---------|-----|
| 1 | Sync database calls in async app | Use async SQLAlchemy/databases |
| 2 | Business logic in route handlers | Move to service layer |
| 3 | No input validation | Pydantic models on every endpoint |
| 4 | Returning ORM models directly | Use response schemas (from_attributes) |
| 5 | Hardcoded config values | Pydantic Settings + env vars |
| 6 | No error handling strategy | Custom exception hierarchy + global handler |
| 7 | Missing health checks | /health + /ready endpoints |
| 8 | `print()` for logging | structlog with JSON output |
| 9 | No pagination on list endpoints | Default limit, max cap (100) |
| 10 | Testing against production DB | Test fixtures with separate DB |
## Quality Scoring (0–100)
| Dimension | Weight | 0–25 | 50 | 75 | 100 |
|-----------|--------|------|----|----|-----|
| Type Safety | 15% | No types | Partial Pydantic | Full schemas | Strict mypy pass |
| Error Handling | 15% | Bare HTTPException | Custom errors | Full hierarchy | + monitoring |
| Testing | 15% | None | Happy path | 80%+ coverage | + contract tests |
| Security | 15% | No auth | Basic JWT | + RBAC + rate limit | + scanning + audit |
| Performance | 10% | Sync everything | Async DB | + caching | + profiling |
| Observability | 10% | print() | Structured logs | + tracing | + metrics + alerts |
| Database | 10% | Raw SQL | ORM + migrations | + repository pattern | + connection tuning |
| Deployment | 10% | Manual | Dockerfile | + CI/CD | + health + rollback |
**Scoring:** Your Score = Σ (dimension score × weight). **< 40 = critical, 40–60 = needs work, 60–80 = solid, 80+ = production-grade.**
## 10 Commandments of FastAPI Production
1. **Pydantic models at every boundary** — request, response, config
2. **Async all the way down** — one sync call blocks the event loop
3. **Services own business logic** — routers are thin wrappers
4. **Dependency injection for testability** — `Depends()` is your best friend
5. **Structured errors, structured logs** — JSON everything
6. **Health checks are non-negotiable** — liveness + readiness
7. **Test the sad paths** — 40% of tests should be error cases
8. **Migrations before deployment** — never modify schema manually
9. **Secrets in environment, never in code** — `SecretStr` enforces this
10. **Profile before optimizing** — measure, don't guess
## Natural Language Commands
- `audit my FastAPI project` → Run health check, identify gaps
- `set up a new FastAPI project` → Generate project structure + config
- `add authentication to my API` → JWT + RBAC dependency pattern
- `create a CRUD feature for [resource]` → Full router/service/repo/schemas
- `optimize my database queries` → Connection pooling + async + N+1 prevention
- `add structured logging` → Structlog + request ID middleware
- `write tests for [feature]` → Async test patterns + fixtures
- `prepare for production deployment` → Dockerfile + CI + checklist
- `add caching to my API` → Redis caching pattern
- `set up error handling` → Custom exception hierarchy + global handler
- `add WebSocket support` → Connection manager pattern
- `review my API security` → 10-point security checklist audit
---
⚡ **Level up your FastAPI APIs** → Get the [AfrexAI SaaS Context Pack ($47)](https://afrexai-cto.github.io/context-packs/) for complete SaaS architecture, pricing strategies, and go-to-market playbooks.
🔗 **More free skills by AfrexAI:**
- [afrexai-python-production](https://clawhub.com/skills/afrexai-python-production) — Python production engineering
- [afrexai-api-architecture](https://clawhub.com/skills/afrexai-api-architecture) — API design methodology
- [afrexai-database-engineering](https://clawhub.com/skills/afrexai-database-engineering) — Database patterns
- [afrexai-test-automation-engineering](https://clawhub.com/skills/afrexai-test-automation-engineering) — Testing strategy
- [afrexai-cicd-engineering](https://clawhub.com/skills/afrexai-cicd-engineering) — CI/CD pipeline design
🛒 Browse all packs → [AfrexAI Storefront](https://afrexai-cto.github.io/context-packs/)