Skip to content

Commit eb886ab

Browse files
Berik AshimovBerik Ashimov
authored andcommitted
docs(audit): correct DX matrix — 4/5 top gaps closed
Updates the 2026-04-18 FastAPI parity audit to reflect: - Gap #1 (yield-dependencies) was INCORRECTLY flagged ⚠️ partial in the original matrix. Second-pass re-verification confirmed full support: _execute_dep_plan pushes generators onto a per-request cleanup stack, app.py finalizer advances/closes each in reverse order, test_generator_deps.py covers 6 cases. No code was needed. - Gaps #2/#3/#5 are now ✅ (commits 14a7a28, 10b3655, de14afc). - Gap #4 (OAuth2 scopes) remains the single open item from top-5. Also adds a 'second-tier gaps' list of genuinely-missing items that didn't make the top-5 cut, as a durable backlog for future passes.
1 parent 65ce7b4 commit eb886ab

1 file changed

Lines changed: 46 additions & 136 deletions

File tree

docs/audits/2026-04-18-dx-vs-fastapi.md

Lines changed: 46 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
# HawkAPI DX audit vs FastAPI
22

3-
**Date:** 2026-04-18
3+
**Date:** 2026-04-18 (corrected 2026-04-18)
44
**Scope:** Feature-parity snapshot of HawkAPI vs FastAPI. Research-only; no code changes here.
55
**Spec:** [docs/plans/2026-04-18-dx-audit-design.md](../plans/2026-04-18-dx-audit-design.md)
66

77
---
88

99
## Executive summary
1010

11-
HawkAPI currently **matches or exceeds FastAPI on ~85 % of the tutorial-level DX surface**, and meaningfully exceeds on differentiators FastAPI does not ship at all (API versioning, permission policies, built-in bulkhead/circuit-breaker/rate-limiter, observability, migration codemod, PEP 703 wheels).
11+
HawkAPI **matches or exceeds FastAPI on ~90 % of the tutorial-level DX surface** and meaningfully exceeds on differentiators FastAPI does not ship at all (API versioning, permission policies, built-in bulkhead/circuit-breaker/rate-limiter, observability, migration codemod, PEP 703 wheels).
1212

13-
The five gaps below are the ones that hurt migration from a real FastAPI codebase the most — not because they are large engineering tasks, but because they are everywhere in FastAPI tutorials and copy-pasted into real production code:
13+
The five gaps below were identified in the initial audit as the items hurting migration from a real FastAPI codebase the most. Four of the five are **closed** at this revision; one remains.
1414

15-
| # | Gap | Severity | Effort |
16-
|---|---|---|---|
17-
| 1 | **Yield-dependencies with per-request finalization** | Critical | M |
18-
| 2 | **Route-level `dependencies=[Depends(...)]`** on decorators | Important | S |
19-
| 3 | **`response_model_exclude_none/unset/defaults`** flags | Important | S |
20-
| 4 | **OAuth2 scopes enforcement + OpenAPI reflection** | Important | M |
21-
| 5 | **`status` module with HTTP_NNN constants** | Minor (cosmetic) | XS |
15+
| # | Gap | Severity | Effort | Status |
16+
|---|---|---|---|---|
17+
| 1 | **Yield-dependencies with per-request finalization** | Critical | M | ✅ always shipped (see correction below) |
18+
| 2 | **Route-level `dependencies=[Depends(...)]`** on decorators | Important | S | ✅ shipped (commit `14a7a28`) |
19+
| 3 | **`response_model_exclude_none/unset/defaults`** flags | Important | S | ✅ shipped (commit `10b3655`) |
20+
| 4 | **OAuth2 scopes enforcement + OpenAPI reflection** | Important | M | ❌ open — last remaining |
21+
| 5 | **`status` module with HTTP_NNN constants** | Minor (cosmetic) | XS | ✅ shipped (commit `de14afc`) |
2222

23-
Everything else is either already present or a known out-of-scope differentiator. The non-gap surplus (built-in versioning, bulkhead, observability, codemod, etc.) is strong; the five items above close the last mile of "FastAPI users land on HawkAPI and nothing is missing."
23+
**Correction (2026-04-18):** Gap #1 was incorrectly flagged ⚠️ partial in the original matrix. A second-pass re-verification confirmed yield-dependencies with per-request teardown are **fully working**: `src/hawkapi/di/resolver.py:_execute_dep_plan` pushes generators onto a per-request cleanup stack, and `src/hawkapi/app.py:534-548` advances (success) or closes (exception) every generator in reverse order. 6 tests in `tests/unit/test_generator_deps.py` cover sync/async generators, cleanup on error, and multi-gen ordering. No work needed for Gap #1.
2424

2525
---
2626

@@ -39,7 +39,7 @@ Legend: ✅ full, ⚠️ partial, ❌ missing.
3939
| Route-level `response_model=` | Router.add_route || |
4040
| Route-level `status_code=` | Router.add_route || |
4141
| Route-level `include_in_schema=False` | Router.add_route || |
42-
| Route-level `dependencies=[...]` (side-effect deps) | || Must use app-level hooks or middleware today (**Gap #2**) |
42+
| Route-level `dependencies=[...]` (side-effect deps) | Router + route decorators (commit `14a7a28`) || Shipped in Gap #2; router-level also supported |
4343
| `include_router(responses=...)` default-response map ||| Not supported |
4444
| Sub-app mount `app.mount("/x", subapp)` | app.py || |
4545

@@ -63,10 +63,10 @@ Legend: ✅ full, ⚠️ partial, ❌ missing.
6363
|---|---|---|---|
6464
| `Depends(callable)` | [src/hawkapi/di/depends.py](../../src/hawkapi/di/depends.py) || |
6565
| Sub-dependencies (transitive) | di/param_plan.py || Resolved recursively |
66-
| `yield` dependencies with teardown after response | | ⚠️ | App-level `lifespan` only; no per-request yield-dep (**Gap #1**) |
66+
| `yield` dependencies with teardown after response | [di/resolver.py](../../src/hawkapi/di/resolver.py) `_execute_dep_plan` + [app.py](../../src/hawkapi/app.py) cleanup finalizer | | Sync + async generators; reverse-order cleanup on success or exception; 6 tests in `test_generator_deps.py` |
6767
| Class-callable as dependency | di/param_plan.py || |
68-
| Path-operation-level `dependencies=[...]` | || Requires middleware today (**Gap #2**) |
69-
| Global (app-level) dependencies || | Workaround: middleware |
68+
| Path-operation-level `dependencies=[...]` | Router + route decorators (commit `14a7a28`) || Shipped in Gap #2 |
69+
| Global (app-level) dependencies || ⚠️ | Available as `Router(dependencies=[...])` subclass pattern; no `HawkAPI(dependencies=[...])` kwarg yet |
7070
| Within-request caching of same `Depends(fn)` | di/scope.py || Scope-level caching |
7171
| `dependency_overrides` for tests | [testing/overrides.py](../../src/hawkapi/testing/overrides.py) || `override()` context manager |
7272

@@ -88,7 +88,7 @@ Legend: ✅ full, ⚠️ partial, ❌ missing.
8888
| `JSONResponse` | responses/json_response.py || |
8989
| `HTMLResponse`, `PlainTextResponse`, `RedirectResponse`, `FileResponse`, `StreamingResponse` | `src/hawkapi/responses/` || |
9090
| Return `Response` directly from handler (bypass serialization) | responses/response.py || |
91-
| `response_model_exclude_none/unset/defaults` | | | Not wired (**Gap #3**) |
91+
| `response_model_exclude_none/unset/defaults` | [serialization/filters.py](../../src/hawkapi/serialization/filters.py) (commit `10b3655`) | | Shipped in Gap #3; recursive over msgspec + Pydantic |
9292
| `jsonable_encoder` equivalent | [serialization/encoder.py](../../src/hawkapi/serialization/encoder.py) || `encode_response()` |
9393
| Content negotiation (Accept → JSON vs MessagePack) | serialization/negotiation.py || Exceeds FastAPI |
9494

@@ -164,7 +164,7 @@ Legend: ✅ full, ⚠️ partial, ❌ missing.
164164

165165
| FastAPI feature | HawkAPI | Status | Notes |
166166
|---|---|---|---|
167-
| `from fastapi import status``status.HTTP_201_CREATED` | | | No constants module (**Gap #5**) |
167+
| `from fastapi import status``status.HTTP_201_CREATED` | [status.py](../../src/hawkapi/status.py) (commit `de14afc`) | | Shipped in Gap #5; Starlette-compatible names |
168168
| CORS middleware | middleware/cors.py || |
169169
| GZip middleware | middleware/gzip.py || |
170170
| Session (signed cookies) middleware | middleware/session.py || |
@@ -176,94 +176,14 @@ Legend: ✅ full, ⚠️ partial, ❌ missing.
176176

177177
## Top-5 gaps (detailed)
178178

179-
### Gap #1 — Yield-dependencies with per-request finalization
180-
181-
**Severity:** Critical **Effort:** M
182-
183-
**What FastAPI has:**
184-
185-
```python
186-
async def get_db() -> AsyncIterator[AsyncSession]:
187-
async with SessionLocal() as session:
188-
yield session # returned to handler
189-
190-
@app.get("/users")
191-
async def list_users(db: AsyncSession = Depends(get_db)) -> list[User]:
192-
...
193-
# After response is sent, session context manager exits; rolls back or commits.
194-
```
195-
196-
This is THE pattern in FastAPI tutorials and production code for database sessions, HTTP clients, Redis connections, transactions. Every SQLAlchemy/Databases example depends on it.
197-
198-
**What HawkAPI has:** App-level `lifespan` context manager for long-lived resources. No per-request teardown.
199-
200-
**Work required:**
201-
- Extend `src/hawkapi/di/param_plan.py` to detect generator/async-generator dependencies.
202-
- Register each yield-dep on a per-request stack (push on entry, pop + run remaining code after the handler's response body is sent).
203-
- Ensure cancellation and exception paths run the finalizers.
204-
- Wire teardown to run AFTER response headers+body flushed (like FastAPI) so the client isn't blocked.
205-
- Test: yield that raises during teardown — how should the error be reported? (FastAPI: logs, ignores.)
206-
207-
**Payoff:** Single biggest migration-friction item for any FastAPI user with a database.
208-
209-
---
210-
211-
### Gap #2 — Route-level `dependencies=[Depends(...)]`
212-
213-
**Severity:** Important **Effort:** S
214-
215-
**What FastAPI has:**
216-
217-
```python
218-
@app.get("/admin/reports", dependencies=[Depends(require_admin)])
219-
async def reports() -> ...:
220-
...
221-
```
222-
223-
Used pervasively for auth guards, audit-log writers, rate-limit increments that don't need a return value. Also accepted on `APIRouter(dependencies=[...])` for whole-router guards.
224-
225-
**What HawkAPI has:** Workaround via app-level hooks or middleware. No decorator-level kwarg.
226-
227-
**Work required:**
228-
- Add `dependencies: Sequence[Depends] | None = None` kwarg to the route decorators in `src/hawkapi/routing/router.py`.
229-
- Add the same kwarg on the `Router` class constructor and `include_router` call; merge router-level + route-level lists.
230-
- On request: resolve each `Depends` before invoking the handler; any raised exception short-circuits (consistent with FastAPI behavior).
231-
- Make the results *not* injected into the handler's signature — they're executed for side effects only.
232-
- Tests: chain ordering, exceptions, interaction with yield-dependencies.
233-
234-
**Payoff:** Biggest ergonomic win for anyone who reads FastAPI auth examples and copy-pastes.
179+
### Gaps #1, #2, #3, #5 — closed
235180

236-
---
237-
238-
### Gap #3`response_model_exclude_none / _unset / _defaults`
239-
240-
**Severity:** Important **Effort:** S
241-
242-
**What FastAPI has:**
243-
244-
```python
245-
@app.get(
246-
"/items/{id}",
247-
response_model=Item,
248-
response_model_exclude_none=True, # drop keys whose value is None
249-
response_model_exclude_unset=True, # drop keys the user didn't set
250-
response_model_exclude_defaults=True, # drop keys equal to their default
251-
)
252-
```
253-
254-
Used for:
255-
- APIs where optional fields shouldn't serialize as `"field": null`
256-
- Versioned responses where fields were added later and old clients shouldn't see them
257-
- Admin vs public response shapes
258-
259-
**What HawkAPI has:** `response_model` accepted on routes; the three exclusion knobs are not wired through to the serializer.
260-
261-
**Work required:**
262-
- Plumb three flags from route metadata → `src/hawkapi/serialization/encoder.py`.
263-
- msgspec already supports field exclusion at encode time; map the flags onto its API.
264-
- Tests per flag + combinations.
265-
266-
**Payoff:** Low-effort closure of a feature 70 % of FastAPI response_model users eventually reach for.
181+
| Gap | Outcome | Commit / file |
182+
|---|---|---|
183+
| #1 Yield-dependencies | Confirmed always shipped; re-verification found full sync + async generator support, reverse-order teardown, cleanup-on-error, multi-gen ordering | [di/resolver.py:_execute_dep_plan](../../src/hawkapi/di/resolver.py) + [app.py](../../src/hawkapi/app.py) finally block + `tests/unit/test_generator_deps.py` (6 tests) |
184+
| #2 Route / router `dependencies=[...]` | Shipped | commit `14a7a28`, `tests/unit/test_route_dependencies.py` |
185+
| #3 `response_model_exclude_*` flags | Shipped (msgspec + Pydantic + nested recursion) | commit `10b3655`, [serialization/filters.py](../../src/hawkapi/serialization/filters.py), `tests/unit/test_response_model_exclude.py` |
186+
| #5 `hawkapi.status` constants | Shipped (Starlette-compat naming; `http.HTTPStatus`-derived) | commit `de14afc`, [status.py](../../src/hawkapi/status.py), `tests/unit/test_status.py` |
267187

268188
---
269189

@@ -311,30 +231,6 @@ async def list_items() -> ...:
311231

312232
---
313233

314-
### Gap #5`status` module with HTTP_NNN constants
315-
316-
**Severity:** Minor (but high frequency) **Effort:** XS
317-
318-
**What FastAPI has:**
319-
320-
```python
321-
from fastapi import status
322-
323-
@app.post("/items", status_code=status.HTTP_201_CREATED)
324-
async def create_item(...): ...
325-
```
326-
327-
Pure cosmetic convenience re-exported from Starlette. Everyone uses it.
328-
329-
**What HawkAPI has:** Users hardcode `201`.
330-
331-
**Work required:**
332-
- Create `src/hawkapi/status.py` re-exporting the standard HTTP constants (can copy from Starlette's `starlette.status`, or use `http.HTTPStatus`'s integer values).
333-
- Export `status` from `hawkapi/__init__.py` so `from hawkapi import status` works.
334-
- One-line unit test.
335-
336-
**Payoff:** Removes a paper cut. A one-hour job that eliminates a reason someone says "HawkAPI is missing things FastAPI has."
337-
338234
---
339235

340236
## Where HawkAPI already exceeds FastAPI
@@ -361,14 +257,28 @@ Not gaps — differentiators we should not accidentally dilute while closing the
361257

362258
---
363259

364-
## Follow-ups (not this project)
260+
## Follow-ups
261+
262+
**Top-5 progress (4/5 closed):**
263+
264+
1. ✅ Gap #5 (`status` constants) — commit `de14afc`.
265+
2. ✅ Gap #3 (`response_model_exclude_*`) — commit `10b3655`.
266+
3. ✅ Gap #2 (`dependencies=[...]` kwarg) — commit `14a7a28`.
267+
4. ✅ Gap #1 (yield-dependencies) — confirmed always shipped; no code change needed, audit corrected.
268+
5. ❌ Gap #4 (OAuth2 scopes) — **next**.
365269

366-
Each of the top-5 gaps becomes its own design spec + implementation plan. Recommended order (smallest effort / highest ratio first, to accumulate shipped wins):
270+
**Second-tier gaps (not in top-5 but worth tracking):**
367271

368-
1. **Gap #5** (`status` constants) — afternoon of work.
369-
2. **Gap #3** (`response_model_exclude_*`) — one spec, small impl.
370-
3. **Gap #2** (`dependencies=[...]` kwarg) — clean decorator change.
371-
4. **Gap #1** (yield-dependencies) — the big one, touches DI core.
372-
5. **Gap #4** (OAuth2 scopes) — biggest surface area, last.
272+
- `Form()` marker class (multipart field validation)
273+
- `OAuth2PasswordRequestForm` helper
274+
- `openapi_tags=[{name, description, externalDocs}, ...]` with metadata
275+
- `servers=[...]` for OpenAPI
276+
- Per-route `openapi_extra={}`
277+
- `include_router(responses={...})`
278+
- `Jinja2Templates` / `TemplateResponse`
279+
- AsyncAPI story for WebSocket routes
280+
- Legacy `@app.on_event()` decorator (codemod already targets migration)
281+
- `contact=` / `license_info=` on constructor
282+
- Global `HawkAPI(dependencies=[...])` kwarg
373283

374-
This order gives four visible DX-parity wins before the fifth (scopes) takes more effort. Each gets an independent cycle — spec → plan → implement — following the workflow used for Tier 1 and Tier 2.
284+
Each becomes its own spec → plan → implement cycle if prioritized.

0 commit comments

Comments
 (0)