Initial extracted documentation set

This commit is contained in:
Rodin
2026-06-01 21:42:05 +00:00
commit a23e494026
16 changed files with 1414 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
# FastAPI Conventions Process
This file documents the workflow used to build and refine this repo so the extraction can be repeated without guesswork.
## Goal
Turn repeated FastAPI-adjacent service patterns from mature upstream codebases into concise convention docs with verifiable citations.
## Scope split
Keep this repo at the **framework/service-boundary** level.
Good fits:
- route shape and handler thinness
- dependency injection and request-scoped wiring
- request/response schema boundaries
- persistence/session handling in web services
- app lifespan and startup/shutdown wiring
- service testing at the HTTP/ASGI boundary
Do **not** re-document generic Python guidance here unless FastAPI meaningfully bends it.
## Upstream selection rule
Use a mixed source set: framework internals plus at least one or two app-style repos.
Current first-wave set:
- `fastapi/fastapi`
- `encode/starlette`
- `pydantic/pydantic`
- `encode/httpx`
Current refinement/app-layer set:
- `fastapi-users/fastapi-users`
- `fastapi/full-stack-fastapi-template`
Selection criteria:
- respected and maintained
- reveals real service-boundary behavior, not just toy usage
- enough tests/examples to show lifecycle and caveats
- includes both intended abstractions and pragmatic app wiring
## Directory contract
- `sources/` = raw evidence notes, one file per upstream repo
- `patterns/` = synthesized conventions from repeated evidence
- `comparison/` = explicit notes on where FastAPI conventions differ from broader Python patterns
## Step-by-step workflow
### 1) Separate framework conventions from Python-wide rules
That split keeps guidance crisp and prevents vague “it depends” docs.
### 2) Make upstream code available locally
Local checkouts make `file:line` verification cheap and keep the process grounded.
### 3) Write source notes first
For each upstream repo, create `sources/<repo>.md` with:
- why the repo was chosen
- repeated patterns
- caveats/counterexamples
- exact `file:line` citations
- pattern candidates supported by the evidence
Good source notes are dense evidence, not polished guidance.
### 4) Synthesize conventions from the strongest repeated signals
Start with the topics where evidence is strongest.
In this repo that meant:
- routes
- dependencies
- errors
- testing
- pydantic boundaries
- persistence
Each convention doc should usually include:
- the convention
- why it exists
- where to apply it
- where not to cargo-cult it
- preferred shapes
- counterexamples
- source signals/citations
### 5) Add comparison notes where FastAPI bends general Python guidance
Examples:
- dependency injection via callables/signatures instead of constructor wiring
- request/response model boundaries as framework-facing objects
- lifespan hooks instead of generic entrypoint/context patterns
### 6) Refine before broadening
After the first useful convention set exists, do not rush to add more repos.
Instead:
- improve convention docs in fresh contexts
- strengthen citations
- rewrite `sources/*.md` so they are denser and more reusable
- reduce duplicated guidance across docs
- preserve subtle framework caveats that are easy to flatten away
That was the right next move for this repo once the first source base existed.
## Fresh-context refinement pattern
A good refinement split is:
- one fresh pass over convention docs
- one fresh pass over source-note files
- one fresh pass doing citation audit across both
## Review checklist
### For source notes
- Does the file distinguish repeated conventions from isolated examples?
- Does it preserve framework caveats and edge cases?
- Are citations exact and fast to verify?
- Does it avoid vague claims that are not source-backed?
### For convention docs
- Is the guidance really FastAPI/service-boundary specific?
- Is the framework-owned behavior described at the right seam?
- Are testing and dependency recommendations grounded in actual code/tests?
- Are exceptions and tradeoffs preserved instead of erased?
## Local git workflow used here
When the repo is ready for human review:
1. initialize a local git repo
2. stage the current documentation set
3. create a single initial commit so review has a stable baseline
This repo intentionally avoids pushing or creating remotes unless explicitly requested.
## How to repeat this process next time
1. Define the scope split first.
2. Pick a compact but high-signal upstream set.
3. Build `sources/` before `patterns/`.
4. Synthesize the strongest conventions first.
5. Add comparison notes where the framework bends Python defaults.
6. Run a fresh-context refinement wave.
7. Initialize git only when the repo is reviewable.
## What to avoid
- documenting FastAPI from memory or tutorial vibes
- mixing Python-wide guidance into this repo
- broadening source coverage before tightening evidence quality
- flattening dependency/lifespan/testing caveats into one-size-fits-all rules
- weak citations that are annoying to re-check
+73
View File
@@ -0,0 +1,73 @@
# FastAPI Conventions
**Descriptive first, prescriptive second.** This repo captures how mature FastAPI-adjacent codebases structure web services, then distills the conventions worth following.
Use this repo for framework and service-boundary concerns that do **not** belong in a language-level Python patterns repo.
## Structure
- `patterns/` — route, dependency, schema, persistence, error, and testing conventions
- `comparison/` — where FastAPI conventions differ from broader Python patterns
- `sources/` — upstream notes, code excerpts, and rationale
- `PROCESS.md` — the repeatable extraction/refinement workflow used to build this repo
## Current source base
Primary upstreams mined so far:
- `fastapi/fastapi`
- `encode/starlette`
- `pydantic/pydantic`
- `encode/httpx`
- `fastapi-users/fastapi-users`
- `fastapi/full-stack-fastapi-template`
Why this mix works:
- FastAPI: router composition, dependencies, semantic exceptions, override-based testing seams
- Starlette: lifespan, middleware, exception-layer mechanics, application assembly
- Pydantic: request/response model boundaries and validation/serialization caveats
- HTTPX: ASGI and transport-based testing seams
- FastAPI Users: dependency-driven auth and router factory patterns
- Full-stack FastAPI Template: real service module structure, session/auth wiring, and schema transitions
## Current convention set
- `patterns/routes.md`
- `patterns/dependencies.md`
- `patterns/pydantic-boundaries.md`
- `patterns/errors.md`
- `patterns/persistence.md`
- `patterns/testing.md`
- `comparison/fastapi-vs-python.md`
## What belongs here
Examples:
- thin route handlers vs embedded business logic
- request/response schema boundaries
- dependency injection shape and lifetime
- error envelope and exception translation
- app startup/shutdown and resource wiring
- sync/async boundaries at the HTTP layer
## What does **not** belong here
Do not duplicate generic Python rules unless FastAPI deliberately bends them. Link back to `python-patterns` instead.
## Reviewing this repo
Recommended review order:
1. `README.md`
2. `PROCESS.md`
3. `sources/*.md` for evidence quality
4. `patterns/*.md` for synthesis quality
5. `comparison/*.md` for split-of-concerns clarity
Questions to ask during review:
- Is the guidance actually FastAPI/service-boundary specific?
- Are framework seams described where the framework really owns behavior?
- Are testing and dependency claims grounded in real source and tests?
- Are caveats preserved instead of over-generalized?
## Core rule
Do not write convention docs from memory. First collect repeated upstream examples with `file:line` citations, then synthesize the convention.
+9
View File
@@ -0,0 +1,9 @@
# FastAPI vs Python
Use this to capture places where FastAPI/Starlette conventions deliberately differ from broader Python guidance.
Examples to watch for:
- DI through callables and annotations instead of explicit constructor wiring
- framework-facing Pydantic models at transport boundaries
- async-first request handlers even when core logic stays sync
- startup/lifespan hooks instead of plain context management entrypoints
+135
View File
@@ -0,0 +1,135 @@
# Dependencies
Use FastAPI dependencies for request-scoped wiring: sessions, auth, permission checks, and other collaborators that should be assembled by the framework before the handler runs.
## Why this convention exists
Dependencies are FastAPI's main request-time composition tool. They let you:
- keep wiring visible in the handler signature
- reuse auth and resource access across routes
- give tests a clean override seam
- manage `yield`-based resources with framework-controlled lifetime
That is better than opening sessions, parsing tokens, or repeating permission logic inside every route body.
## The convention
1. Put reusable request wiring in dependency callables.
2. Use `Annotated[..., Depends(...)]` aliases when they make signatures easier to read.
3. Keep handlers consuming already-shaped collaborators.
4. Raise semantic HTTP errors inside dependencies when the failure is inherently about the request or caller.
5. Use lifespan for app-wide startup resources; use dependencies for per-request access to them.
## Follow it when the concern is request-scoped
Typical fits:
- DB/session access
- current-user lookup
- permission gates
- token/header parsing
- reusable request validation
## Do not over-abstract it
Do *not* create a dependency just because a helper exists.
Skip dependency injection when:
- the logic is local to one route and trivial
- the helper is plain business logic with no FastAPI concerns
- the abstraction makes the signature harder to understand than the inline code would
Also avoid turning the dependency graph into a maze. If a reader has to chase six `Depends(...)` calls just to find the session, the abstraction is working against you.
## Preferred shape
```python
from typing import Annotated
from fastapi import Depends, HTTPException
def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_db)]
def get_current_user(session: SessionDep, token: TokenDep) -> User:
user = load_user_from_token(session, token)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
@router.get("/items/{id}")
def read_item(session: SessionDep, current_user: CurrentUser, id: UUID) -> ItemPublic:
...
```
Why this works:
- the route tells you exactly what it needs
- auth and session logic are reusable
- tests can swap dependencies without patching route internals
## Counterexamples
### Hidden wiring inside handlers
```python
@router.get("/items/{id}")
def read_item(id: UUID):
session = Session(engine)
token = parse_auth_header_somehow()
user = load_user(session, token)
...
```
Bad because resource and auth wiring are duplicated and easy to get inconsistently wrong.
### Business logic buried in dependencies
```python
def get_processed_report(...) -> Report:
# loads DB, runs domain rules, emails people, mutates state, etc.
```
Bad because dependencies should shape request collaborators, not become a second invisible service layer.
### Startup work disguised as a dependency
If initialization is app-wide and long-lived, it belongs in lifespan, not in a request dependency that quietly performs setup.
## Source signals
### FastAPI
- `docs_src/bigger_applications/app_an_py310/dependencies.py:6-13` uses a dedicated dependency callable for request validation and raises an HTTP error there.
- `fastapi/routing.py:95-136` wraps request handling in `AsyncExitStack`, which is the mechanism FastAPI uses to manage dependency lifetime, including `yield`-based dependencies.
- `tests/test_dependency_security_overrides.py:24-29` defines a route whose collaborators come from `Security(...)` and `Depends(...)` rather than inline setup.
- `tests/test_dependency_security_overrides.py:44-63` replaces those collaborators through `app.dependency_overrides`, which is the testing seam FastAPI expects you to use.
### Full-stack FastAPI Template
- `backend/app/api/deps.py:21-27` defines a yielded DB session dependency and exposes it as `SessionDep`.
- `backend/app/api/deps.py:30-57` decodes auth, loads the user, and raises semantic HTTP errors in a reusable current-user dependency.
- `backend/app/api/routes/items.py:13-16` shows the result in practice: the route signature receives `SessionDep` and `CurrentUser` directly.
### Starlette and FastAPI Users
- `starlette/applications.py:46-48` says lifespan is the preferred replacement for `on_startup` and `on_shutdown`, reinforcing the split between app-wide resource setup and request-time dependencies.
- `examples/beanie-oauth/app/app.py:17-28` initializes Beanie in lifespan.
- `examples/beanie-oauth/app/app.py:60-62` protects a route with `Depends(current_active_user)` instead of doing auth lookup inline.
## Bottom line
Dependencies should make request wiring more obvious, more reusable, and easier to test.
If a dependency hides core business behavior, it is probably the wrong abstraction.
+121
View File
@@ -0,0 +1,121 @@
# Errors
Use handlers and dependencies to signal HTTP meaning clearly, and let FastAPI or Starlette own the wire format.
## Why this convention exists
FastAPI sits at a boundary where several kinds of failures meet:
- invalid requests
- permission failures
- missing resources
- domain or infrastructure errors
- framework/runtime exceptions
Good code keeps those concerns separate.
The stable pattern is:
- handlers and dependencies raise semantic HTTP exceptions for boundary failures
- request validation errors come from FastAPI and Pydantic automatically
- exception handlers turn those failures into HTTP responses
- tests assert the HTTP contract, not private exception plumbing
That keeps route code small and error responses consistent.
## The convention
1. Raise `HTTPException` for request-level failures such as 400, 401, 403, and 404.
2. Let FastAPI and Pydantic produce validation errors instead of hand-rolling error dicts.
3. Centralize error-envelope formatting in exception handlers when the API needs a stable shape.
4. Keep internal exception details out of client responses unless that disclosure is part of the contract.
5. Keep domain exceptions framework-agnostic and translate them at the HTTP boundary.
## Follow it at the HTTP boundary
Use this whenever the failure is about what the caller sent, what the caller can access, or what the API contract should look like on failure.
## Avoid these mistakes
Do *not* raise `HTTPException` from deep reusable domain code.
Do *not* build ad hoc error JSON in each route.
Do *not* catch every exception just to expose `str(exc)` to clients.
## Preferred shape
```python
@router.get("/{id}")
def read_item(session: SessionDep, current_user: CurrentUser, id: UUID) -> ItemPublic:
item = session.get(Item, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
if item.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return item
```
Why this works:
- the failure is explicit at the boundary
- the status code carries the meaning
- the framework serializes it consistently
## Counterexamples
### Returning error dicts from handlers
```python
return {"error": "not_found", "message": "Item not found"}
```
Bad because success and failure flows blur together and status handling becomes inconsistent.
### Framework exceptions in core domain code
```python
class BillingService:
def charge(...):
raise HTTPException(status_code=402, detail="Payment required")
```
Bad because domain code is now coupled to one web framework.
### Catch-all 500 handling that leaks internals
```python
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))
```
Bad because it leaks internal details and destroys useful operational distinctions.
## Source signals
### FastAPI
- `fastapi/exception_handlers.py:11-26` is the core citation here: `HTTPException` becomes a JSON `{"detail": ...}` response when a body is allowed, and `RequestValidationError` becomes a 422 response using `exc.errors()`.
- `docs_src/bigger_applications/app_an_py310/routers/items.py:21-25` raises `HTTPException(status_code=404, detail="Item not found")` for a missing resource.
- `docs_src/bigger_applications/app_an_py310/routers/items.py:33-37` raises `HTTPException(status_code=403, detail=...)` for a forbidden update.
### Full-stack FastAPI Template
- `backend/app/api/deps.py:30-45` translates invalid token, missing user, and inactive user states into semantic HTTP errors inside a dependency.
- `backend/app/api/routes/items.py:53-57` uses 404 and 403 in the read handler.
- `backend/app/api/routes/items.py:86-95` repeats the same boundary pattern on update before applying the mutation.
- `backend/app/api/routes/items.py:106-112` repeats it on delete before committing the write.
### Pydantic and Starlette
- `pydantic/docs/index.md:109-152` shows invalid input producing structured validation errors rather than ad hoc strings.
- `starlette/tests/test_applications.py:218-238` asserts 404, custom 405, and 500 responses at the HTTP layer, reinforcing that response behavior is a framework contract, not just an implementation detail.
- `starlette/applications.py:35-48` separates middleware and exception handlers in application setup, which is part of why centralized formatting works cleanly.
## Bottom line
Routes and dependencies should express *HTTP meaning*.
Exception handlers should own *HTTP formatting*.
Core logic should stay framework-agnostic until the boundary translates it.
+127
View File
@@ -0,0 +1,127 @@
# Persistence
Keep session lifetime and write boundaries explicit. Route handlers can orchestrate straightforward database work, but persistence concerns should stay visible instead of hiding behind ambient sessions or magical commits.
## Why this convention exists
FastAPI apps often stay clean until persistence gets sloppy. Then you get:
- hidden session creation inside helpers
- unclear transaction boundaries
- handlers packed with ORM choreography
- tests that only pass because the database layer was mocked out of existence
The stronger pattern in mature examples is straightforward:
- sessions arrive through dependencies
- read and write boundaries are visible in the calling flow
- sync and async persistence are deliberate choices
- tests exercise real session lifetime and cleanup
## The convention
1. Provide sessions through dependencies.
2. Keep transaction and commit boundaries visible.
3. Make sync versus async persistence an explicit stack decision.
4. Let routes do simple persistence orchestration, but move reusable business rules out once the flow stops being trivial.
5. Test against real persistence seams when practical.
## Follow it when stored state matters
Typical fits:
- routes that read or write the database
- flows where transaction scope matters
- auth-aware queries
- cleanup-sensitive tests
## Avoid these mistakes
Do *not* invent a repository or service layer for every one-line query.
Do *not* hide session creation deep inside helper functions.
Do *not* force async persistence just because the web layer is async. Choose sync or async deliberately based on the real stack.
## Preferred shapes
### Session through dependency
```python
def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
```
This makes resource lifetime explicit and testable.
### Visible write boundary
```python
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
session.add(item)
session.commit()
session.refresh(item)
return item
```
This is honest about where state changes happen.
### Explicit partial-update flow
```python
update_dict = item_in.model_dump(exclude_unset=True)
item.sqlmodel_update(update_dict)
session.add(item)
session.commit()
session.refresh(item)
```
Again: no hidden writes, no mystery transaction.
## Counterexamples
### Hidden ambient session access
```python
def create_item(item_in: ItemCreate) -> Item:
session = Session(engine)
...
```
Bad because session lifetime is buried in the wrong layer.
### Commit hidden deep in helpers
If callers cannot tell where the write boundary is, transaction reasoning gets ugly fast.
### Mock-only persistence tests
If no test ever exercises real session lifetime, query shape, or cleanup behavior, the persistence layer will surprise you in production.
## Source signals
### Full-stack FastAPI Template
- `backend/app/api/deps.py:21-27` provides DB access through a yielded session dependency.
- `backend/app/api/routes/items.py:21-45` keeps read query shape explicit in the route, including auth-aware filtering and response shaping.
- `backend/app/api/routes/items.py:68-72` shows a visible create, commit, and refresh boundary.
- `backend/app/api/routes/items.py:86-95` shows the same visibility on update.
- `backend/app/api/routes/items.py:106-112` shows delete followed by an explicit commit.
- `backend/tests/conftest.py:15-24` keeps test session lifetime and cleanup explicit.
### SQLAlchemy and Starlette lifespan
- `examples/asyncio/async_orm.py:63-104` in SQLAlchemy shows explicit async session and transaction handling instead of hidden ambient state.
- `examples/inheritance/joined.py:93-120` shows the sync equivalent with visible `Session(...)` and `commit()` boundaries.
- `starlette/applications.py:46-48` reinforces that long-lived app resources belong in lifespan rather than being initialized lazily inside request-time persistence helpers.
## Bottom line
Persistence should be visible.
Sessions should come from clear boundaries.
Transactions should not be magical.
If a reader cannot tell where data is loaded, changed, and committed, the persistence design needs work.
+121
View File
@@ -0,0 +1,121 @@
# Pydantic Boundaries
Use Pydantic models deliberately at FastAPI transport boundaries. They are excellent for parsing, validation, and serialization, but they should not automatically become your entire domain model.
## Why this convention exists
FastAPI and Pydantic fit together especially well at request and response boundaries:
- parsing external input
- validating shape and types
- coercing raw values
- serializing output
- generating OpenAPI schema
That convenience is the point. The trap is letting one transport model silently become the request model, response model, persistence model, and domain model for accidental reasons rather than deliberate ones.
## The convention
1. Use Pydantic models for request and response boundaries.
2. Validate external input explicitly.
3. Serialize explicitly at the response boundary.
4. Translate between transport models and domain or persistence objects when the concerns diverge.
5. Keep field-level validation and coercion on the schema that owns those rules.
## Follow it when data crosses the API boundary
Typical fits:
- request bodies
- response models
- field aliasing or defaulting rules
- API shapes that should stay stable even if internal objects change
## Do not split models just to satisfy a slogan
A tiny app may genuinely use the same shape all the way through.
But split models once the concerns stop matching, especially when:
- API names diverge from internal names
- persistence fields should not leak outward
- the response shape is not the storage shape
- business invariants deserve their own representation
## Preferred shapes
### Request model at the boundary, explicit translation inward
```python
@router.post("/items", response_model=ItemPublic)
def create_item(session: SessionDep, current_user: CurrentUser, item_in: ItemCreate) -> ItemPublic:
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
session.add(item)
session.commit()
session.refresh(item)
return item
```
This keeps request parsing explicit while still allowing a translation step into the stored object.
### Explicit partial-update semantics
```python
update_dict = item_in.model_dump(exclude_unset=True)
```
That is clearer than guessing which missing fields mean "leave unchanged."
## Counterexamples
### One transport schema leaked into every layer
```python
class UserPayload(BaseModel):
...
# same class used for HTTP input, DB persistence, internal domain state,
# background jobs, and every outbound response
```
Sometimes fine for a toy app; usually brittle over time.
### Ad hoc dict shaping in handlers
```python
return {"id": item.id, "name": item.name, "owner": current_user.email}
```
Bad because the response contract drifts away from any explicit schema.
### Validation rules smeared across unrelated layers
If field coercion belongs to the request schema, keep it there instead of duplicating it in routes, services, and ORM hooks.
## Source signals
### Pydantic
- `pydantic/main.py:253-264` states that model construction parses and validates input data and raises `ValidationError` when the input cannot form a valid model.
- `pydantic/docs/index.md:68-107` shows external data becoming a typed model and then being serialized with `model_dump()`.
- `pydantic/docs/index.md:109-152` shows invalid input producing structured validation errors.
- `pydantic/docs/concepts/validators.md:91-114` shows field-scoped `mode='after'` validation.
- `pydantic/docs/concepts/validators.md:160-252` shows `mode='before'` validators operating on raw input before normal parsing.
### Full-stack FastAPI Template
- `backend/app/api/routes/items.py:61-72` accepts `ItemCreate`, translates it into an `Item`, commits it, and returns the stored object.
- `backend/app/api/routes/items.py:44-45` converts query results into `ItemPublic` response models for collection responses.
- `backend/app/api/routes/items.py:91-95` uses `model_dump(exclude_unset=True)` before applying a patch-style update.
### FastAPI
- `fastapi/exception_handlers.py:20-26` turns request validation failures into a consistent 422 response instead of leaving each route to invent its own error format.
## Bottom line
Pydantic belongs at the HTTP boundary by default.
Use it there aggressively.
When transport concerns and domain concerns diverge, translate between them instead of forcing one class to own every job forever.
+120
View File
@@ -0,0 +1,120 @@
# Routes
Keep route handlers thin. They should own HTTP concerns, not become the place where auth, persistence, validation, and business rules all pile up.
## Why this convention exists
FastAPI already gives routes strong boundary mechanics: parsed inputs, dependency injection, response-model shaping, and framework-managed error handling. The clean pattern in both FastAPI examples and production templates is simple:
- declare request inputs in the signature
- accept collaborators from dependencies
- do small boundary checks
- call the next layer
- return a typed response
When handlers grow past that, they become hard to read, hard to reuse, and hard to test.
## The convention
1. Keep HTTP metadata on the router or route decorator.
2. Keep request parsing explicit in the function signature.
3. Use dependencies for shared request wiring.
4. Keep reusable or non-trivial business rules outside the handler.
5. Return explicit response models or well-understood objects that FastAPI can serialize predictably.
## Follow it by default
This fits almost every application route.
"Thin" does *not* mean the route does nothing. A good route can still:
- load a record
- perform obvious 404/403 checks
- map an input model into a stored object
- commit and return a response
The smell is not "the route did work." The smell is "the route became the system's junk drawer."
## Preferred shape
```python
@router.get("/{id}", response_model=ItemPublic)
def read_item(session: SessionDep, current_user: CurrentUser, id: UUID) -> ItemPublic:
item = session.get(Item, id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
if not current_user.is_superuser and item.owner_id != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return item
```
Why this works:
- inputs and collaborators are visible in one line
- boundary failures are explicit
- the handler stays readable without inventing extra layers
## Counterexamples
### Handler as a god function
```python
@router.post("/")
def create_item(...):
# parse auth manually
# open DB manually
# validate request shape again
# run business rules
# send email
# publish event
# build custom response dict
```
Bad because every concern is fused into one boundary blob.
### Hidden response shaping
```python
return {
"ok": True,
"item": some_orm_obj.__dict__,
"extra": weird_runtime_state,
}
```
Bad because the API contract becomes informal and drifts easily.
### Same ownership check copied into every handler body
If the wiring is structurally shared, move it toward a dependency or helper instead of repeating it across routes.
## Source signals
### FastAPI
- `docs_src/bigger_applications/app_an_py310/routers/items.py:5-10` puts prefix, tags, shared dependencies, and canned responses on the router instead of inside each handler.
- `docs_src/bigger_applications/app_an_py310/routers/items.py:16-38` keeps the handlers tiny: one returns a collection, one raises a 404 for a missing item, and one raises a 403 for a forbidden update.
- `docs_src/bigger_applications/app_an_py310/main.py:1-18` assembles routers centrally, which is another signal that route modules are boundary slices, not the whole application.
### Full-stack FastAPI Template
- `backend/app/api/routes/items.py:13-45` keeps list handling inside a single readable flow: explicit params, auth-aware filtering, and response-model construction.
- `backend/app/api/routes/items.py:48-58` shows a narrow read-by-id handler with only fetch + permission checks + return.
- `backend/app/api/routes/items.py:61-72` shows that simple persistence orchestration can still live in the route when the flow is straightforward.
- `backend/app/api/main.py:6-14` centralizes router registration instead of scattering feature mounting across the codebase.
### FastAPI Users
- `examples/beanie-oauth/app/app.py:30-57` composes feature routers at app assembly time instead of stuffing auth flows into one giant module.
- `examples/beanie-oauth/app/app.py:60-62` keeps an authenticated endpoint tiny because auth comes from a dependency.
## Bottom line
A good FastAPI route is a clean HTTP boundary:
- explicit inputs
- explicit collaborators
- explicit failures
- explicit outputs
If a handler starts absorbing subsystems, pull those concerns back out.
+138
View File
@@ -0,0 +1,138 @@
# Testing
Test FastAPI applications at the HTTP or ASGI boundary whenever practical. Prefer client-based tests, dependency overrides, and explicit lifespan or session fixtures over patching internals.
## Why this convention exists
FastAPI does a lot of important work *around* your code:
- request parsing
- dependency injection
- auth wiring
- validation
- error serialization
- startup and shutdown behavior
If tests bypass those seams, they skip the behavior the framework is responsible for.
The repeated pattern across FastAPI, Starlette, HTTPX, and production templates is:
- drive the app through a client
- make fixture lifetime explicit
- override dependencies at the framework seam
- exercise lifespan behavior deliberately
## The convention
1. Use `TestClient` or HTTPX ASGI clients for route tests.
2. Put shared app, client, and session setup in fixtures.
3. Use `app.dependency_overrides` to replace request-time collaborators.
4. Use `yield` fixtures when cleanup matters.
5. Enter lifespan-aware client contexts when startup or shutdown behavior is part of the test.
## Follow it for boundary behavior
This is the default for:
- route tests
- auth and permission tests
- validation and error-contract tests
- startup and shutdown behavior
- request and response schema behavior
Pure business logic is different: if the code is framework-agnostic, test it directly as normal Python.
## Preferred shapes
### Client fixture with explicit lifetime
```python
@pytest.fixture
def client() -> Generator[TestClient, None, None]:
with TestClient(app) as client:
yield client
```
This makes startup and teardown behavior visible.
### Dependency overrides for request-time seams
```python
app.dependency_overrides[get_current_user] = fake_user
try:
response = client.get("/items")
finally:
app.dependency_overrides = {}
```
This swaps collaborators where FastAPI expects them to be swapped.
### Explicit DB or session cleanup fixture
```python
@pytest.fixture(scope="session", autouse=True)
def db() -> Generator[Session, None, None]:
with Session(engine) as session:
init_db(session)
yield session
cleanup_db(session)
session.commit()
```
This makes resource lifetime and cleanup rules obvious.
## Counterexamples
### Monkeypatch soup
```python
monkeypatch.setattr("app.api.routes.items.session", fake_session)
monkeypatch.setattr("app.api.routes.items.get_user", fake_user)
```
Bad because the test couples to route internals instead of public seams.
### Testing startup-sensitive behavior without entering client context
If the app depends on startup-initialized resources, a client used without the proper lifespan context can produce misleading green tests.
### Calling route functions directly for integration-style assertions
That skips request parsing, dependency injection, validation, and error serialization, which is most of what FastAPI is doing for you.
## Source signals
### FastAPI
- `tests/test_dependency_security_overrides.py:24-29` defines a route whose collaborators come from dependency injection.
- `tests/test_dependency_security_overrides.py:44-63` overrides those collaborators with `app.dependency_overrides` and then resets the override map.
### Starlette
- `tests/test_applications.py:160-163` uses a yielded `client` fixture around a test client context manager.
- `tests/test_applications.py:234-238` uses `raise_server_exceptions=False` so tests can inspect a 500 response instead of immediately re-raising the server exception.
- `tests/test_applications.py:394-409` verifies lifespan startup before `yield` and cleanup after `yield` through the client context.
- `starlette/applications.py:46-48` explicitly documents lifespan as the preferred startup and shutdown mechanism.
### HTTPX
- `httpx/_transports/asgi.py:63-83` exposes `ASGITransport` specifically for driving an ASGI app in-process, including testing-oriented constructor options.
- `httpx/_transports/asgi.py:78-81` documents `raise_app_exceptions=False` for inspecting application failures as responses.
- `httpx/_transports/mock.py:15-43` exposes `MockTransport` as an explicit transport seam when testing HTTP clients.
### Full-stack FastAPI Template
- `backend/tests/conftest.py:15-24` keeps DB setup and cleanup explicit in a yielded session fixture.
- `backend/tests/conftest.py:27-30` defines a `TestClient` fixture with a visible context boundary.
- `backend/tests/conftest.py:33-42` derives auth fixtures from the client instead of bypassing the app boundary.
## Bottom line
Test the app where FastAPI actually does work:
- through the client
- through dependencies
- through lifespan
- with explicit fixture lifetime
Patch internals only when there is no better seam.
+31
View File
@@ -0,0 +1,31 @@
# Source Notes
This directory stores the reusable evidence behind the convention docs.
## What belongs here
One note per upstream repo, with:
- why the repo was chosen
- repeated conventions, not just isolated examples
- caveats and counterexamples
- exact `file:line` anchors
- pattern candidates supported by the evidence
## Current notes
- `fastapi.md`
- `starlette.md`
- `pydantic.md`
- `httpx.md`
- `fastapi-users.md`
- `full-stack-fastapi-template.md`
## Quality bar
A source note is good when it makes later synthesis cheap:
- repeated patterns are clearly separated from one-off examples
- framework-owned caveats are preserved
- citations are fast to verify
- vague claims are trimmed away
Read `../PROCESS.md` for the full repeatable workflow.
+63
View File
@@ -0,0 +1,63 @@
# FastAPI Users source notes
Repo: `fastapi-users/fastapi-users`
Local checkout: `/home/ubuntu/repos/rodin-sources/fastapi-users`
## Why this repo was chosen
- This repo shows how a substantial FastAPI ecosystem package composes auth/user flows as routers plus dependency factories, rather than bespoke checks inside each endpoint.
- It is especially useful for identifying patterns around router composition, auth dependencies, and the behavior differences between strict and optional auth gates.
## Repeated patterns
### 1) Feature slices are delivered as routers and assembled centrally
- `examples/beanie-oauth/app/app.py:28-57` builds one `FastAPI(lifespan=...)` app and includes router factories for JWT auth, registration, password reset, verification, users, and OAuth.
- `tests/test_fastapi_users.py:24-42` repeats the same assembly pattern in tests, including multiple auth-related routers plus a users router.
Why chosen:
- The example app and the test app use the same composition model, which makes this a repeated package convention rather than demo-only structure.
Implication for synthesis:
- Strong evidence for composing auth/account features as routers, not implementing login/register/verify flows endpoint by endpoint.
### 2) Auth is expressed as dependency factories with explicit policy flags
- `examples/beanie-oauth/app/app.py:60-62` protects a route with `Depends(current_active_user)`.
- `tests/test_fastapi_users.py:44-114` shows the broader pattern: `fastapi_users.current_user(...)` generates dependencies for current, active, verified, superuser, and optional variants.
- `fastapi_users/router/oauth.py:235-237` derives `get_current_active_user` from `authenticator.current_user(active=True, verified=requires_verification)` and then injects it into the OAuth association route at `fastapi_users/router/oauth.py:257-262`.
Why chosen:
- This is stronger than a single protected-route example: policy is parameterized and reused as dependency wiring.
Implication for synthesis:
- Recommend expressing auth requirements in dependency declarations (`active=True`, `verified=True`, `superuser=True`, `optional=True`) rather than hidden role checks inside handlers.
### 3) Optional auth dependencies deliberately change failure behavior
- `tests/test_fastapi_users.py:156-203` shows non-optional `current_user` / `current_user(active=True)` endpoints returning `401` for missing or invalid tokens.
- `tests/test_fastapi_users.py:320-412` shows optional variants returning `200` with `null` when the token is missing, invalid, or does not satisfy the extra policy.
- `tests/test_fastapi_users.py:219-315` also distinguishes authorization outcomes: verified/superuser constraints return `403` when a valid user lacks the required property.
Why chosen:
- This is exactly the kind of subtle behavior shift that future synthesis could easily flatten incorrectly.
Caveat / counterexample:
- `optional=True` is not just "same dependency, but maybe absent." It changes endpoint semantics from auth failure to nullable user context.
Implication for synthesis:
- If optional auth appears in synthesized guidance, call out the semantic shift explicitly and avoid presenting it as a drop-in default.
### 4) Lifespan owns startup resources here too
- `examples/beanie-oauth/app/app.py:17-28` initializes Beanie in an `@asynccontextmanager` lifespan and passes it to `FastAPI(lifespan=...)`.
Why chosen:
- Confirms that ecosystem packages/examples align with the Starlette/FastAPI lifespan pattern rather than using import-time initialization.
## Strong citation candidates
- Router-factory composition in app assembly: `examples/beanie-oauth/app/app.py:28-57`
- Dependency-factory auth policies: `tests/test_fastapi_users.py:44-114`
- Optional auth returns `200` + `null` instead of `401/403`: `tests/test_fastapi_users.py:320-412`
- OAuth route derives auth dependency from policy flags: `fastapi_users/router/oauth.py:235-262`
## Pattern candidates supported by this repo
- compose auth/account feature areas as routers
- declare auth policy in dependencies rather than inline endpoint checks
- distinguish required auth from optional nullable-user contexts
- initialize backing resources in lifespan
+76
View File
@@ -0,0 +1,76 @@
# FastAPI source notes
Repo: `fastapi/fastapi`
Local checkout: `/home/ubuntu/repos/rodin-sources/fastapi`
## Why this repo was chosen
- Core framework source plus first-party docs/tests. It is the best place to separate actual repeated FastAPI conventions from tutorial folklore.
- Especially useful here because the same patterns appear in docs examples, framework internals, and tests: router composition, dependency injection, semantic exceptions, and dependency-driven test seams.
## Repeated patterns
### 1) App assembly is declarative: app-level and router-level configuration carry shared concerns
- `docs_src/bigger_applications/app_an_py310/main.py:7-18` creates the app with a global dependency, then includes routers with per-router prefix/tags/dependencies/responses.
- `docs_src/bigger_applications/app_an_py310/routers/items.py:5-10` shows the same concern split one level down: the router owns prefix/tags/dependencies/common responses.
- `fastapi/applications.py:330-348` documents that `FastAPI(..., dependencies=[...])` applies those dependencies to every path operation, including sub-routers.
Why chosen:
- This is not a one-off tutorial style; the public constructor explicitly supports the same pattern the docs teach.
Implication for synthesis:
- Favor centralized app/router configuration for auth headers, tags, common responses, and prefixes.
- Do not describe thin handlers as a style preference only; the framework API is built to move cross-cutting concerns out of handlers.
### 2) Dependencies are first-class request wiring, including teardown-aware resources
- `docs_src/bigger_applications/app_an_py310/dependencies.py:6-13` keeps request validation in dedicated dependency callables that raise HTTP errors directly.
- `fastapi/routing.py:95-136` wraps request handling in nested `AsyncExitStack`s specifically so dependency setup/teardown, including `yield` dependencies, participates in request lifecycle.
- `tests/test_dependency_security_overrides.py:24-29` defines a route entirely in terms of `Security(...)` and `Depends(...)` collaborators.
Why chosen:
- This combines public examples with internal lifecycle machinery. Dependencies are not sugar over helper calls; they are a core execution model.
Caveat / counterexample:
- `fastapi/routing.py:124-130` raises a `FastAPIError` if application code swallows an exception in a `yield` dependency and fails to re-raise. So "just hide cleanup in a dependency" is incomplete; dependency cleanup must preserve exception flow.
Implication for synthesis:
- Prefer dependencies for auth, validated request context, DB/session access, and request-scoped resources.
- Mention that `yield` dependencies are appropriate when setup/teardown must be coupled to a request.
### 3) Handlers and dependencies raise semantic HTTP exceptions; framework handlers translate them
- `docs_src/bigger_applications/app_an_py310/routers/items.py:21-25` raises `HTTPException(404, ...)` when an item is missing.
- `docs_src/bigger_applications/app_an_py310/routers/items.py:28-38` adds operation-specific response metadata and raises `HTTPException(403, ...)` for forbidden updates.
- `fastapi/exception_handlers.py:11-26` turns `HTTPException` and `RequestValidationError` into HTTP responses, including a 422 JSON body for validation failures.
- `fastapi/exception_handlers.py:13-17` also shows a subtle rule: statuses that must not include a body return a bare `Response`, not JSON.
Why chosen:
- The docs examples and framework exception handlers line up exactly: route code signals semantics, framework code owns response formatting.
Implication for synthesis:
- Recommend raising semantic exceptions from route/dependency code instead of hand-building error JSON in each handler.
- Note that response-body behavior is status-sensitive and partly framework-owned.
### 4) Preferred test seam: override dependencies, not handler internals
- `tests/test_dependency_security_overrides.py:24-29` keeps the route signature explicit about collaborators.
- `tests/test_dependency_security_overrides.py:44-63` replaces both ordinary dependencies and security dependencies through `app.dependency_overrides`, then resets overrides after each test.
Why chosen:
- This is a repo-level testing pattern, not an incidental example.
Caveat / counterexample:
- The reset step is part of the pattern, not cleanup fluff: `tests/test_dependency_security_overrides.py:52-63` clears `app.dependency_overrides = {}` after each override-based test.
Implication for synthesis:
- When describing testing conventions, emphasize overrideable dependency seams as the intended unit of substitution.
## Strong citation candidates
- Global dependencies apply across sub-routers: `fastapi/applications.py:330-348`
- Dependency lifecycle is request-lifecycle machinery, not style guidance: `fastapi/routing.py:95-136`
- `yield` dependency exception-swallowing failure mode: `fastapi/routing.py:124-130`
- Central exception translation, including no-body statuses: `fastapi/exception_handlers.py:11-17`
## Pattern candidates supported by this repo
- centralize cross-cutting concerns in app/router configuration
- use dependencies as the main request-wiring mechanism
- use `yield` dependencies for request-scoped resources with teardown
- raise semantic `HTTPException`s from handlers/dependencies and let framework handlers shape responses
- test by overriding dependencies and resetting overrides explicitly
+72
View File
@@ -0,0 +1,72 @@
# Full-stack FastAPI Template source notes
Repo: `fastapi/full-stack-fastapi-template`
Local checkout: `/home/ubuntu/repos/rodin-sources/full-stack-fastapi-template`
## Why this repo was chosen
- This template is a production-oriented FastAPI service skeleton, so it is useful for spotting conventions that survive outside toy examples: central assembly, typed dependency aliases, explicit schema boundaries, and visible test resource lifetimes.
- It is also valuable because it includes a few pragmatic exceptions that should not be over-generalized.
## Repeated patterns
### 1) Route modules are grouped by feature and assembled centrally
- `backend/app/api/main.py:6-14` creates a single `api_router`, includes feature routers (`login`, `users`, `utils`, `items`), and conditionally includes a `private` router only in local environments.
- `backend/app/main.py:17-33` creates the app, configures OpenAPI identity generation and CORS middleware, then mounts the API router under the versioned prefix.
Why chosen:
- This is the top-level assembly pattern for the template, not a side example.
Caveat / counterexample:
- `backend/app/api/main.py:13-14` shows an environment-specific router include. Good synthesis should treat this as a deliberate local-only escape hatch, not a reason to scatter router registration across arbitrary modules.
Implication for synthesis:
- Favor one central API router and one central app assembly module.
- Mention that environment-specific routes can still be expressed centrally.
### 2) Dependencies own session/auth wiring, and typed aliases keep route signatures concise
- `backend/app/api/deps.py:21-27` defines `get_db()` and publishes `SessionDep` / `TokenDep` as `Annotated[..., Depends(...)]` aliases.
- `backend/app/api/deps.py:30-57` decodes the token, validates it with `TokenPayload`, loads the user, and raises semantic HTTP errors for invalid credentials, missing users, inactive users, and insufficient privileges.
- `backend/app/api/routes/items.py:13-16`, `48-49`, `61-64`, `75-82`, and `99-102` repeatedly inject `SessionDep` and `CurrentUser` directly in handler signatures.
Why chosen:
- The dependency alias pattern is repeated across route handlers, not used once.
Implication for synthesis:
- Strong evidence for typed dependency aliases when the same collaborators appear across many handlers.
- Shared auth/session logic belongs in dependencies, not repeated inline in each route.
### 3) Schema transitions stay explicit at route boundaries
- `backend/app/api/routes/items.py:44-45` converts ORM/domain objects to `ItemPublic` before constructing the collection response.
- `backend/app/api/routes/items.py:61-72` accepts `ItemCreate`, validates it into an `Item` with `owner_id` injected, persists it, and returns the created object.
- `backend/app/api/routes/items.py:91-96` uses `item_in.model_dump(exclude_unset=True)` before applying a partial update.
Why chosen:
- The route code repeatedly marks transitions between input schema, persistence model, and output schema.
Caveat / counterexample:
- `backend/app/api/routes/items.py:48-58`, `75-96`, and `99-113` still return ORM objects directly in some single-item handlers while relying on `response_model` to shape output. So the template mixes explicit conversion with response-model-driven shaping; future synthesis should describe that nuance instead of claiming one exclusive pattern.
Implication for synthesis:
- Prefer explicit schema boundaries, especially for collections and patch-style updates.
- When handlers return domain objects directly, note that `response_model` is still doing boundary work.
### 4) Tests keep DB lifetime and cleanup visible
- `backend/tests/conftest.py:15-24` creates a session fixture, initializes the DB once, yields the session, then deletes `Item` and `User` rows and commits cleanup.
- `backend/tests/conftest.py:27-30` keeps the test client lifecycle explicit with a context-managed `TestClient` fixture.
Why chosen:
- This is the template's default testing setup, so it is strong evidence for visible resource lifetime management.
## Strong citation candidates
- Central API router assembly with local-only conditional include: `backend/app/api/main.py:6-14`
- Typed dependency aliases: `backend/app/api/deps.py:21-27`
- Auth dependency handles token decode + user lookup + semantic errors: `backend/app/api/deps.py:30-57`
- Explicit patch update boundary via `model_dump(exclude_unset=True)`: `backend/app/api/routes/items.py:91-96`
- Test DB lifetime and cleanup: `backend/tests/conftest.py:15-24`
## Pattern candidates supported by this repo
- centralize app and API router assembly
- use typed dependency aliases for repeated collaborators
- keep auth/session logic in dependencies
- keep request/update/response schema transitions explicit
- keep test resource lifetime and cleanup visible
+48
View File
@@ -0,0 +1,48 @@
# HTTPX source notes for FastAPI conventions
Repo: `encode/httpx`
Local checkout: `/home/ubuntu/repos/rodin-sources/httpx`
## Why this repo was chosen
- FastAPI services often depend on HTTPX both for in-process ASGI testing and for outbound HTTP seams. HTTPX source clarifies where those seams actually live.
- Useful here because the transport layer gives stronger evidence than generic client examples.
## Repeated patterns
### 1) HTTPX supports testing an ASGI app across the HTTP boundary without leaving process
- `httpx/_transports/asgi.py:63-83` defines `ASGITransport` as a transport that sends requests directly to an ASGI app, with explicit knobs for `raise_app_exceptions`, `root_path`, and client address.
- `httpx/_transports/asgi.py:105-119` shows the transport building a real ASGI scope from the outgoing request, including method, headers, path, query string, server, client, and `root_path`.
Why chosen:
- This is a concrete framework-aligned seam for FastAPI/Starlette apps; it preserves HTTP/ASGI behavior better than calling route functions directly.
Caveat / counterexample:
- `httpx/_transports/asgi.py:79-81` explicitly says app exceptions are raised by default. If you want to inspect a 500 response body instead of crashing the test, you must set `raise_app_exceptions=False`.
Implication for synthesis:
- Prefer in-process ASGI transport/client tests when you want realistic request/response behavior without network overhead.
- Mention the `raise_app_exceptions=False` toggle when describing 500-response testing.
### 2) HTTPX makes outbound substitution a transport concern, not a patching concern
- `httpx/_transports/mock.py:15-17` defines `MockTransport` from a request handler.
- `httpx/_transports/mock.py:19-43` routes fully built requests through that handler for both sync and async clients, reading the request body first and requiring/awaiting a real `Response`.
Why chosen:
- This is a stronger seam than monkeypatching nested `client.get(...)` call sites; the substitution happens at the client transport boundary.
Caveat / counterexample:
- `httpx/_transports/mock.py:25-26` raises a `TypeError` if a sync client is given an async handler. The sync/async client mode still matters even when mocking.
Implication for synthesis:
- Recommend transport substitution for HTTP integrations instead of monkeypatching internal helper functions or third-party SDK calls.
## Strong citation candidates
- ASGI transport is the in-process app test seam: `httpx/_transports/asgi.py:63-83`
- ASGI scope construction proves tests still exercise HTTP/ASGI boundaries: `httpx/_transports/asgi.py:105-119`
- 500-response testing requires `raise_app_exceptions=False`: `httpx/_transports/asgi.py:79-81`
- Mock transport is request/response substitution at the client boundary: `httpx/_transports/mock.py:15-43`
## Pattern candidates supported by this repo
- prefer in-process ASGI transport tests for HTTP behavior
- use transport-level substitution for outbound HTTP dependencies
- keep sync/async client mode aligned with the transport handler you provide
+65
View File
@@ -0,0 +1,65 @@
# Pydantic source notes for FastAPI conventions
Repo: `pydantic/pydantic`
Local checkout: `/home/ubuntu/repos/rodin-sources/pydantic`
## Why this repo was chosen
- FastAPI's request/response model layer is built on Pydantic. This repo is the right source for boundary-shaping rules: coercion, validation timing, explicit serialization, and validator failure modes.
- Important here because Pydantic's docs include both recommended patterns and dangerous counterexamples.
## Repeated patterns
### 1) Pydantic models are designed for transport boundaries: parse external input, then serialize explicitly
- `docs/index.md:61-89` shows raw external input being coerced into a typed `BaseModel`, including datetime parsing, bytes-to-string coercion, and string-to-int coercion.
- `docs/index.md:82-107` uses `model_dump()` as the explicit serialization boundary.
- `docs/index.md:109-152` shows invalid external data producing a structured `ValidationError` with per-field errors.
Why chosen:
- This is the front-door example in the docs and directly matches how FastAPI uses models for requests and responses.
Caveat / counterexample:
- `docs/index.md:93-104` makes clear that default validation is not strict-only; coercion is normal behavior. Any synthesized guidance that assumes "Pydantic means no coercion" would be wrong without an explicit strictness choice.
Implication for synthesis:
- Recommend models at the API boundary, but mention that Pydantic may coerce inputs unless strict behavior is configured.
- Prefer explicit `model_dump()`/response-model boundaries over ad hoc dict shaping deep in business logic.
### 2) Use field validators to keep domain-specific checks close to fields, and choose validator timing deliberately
- `docs/concepts/validators.md:38-45` defines the contract for field validators: they receive a value and must return the validated value.
- `docs/concepts/validators.md:46-80` shows after-validators as the type-safer default because they run after Pydantic's internal validation.
- `docs/concepts/validators.md:160-209` shows before-validators receiving raw input, making them useful for coercion/preprocessing before core parsing.
Why chosen:
- These are not isolated APIs; the docs frame validator timing as a central design choice.
Caveat / counterexample:
- `docs/concepts/validators.md:160-206` warns that before-validators must handle arbitrary raw input and should avoid unsafe mutation when later raising errors, especially with unions.
Implication for synthesis:
- Prefer after-validators for invariant checks on already-parsed values.
- Use before-validators only when the input truly needs normalization before parsing.
### 3) Not every validator mode is safe for API-boundary schemas
- `docs/concepts/validators.md:254-311` shows plain validators short-circuiting internal validation entirely; the example accepts `number='invalid'` even though the field is annotated as `int`.
- `docs/concepts/validators.md:313-320` explains that wrap validators can also bypass normal validation flow if they return early or skip the handler.
Why chosen:
- These are exactly the kinds of footguns that can quietly weaken FastAPI request validation if used casually.
Implication for synthesis:
- Avoid recommending plain/wrap validators as the default pattern for request models.
- If they appear in synthesized guidance, frame them as advanced escape hatches with validation-bypass risk.
## Strong citation candidates
- External data coercion + explicit serialization boundary: `docs/index.md:61-107`
- Validation errors are structured and field-specific: `docs/index.md:109-152`
- Validator contract and return-value requirement: `docs/concepts/validators.md:38-45`
- After-vs-before validator tradeoff: `docs/concepts/validators.md:46-80`, `docs/concepts/validators.md:160-209`
- Plain validators can accept invalid typed data: `docs/concepts/validators.md:254-311`
## Pattern candidates supported by this repo
- use Pydantic models at transport boundaries
- keep serialization explicit with `model_dump()` or response-model shaping
- prefer after-validators for parsed-value invariants
- use before-validators sparingly for raw-input normalization
- treat plain/wrap validators as advanced tools, not defaults for API schemas
+64
View File
@@ -0,0 +1,64 @@
# Starlette source notes
Repo: `encode/starlette`
Local checkout: `/home/ubuntu/repos/rodin-sources/starlette`
## Why this repo was chosen
- FastAPI inherits key runtime behavior from Starlette. Starlette is the right source for conventions around app assembly, middleware ordering, lifespan, and request state.
- Valuable here because it distinguishes framework guarantees from habits that only appear in sample apps.
## Repeated patterns
### 1) App construction keeps routing, middleware, exception handlers, and lifespan as separate inputs
- `starlette/applications.py:22-55` defines the constructor with distinct parameters for routes, middleware, exception handlers, and lifespan.
- `tests/test_applications.py:139-157` builds one app by explicitly passing routes, exception handlers, middleware, and lifespan together.
Why chosen:
- This separation exists both in the public constructor and in the main integration-style test app.
Implication for synthesis:
- Treat app assembly concerns as separate layers; do not blur middleware, routing, error handling, and startup resources into one module-level setup blob.
### 2) Middleware ordering is intentional and framework-enforced
- `starlette/applications.py:35-40` documents that `ServerErrorMiddleware` is outermost and `ExceptionMiddleware` innermost.
- `starlette/applications.py:68-77` shows the actual stack construction: framework error middleware, then user middleware, then framework exception middleware around the router.
- `starlette/applications.py:98-101` forbids adding middleware after the application has started.
Why chosen:
- This is stronger than a docs recommendation; it is constructor/runtime behavior.
Caveat / counterexample:
- User middleware is not the whole stack. If a synthesized convention says "middleware runs in the order you add it," that is incomplete because Starlette wraps user middleware with framework-owned layers.
Implication for synthesis:
- When discussing middleware order, mention Starlette's outer/inner framework layers explicitly.
- Avoid recommendations that rely on mutating middleware after startup.
### 3) Lifespan is the preferred resource boundary, and `asynccontextmanager` is the clearest shape
- `starlette/applications.py:46-48` says lifespan replaces `on_startup`/`on_shutdown`; use one style or the other, not both.
- `tests/test_applications.py:394-409` shows the preferred shape: an `@asynccontextmanager` lifespan marks startup before `yield` and cleanup after `yield`, with both verified by entering/exiting the client context.
- `tests/test_applications.py:108-114` defines lifespan state by yielding `{"count": 1}`.
- `tests/test_applications.py:241-244` verifies request handlers can consume that state via `request.state`.
Why chosen:
- This combines constructor guidance with working tests for startup/cleanup and state propagation.
Caveat / counterexample:
- `tests/test_applications.py:421-459` still supports async/sync generator lifespan forms, but those tests are under a deprecation-warning filter. For future-facing synthesis, prefer `@asynccontextmanager`, not raw generator lifespans.
Implication for synthesis:
- Recommend lifespan for DB pools, clients, caches, and other app resources.
- Note that lifespan can yield structured state for handlers instead of relying on import-time globals.
## Strong citation candidates
- Constructor-level separation of app assembly concerns: `starlette/applications.py:22-55`
- Middleware stack ordering is framework-defined: `starlette/applications.py:68-77`
- Cannot add middleware after startup: `starlette/applications.py:98-101`
- Lifespan replaces startup/shutdown hooks: `starlette/applications.py:46-48`
- Preferred `@asynccontextmanager` lifespan behavior: `tests/test_applications.py:394-409`
## Pattern candidates supported by this repo
- separate routes, middleware, exception handlers, and lifespan at app assembly time
- preserve middleware ordering intentionally, including framework-owned outer/inner layers
- prefer lifespan over startup/shutdown hooks for resource management
- use lifespan-yielded state instead of import-time globals for app-scoped data