FastAPI standards and best practices for this project. Use when adding or changing API routes, services, dependencies, or middleware so the team follows the same patterns and conventions.
backend/. Python also in services/; same style applies. After edits run make format and make lint from repo root..cursor/rules/fastapi-python-best-practices.md, .cursor/rules/fastapi-patterns.md, .cursor/rules/standards.md, .cursor/rules/project.md..cursor/skills/README.md.Standards derived from this project’s implementation. Follow these so the codebase stays consistent and maintainable.
backend/app/api/v1/endpoints/ — one file per domain (e.g. , , ). Only HTTP concerns: parse request, call service/repo via , return response or raise . Use service for write operations (POST/PUT/PATCH/DELETE) and service for read operations (GET). See .data.pydatabase.pymetrics.pyDependsHTTPException.cursor/rules/cqrs.mdbackend/app/services/ — use command services for writes and query services for reads (CQRS). Services receive repositories via constructor; use AsyncSession for DB when in the API path. No raw request/response objects.backend/app/repositories/ — use write repositories (create/update/delete) and read repositories (get/list) per domain. Repositories receive AsyncSession; perform queries and commits. No business rules.backend/app/api/v1/deps.py — define get_* functions that return service or repository instances (injecting get_async_session). Use these in route handlers via Depends(...).backend/app/models/database.py (SQLModel table models), backend/app/models/schemas.py (Pydantic request/response). DB name is fastapi_db.Do not: Put SQL or business logic in endpoint files; put HTTP logic in services or repositories.
router = APIRouter(prefix="/data", tags=["data"])
backend/app/api/v1/__init__.py on the v1 router (prefix="/api/v1").response_model and status_code on route decorators:
@router.post("/process", response_model=TaskResult, status_code=200)
async def process_data(request: DataRequest, service: DataService = Depends(get_data_service)) -> TaskResult:
get_async_session from app.core.database for any route or dependency that touches the DB. Never create a raw engine or session in an endpoint.app.api.v1.deps that depend on get_async_session and return a service or repository instance. Use them in endpoints via Depends(get_data_service), Depends(get_task_repository), etc.Example (from deps.py):
def get_data_service(session: AsyncSession = Depends(get_async_session)) -> DataService:
"""Dependency that returns a DataService instance (async session)."""
return DataService(session)
app.models.schemas (e.g. DataRequest). Declare them as the route parameter; FastAPI validates and parses.TaskResult, RecordResponse, TaskLogResponse) and set response_model= on the decorator. Never return a SQLModel/ORM instance directly; map to a schema and return that (e.g. RecordResponse.model_validate(r) or RecordResponse(**record)).await asyncio.to_thread(blocking_func, ...) and keep the rest of the flow async (e.g. DB write). See DataService.process_data_sync_and_save in app.services.data_service.init_db). Do not use them in request handlers.detail. Do not let uncaught exceptions bubble to the client without mapping.None/result; let the endpoint translate to HTTP (e.g. if not record: raise HTTPException(status_code=404, detail="Record not found")).call_service_via_rabbitmq) raises ValueError, catch it in the endpoint and map to HTTPException(status_code=503, detail=str(e)) (or another suitable code)..cursor/rules/error-handling.md.main.py (e.g. CORSMiddleware). Keep origins/methods/headers explicit for production.Idempotency-Key header on POST endpoints that support it; implemented in middleware (IdempotencyMiddleware). Do not duplicate idempotency logic in services. Paths are listed in the middleware (e.g. /api/v1/data/process, /api/v1/data/process-async).init_db()), @app.on_event("startup") is acceptable. For multi-step startup/shutdown, prefer a lifespan context manager.get_async_session and pass it to services/repositories. Use connection pooling (already configured in app.core.database).AsyncSession in the constructor; use await session.commit(), await session.refresh(record) after writes; use select(Model).where(...) and await session.execute() / session.get() for reads. Avoid N+1; batch or eager load when needed.app.models.database; then from backend/ run alembic revision --autogenerate -m "description" and alembic upgrade head. Do not change the database name from fastapi_db.data_service.py, task_repository.py, data.py for the data router).router. Group related routes in one file (e.g. all /data/* in data.py).DataRequest) and response (e.g. TaskResult, RecordResponse). Use type hints and optional fields where appropriate.backend/app/api/v1/endpoints/ and are mounted in api/v1/__init__.py.Depends(get_async_session) or Depends(get_*_service) / Depends(get_*_repository) from deps.py; no ad-hoc session or service creation in routes.schemas.py; responses use response_model= and never return raw ORM instances.asyncio.to_thread.make format and make lint have been run from repo root and pass.| Concern | Where / how |
|---|---|
| Add route | backend/app/api/v1/endpoints/<domain>.py + include in api/v1/__init__.py |
| Add dependency | backend/app/api/v1/deps.py (e.g. get_*_service, get_*_repository) |
| Request/response models | backend/app/models/schemas.py |
| DB models | backend/app/models/database.py + Alembic migration |
| Business logic | backend/app/services/ (accept session/repos in constructor) |
| Data access | backend/app/repositories/ (accept AsyncSession) |
| Blocking in request path | await asyncio.to_thread(sync_func, ...) then continue async |
| Idempotency | Middleware; add path to IDEMPOTENT_PATHS if needed |
For more detail, see .cursor/rules/fastapi-python-best-practices.md and .cursor/rules/standards.md.