# Pathrule Pattern: FastAPI (Python) (1.0.0)
# ::pathrule:package:fastapi

### [RULE] Type every request and response with a Pydantic model  (path: /app/schemas)
<!-- scope: folder | priority: high | strict -->

FastAPI's validation, serialization, and OpenAPI docs all flow from type hints. An untyped `dict` body opts out of all three and ships unvalidated input straight into your code.

- Type every request body, query, and path parameter. Use a Pydantic `BaseModel` for bodies; never declare a parameter as a bare `dict` or `Any`.
- Set `response_model=` (or a typed return annotation) on each route so output is validated and filtered. This is also what stops an ORM object from leaking password hashes or internal fields into the response.
- Keep separate models for input and output (e.g. `UserCreate` vs `UserRead`); do not reuse one model that both accepts a password and returns the row.
- Use Pydantic v2 idioms: `model_config = ConfigDict(from_attributes=True)` to read from ORM objects, `Field(...)` for constraints and examples. Validate at the boundary so the rest of the handler works with trusted, typed data.

---

### [RULE] Do not block the event loop in async routes  (path: /app/routers)
<!-- scope: folder | priority: high | strict -->

An `async def` route runs on the event loop. One blocking call inside it freezes every concurrent request on that worker, turning a fast server into a slow one under load.

- In an `async def` route, only `await` async-native I/O (async DB driver, `httpx.AsyncClient`, async cache client). Never call a synchronous, blocking client (`requests`, a sync DB cursor, `time.sleep`) directly inside it.
- If a dependency is only available as blocking code, either define the route as a plain `def` (FastAPI runs it in a threadpool automatically) or offload the blocking call with `anyio.to_thread.run_sync` / `run_in_executor`.
- Do CPU-bound work (image processing, heavy parsing) off the event loop - a threadpool for the GIL-friendly cases, a process pool or a background worker/queue for the rest. Do not grind CPU inside an async route.
- Be consistent: an async route calling a sync function calling async code is where event-loop bugs hide. Pick async or sync per route and keep the chain coherent.

---

### [RULE] Inject dependencies with Depends; no module globals for request state  (path: /app)
<!-- scope: folder | priority: high | advisory -->

FastAPI's dependency injection is how request-scoped state stays correct and testable. A module global shared across requests is a race condition and an untestable seam.

- Provide the database session, the authenticated user, pagination, and config as dependencies (`db: Session = Depends(get_db)`, `user: User = Depends(get_current_user)`). Do not store the session or current user in a module-level variable.
- Make the session dependency yield-based so setup and teardown (commit/rollback/close) are guaranteed per request: `def get_db(): db = SessionLocal(); try: yield db; finally: db.close()`.
- Compose dependencies for cross-cutting concerns (auth, role checks, rate limits) and attach them at the router level with `dependencies=[Depends(...)]` when every route needs them.
- Override dependencies in tests with `app.dependency_overrides` to inject a test DB or a fake user. This only works if state actually flows through `Depends`, which is the point.

---

### [RULE] Change the schema only through forward-only Alembic migrations  (path: /app)
<!-- scope: folder | priority: high | advisory -->

The database schema is shared state across every deploy and environment. An untracked change is a deploy that works on one machine and breaks on the next.

- Generate a migration for every model change with `alembic revision --autogenerate`, then read and edit it. Autogenerate misses some changes (type tweaks, server defaults, constraints, enums); never ship the generated file unread.
- Do not rely on `Base.metadata.create_all()` in production. It is fine for a test fixture; it does not evolve an existing schema and will silently drift from your migrations.
- Write migrations forward-only and deploy-safe: add columns nullable or with a default, backfill, then enforce constraints in a later migration. Avoid a single migration that locks a large table for the whole deploy.
- Commit migrations with the code change that needs them, run them as a gated step before the new code serves traffic, and keep one linear history (resolve multiple heads before merging).

---

### [MEMORY] Project layout and app wiring  (path: /app)

We keep a FastAPI service organized by responsibility so routes stay thin and logic stays testable.

- Layout: `app/main.py` (create the app, include routers), `app/routers/` (one `APIRouter` per resource), `app/schemas/` (Pydantic models), `app/models/` (ORM models), `app/services/` (business logic), `app/deps.py` (shared dependencies), `app/core/` (settings, security).
- Routes stay thin: validate via the schema, delegate to a service function, return a typed response. Business logic and DB queries live in services, not in the route handler.
- Mount feature routers with `app.include_router(router, prefix='/users', tags=['users'])`; group related endpoints under one `APIRouter` with shared dependencies.
- Use the `lifespan` async context manager for startup/shutdown (DB pool, clients) rather than the deprecated `@app.on_event` hooks. Acquire resources on enter, release on exit.
- Run with `uvicorn` (one worker per process; scale with multiple workers behind a process manager or `gunicorn -k uvicorn.workers.UvicornWorker`).

See /app for the settings memory and the Pydantic, async, and DI rules.

---

### [MEMORY] Configuration via pydantic-settings  (path: /app/core)

Configuration is typed and centralized, not `os.environ.get` calls sprinkled through the codebase.

- Define a `Settings(BaseSettings)` model with `pydantic-settings`, typing each field (`database_url: str`, `jwt_secret: str`, `debug: bool = False`). It loads from environment variables and a `.env` file and validates types at startup.
- Provide settings as a cached dependency (`@lru_cache` on a `get_settings()` factory, exposed via `Depends`) so the app reads and validates config once and tests can override it.
- Keep secrets out of the repo: `.env` is git-ignored and injected at runtime per environment. A missing required setting should fail fast at startup, not at the first request that needs it.
- Read config only through the settings object, never `os.environ` directly in handlers or services, so every configurable value is discoverable in one typed place.

See /app for the project layout memory; see the secrets-env-management pattern for rotation and injection.

---

### [SKILL] fastapi-endpoint-checklist  (path: /)

---
name: fastapi-endpoint-checklist
description: Checklist for adding or changing a FastAPI endpoint. Run before merging any router, schema, dependency, or model change.
---

# FastAPI endpoint checklist

- [ ] Request body/query/path params are typed; bodies use a Pydantic model - no bare `dict`/`Any`.
- [ ] `response_model` (or typed return) is set; input and output models are separate so internal fields don't leak.
- [ ] Route is `async def` only if it awaits non-blocking I/O; any blocking/CPU work runs in a threadpool, a `def` route, or a worker.
- [ ] DB session and current user come through `Depends`; the session dependency is yield-based with commit/rollback/close.
- [ ] Auth/role checks attached as router or route dependencies; handler authorizes the specific action.
- [ ] Correct status codes (`201` on create, `204` on delete) and `HTTPException` for expected failures with a consistent error shape.
- [ ] Logic lives in a service function; the route stays thin (validate → delegate → return).
- [ ] Schema changes ship as a reviewed Alembic migration (autogenerate then read/edit); no `create_all` in prod; one linear migration history.
- [ ] Config is read from the pydantic-settings object, not `os.environ` inline.
- [ ] Tests override dependencies via `app.dependency_overrides` (test DB / fake user).
