diff --git a/PROCESS.md b/PROCESS.md index 1b71efd..07be4d6 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -67,14 +67,6 @@ 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 @@ -100,14 +92,17 @@ Instead: - 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. +## Handling mixed-concern source code -## Fresh-context refinement pattern +Upstream service code will often mix FastAPI behavior, Pydantic boundary behavior, and ordinary Python design in the same function or line. -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 +When that happens: +- do not classify the whole snippet by file name or repo alone +- split the evidence into **framework-owned signal** and **generic Python signal** +- keep the request/response, dependency, lifecycle, and HTTP-boundary lesson here +- push reusable language-level guidance back to `python-patterns` + +If the claim still makes full sense after removing FastAPI and HTTP, it is probably too generic to live here as a FastAPI convention. ## Review checklist @@ -132,16 +127,6 @@ When the repo is ready for human review: 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 diff --git a/README.md b/README.md index 25d91c6..9aba592 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,6 @@ Primary upstreams mined so far: - `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` @@ -39,34 +31,16 @@ Why this mix works: - `patterns/testing.md` - `comparison/fastapi-vs-python.md` -## What belongs here +## Boundary rubric: does this belong in `fastapi-conventions`? -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 +Use this repo when the rule is mainly about the HTTP or app boundary: routes, `Depends(...)`, request/response schemas, HTTP error meaning, lifespan, or ASGI-level testing. -## What does **not** belong here +Push it back to the sibling `python-patterns` repo when the rule would still make sense in a CLI, library, or batch job. -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? +Quick check: +- if FastAPI or Starlette owns the behavior, this repo should lead +- if deleting the HTTP boundary leaves most of the rule intact, it is probably too generic for this repo +- if the split is awkward, prefer the higher-level doc and cross-link rather than duplicating guidance ## Core rule diff --git a/comparison/fastapi-vs-python.md b/comparison/fastapi-vs-python.md index 43d2d88..5bacf91 100644 --- a/comparison/fastapi-vs-python.md +++ b/comparison/fastapi-vs-python.md @@ -1,9 +1,131 @@ # FastAPI vs Python -Use this to capture places where FastAPI/Starlette conventions deliberately differ from broader Python guidance. +Use this file when a rule *sounds* generic but actually changes once FastAPI owns the boundary. -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 +## Quick decision rule + +Keep the guidance in `python-patterns` when it still makes sense without HTTP or FastAPI. + +Put it here when FastAPI owns the boundary: route handler shape, `Depends(...)` wiring, request/response schemas, auth checks, exception-to-response translation, app lifecycle, or app-level tests. + +## Where FastAPI deliberately bends the generic Python advice + +### 1) Dependency injection: explicit constructor wiring vs signature-driven request wiring + +In general Python code, explicit construction is usually clearer than hidden injection. Prefer normal parameters, constructors, or small factories. + +In FastAPI routes, request-scoped wiring belongs in dependency callables and handler signatures. + +**Python default:** +- pass collaborators explicitly +- construct objects in obvious application setup code +- avoid magical ambient injection + +**FastAPI adjustment:** +- use `Depends(...)` for sessions, current-user lookup, auth gates, and other request-scoped collaborators +- keep the injected object visible in the handler signature +- do not drag `Depends(...)` down into reusable domain code + +See: +- sibling repo: `python-patterns/patterns/module-design.md` +- `patterns/dependencies.md` + +### 2) Data models: internal value objects vs transport schemas + +Generic Python guidance says not to make one model own validation, persistence, transport, and domain behavior all at once. + +FastAPI sharpens that rule: Pydantic is especially valuable at the HTTP boundary, so boundary models are the default even when internal models differ. + +**Python default:** +- keep internal state in small honest types +- validate and serialize explicitly at boundaries +- split models when concerns diverge + +**FastAPI adjustment:** +- use request and response models aggressively at the API boundary +- let Pydantic own coercion and OpenAPI-facing schema behavior +- translate inward when storage or domain shape differs + +See: +- sibling repo: `python-patterns/patterns/data-models.md` +- `patterns/pydantic-boundaries.md` + +### 3) Async design: separate public sync/async APIs vs one app stack choice + +Generic Python guidance often prefers separate sync and async surfaces when semantics differ. + +A FastAPI service is different: the app already chose an HTTP execution model. Inside that service, the important question is not "should I expose both sync and async APIs?" but "where is the sync/async boundary, and is it honest?" + +**Python default:** +- expose separate sync and async entrypoints when callers need different semantics +- do not hide event-loop control behind a fake sync wrapper + +**FastAPI adjustment:** +- keep the app stack consistent and deliberate +- it is fine for async handlers to call sync-oriented persistence if that is the real stack choice and the framework/runtime setup supports it +- do not force async everywhere just because FastAPI can run async handlers +- do not hide blocking work accidentally behind an async-looking boundary + +See: +- sibling repo: `python-patterns/patterns/async-boundaries.md` +- `patterns/persistence.md` + +### 4) Errors: domain exception taxonomy vs HTTP meaning at the edge + +Generic Python guidance focuses on exception types that help callers make decisions. + +FastAPI adds a second responsibility: some failures must be expressed as HTTP status codes and response bodies. + +**Python default:** +- raise exceptions that match the semantic failure +- keep transport concerns out of reusable core logic + +**FastAPI adjustment:** +- raise `HTTPException` for request-level failures such as 400/401/403/404 +- let FastAPI/Pydantic produce validation failures where possible +- translate domain or infrastructure exceptions at the HTTP boundary +- centralize response-envelope formatting in handlers or middleware, not in every route + +See: +- sibling repo: `python-patterns/patterns/error-handling.md` +- `patterns/errors.md` + +### 5) Testing: direct unit tests vs boundary-driven app tests + +Generic Python testing guidance says to keep tests explicit, reusable, and fixture-driven. + +FastAPI changes the highest-value integration seam: you usually want to test the app through HTTP/ASGI boundaries rather than by calling route functions directly. + +**Python default:** +- test framework-agnostic business logic directly +- share setup with fixtures instead of ad hoc duplication + +**FastAPI adjustment:** +- test routes through `TestClient` or HTTPX ASGI clients +- override request-time collaborators with `app.dependency_overrides` +- enter lifespan-aware client contexts when startup/shutdown matters +- still test pure business logic as plain Python when the framework is irrelevant + +See: +- sibling repo: `python-patterns/patterns/testing.md` +- `patterns/testing.md` + +### 6) Application assembly: normal context management vs lifespan-owned resources + +In generic Python, resource lifetime is usually expressed with constructors, factories, and context managers. + +In FastAPI and Starlette, app-wide resources often belong in lifespan, because the framework owns when the application starts and stops. + +**Python default:** +- make resource lifetime explicit +- prefer normal context managers and obvious setup code + +**FastAPI adjustment:** +- use lifespan for app-wide startup/shutdown resources +- use dependencies to hand request-scoped access to those resources into handlers +- do not hide startup work inside request dependencies + +See: +- sibling repo: `python-patterns/patterns/module-design.md` +- `patterns/dependencies.md` +- `patterns/persistence.md`