Initial extracted documentation set
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user