FastAPI Project Rules
FastAPI conventions for Pydantic v2 schemas, Depends-based wiring, async discipline, and a predictable router layout.
Use when: Use in FastAPI services with Pydantic v2 to keep routers thin, dependencies explicit, and blocking calls out of the event loop.
The skill file
Cursor · .cursorrules.cursorrules3356 chars
You are working on a FastAPI service with Pydantic v2, SQLAlchemy 2.0 async, and Python 3.12.
## Router layout
- One router per resource in `app/routers/<resource>.py`, created as `router = APIRouter(prefix="/items", tags=["items"])` and included in `app/main.py`. No routes defined on the app object directly.
- Path functions stay under ~15 lines: validate via types, call one service function, return a schema. Business logic lives in `app/services/`, DB access in `app/repositories/`.
- Every route declares `response_model` (or a return annotation) and explicit `status_code` for non-200 responses (`201` for creates, `204` for deletes).
- Use module structure `app/routers`, `app/services`, `app/repositories`, `app/schemas`, `app/models`, `app/core` (config, security, deps). No `helpers.py` grab-bag.
## Pydantic models
- Pydantic models are the API contract only — never return SQLAlchemy models from a route. Convert with `model_config = ConfigDict(from_attributes=True)`.
- Three schemas per resource, suffixed by role: `ItemCreate`, `ItemUpdate` (all fields optional), `ItemRead`. Never reuse `ItemCreate` as a response model.
- Constrain fields at the schema, not in handler code: `name: str = Field(min_length=1, max_length=120)`, `Annotated[int, Field(ge=1, le=100)]` for page sizes.
- Pydantic v2 idioms only: `model_validate`, `model_dump(mode="json")`, `field_validator`. Flag any `.dict()`, `.parse_obj()`, or `class Config` as legacy and rewrite.
- Settings come from one `Settings(BaseSettings)` class in `app/core/config.py`, accessed through a cached `get_settings()` dependency — no `os.environ` reads elsewhere.
## Dependency injection
- Anything a route needs (DB session, current user, settings, pagination) arrives via `Depends`, declared with `Annotated` aliases defined once in `app/core/deps.py`: `SessionDep = Annotated[AsyncSession, Depends(get_session)]`.
- `get_session` is an async generator that yields the session and commits/rolls back in `finally`. Routes and services never call `commit()` on their own session lifecycle.
- Auth is a dependency (`CurrentUser = Annotated[User, Depends(get_current_user)]`), never an `if` check inside a handler. Authorization failures raise `HTTPException(403)` inside the dependency.
- No module-level singletons constructed at import time (DB engines excepted). Construct clients in `lifespan` and expose them through dependencies so tests can override with `app.dependency_overrides`.
## Async discipline
- All path functions are `async def`. Inside them, never call blocking I/O (`requests`, `time.sleep`, sync DB drivers, `open()` on large files) — use `httpx.AsyncClient`, `asyncio.sleep`, and the async session.
- Unavoidably sync/CPU-bound work goes through `await run_in_threadpool(fn, ...)`, not inline.
- Fire independent awaits concurrently with `asyncio.gather`; sequential awaits on independent calls are a defect.
- Background work that must survive the response uses a task queue (or at minimum `BackgroundTasks`) — never a bare `asyncio.create_task` that nothing supervises.
## Errors
- Raise `HTTPException` only in routers and dependencies. Services raise domain exceptions (`ItemNotFoundError`), translated by exception handlers registered in `main.py`.
- Error responses always carry `{"detail": ...}`; never return a 200 with an error payload inside.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursorrules
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…