Veloce vs FastAPI, Flask, Starlette, and Django¶
Veloce is a from-scratch async Python web framework. Its routing, request/response pipeline, dependency injection, OpenAPI generator, WebSocket layer, and test client are all in-tree — not wrappers around an existing stack. This page lays out how Veloce overlaps with and differs from the four most common Python web frameworks.
When a public name overlaps with FastAPI or Starlette (Depends,
TestClient, JSONResponse), Veloce reimplements the semantics rather
than re-exporting upstream code.
Feature matrix¶
| Area | Veloce | FastAPI | Flask | Starlette | Django |
|---|---|---|---|---|---|
| Async support | Native, async handlers; sync handlers run on a thread executor | Native, sync via thread executor | Async views via asgiref since 2.0 |
Native | Async views since 3.1, ORM partial |
| Typed dependency injection | Depends, Security, SecurityScopes, yield teardown |
Depends, Security, SecurityScopes, yield teardown |
Extensions only | None built in | None built in |
| Pydantic v2 integration | Built in for query / path / header / cookie / body and response_model |
Built in | Extensions only | None | Extensions only |
| OpenAPI 3.1 | Built in, served via Swagger UI and ReDoc | Built in (OpenAPI 3.1 since 0.103) | Extensions only | None | Extensions only (drf-spectacular, etc.) |
| WebSockets | Full ASGI surface, DI, subprotocol negotiation, typed iter helpers | Yes, ASGI-native | Via flask-sock / extensions |
Yes, ASGI-native | Via Django Channels |
| Templating | Jinja2 with url_for / g / current_app globals, async render |
Jinja2 via Starlette utilities | Jinja2 built in | Jinja2 helper module | Django template language (DTL), Jinja2 optional |
| Sessions | Signed cookies + server-side backend, permanent_lifetime, secret rotation |
Via Starlette middleware (signed cookie only) | Signed cookies built in | Signed cookies via middleware | Server-side, DB / cache / cookie backends |
| CSRF | Built-in middleware | Extensions only | Via flask-wtf |
Extensions only | Built in |
Flask-style helpers (g, flash, blueprints) |
g, flash, get_flashed_messages, Blueprint |
None | Built in | None | Different idiom (apps / views) |
| Test client | In-memory TestClient driving the real ASGI surface |
TestClient (Starlette, wraps httpx) |
Werkzeug test client |
TestClient (wraps httpx) |
django.test.Client (WSGI) |
| Custom routing | Radix tree with typed converters (int, float, uuid, path, custom) |
Starlette router (sorted list of routes) | Werkzeug routing (map of Rule objects) |
Sorted list of routes | URLconf list of path / re_path |
| ASGI / WSGI | ASGI-native | ASGI-native (Starlette) | WSGI-native, ASGI via asgiref |
ASGI-native | WSGI-native, ASGI optional |
| Python version | 3.10+ | 3.8+ | 3.9+ | 3.9+ | 3.10+ (5.0), 3.12+ (5.2) |
Read each cell as "what ships in the core distribution with no third-party packages installed". Extensions exist for almost every gap, but the cell tracks core surface.
When to choose Veloce¶
- You want FastAPI-style typed dependency injection and OpenAPI on top of an async-only request pipeline, without the Starlette layering underneath.
- You want Flask-style helpers (
g,flash,Blueprint,current_app,url_for) and an async pipeline in the same project, without bridging two stacks. - You want CSRF, sessions, signed cookies, CORS, GZip, ProxyFix, trusted-host, and HTTPS-redirect middleware available out of the box.
- The dispatch hot path matters — Veloce inspects handler signatures once at registration into a
HandlerPlanand performs no reflection per request.
When to choose FastAPI instead¶
- The project depends on an existing FastAPI plugin, integration, or starter (auth backends, admin UIs, deployment templates) that you do not want to port.
- You need to target Python 3.8 or 3.9.
- You want the larger ecosystem and longer release history that FastAPI currently has.
- Your team is already trained on FastAPI's exact import paths and idioms, and you do not need Flask-style helpers.
When to choose Flask instead¶
- The application is primarily synchronous and request volumes are modest. WSGI under Gunicorn is a well-understood deployment surface.
- The project relies on the Flask extension ecosystem (Flask-Login, Flask-SQLAlchemy, Flask-Admin, Flask-Migrate, etc.) and porting those extensions is not worth the gain.
- You do not need OpenAPI or typed request validation in core.
- You want a smaller core surface than Veloce or FastAPI, and you are comfortable adding extensions à la carte.
When to choose Starlette instead¶
- You want a minimal ASGI toolkit and you intend to assemble routing, DI, and validation yourself.
- You are building a framework, not an application.
When to choose Django instead¶
- The application is content-heavy: admin panel, ORM with migrations, auth, forms, and templates as a single coherent stack.
- You want a long-term-support release cadence and a large package ecosystem aimed at full-stack web apps.
- You do not need a typed, async-first request pipeline at the core.
Migrating from FastAPI¶
Most route handlers move across with only an import change.
The public surface overlaps closely. These names exist in Veloce with comparable semantics:
Depends |
Security |
SecurityScopes |
Body |
Query |
Path |
Header |
Cookie |
Form |
File |
UploadFile |
HTTPException |
JSONResponse |
StreamingResponse |
FileResponse |
TestClient |
Differences to watch for:
- Handlers should be
async def. Sync handlers are accepted and run on a thread executor, but the async path is the supported default. app.state,app.config, andapp.extensionsare Flask-style dicts, not Pydantic settings objects. Pull configuration in at startup and stash it onapp.config.- The
TestClientis in-memory and constructs ASGI scopes directly. There is nohttpxround-trip and no socket. Tests that need real network behaviour must run uvicorn explicitly. - The router is a radix tree, not a sorted list of regex routes. Route precedence rules differ for overlapping prefixes — see the Routing guide.
Migrating from Flask¶
Veloce keeps the Flask helpers that are async-safe. g, flash, and
get_flashed_messages are backed by contextvars, so they are
per-request safe under concurrent handlers. Blueprint supports
nesting and URL prefixes. current_app, url_for, and the Jinja2
globals are wired the same way.
Differences to watch for:
- Handlers run on the event loop. Blocking calls (synchronous database drivers,
requests,time.sleep) will stall the loop unless wrapped inrun_in_executoror replaced with an async equivalent. requestis imported fromveloce, not from a thread-local module global; pass it as a parameter or use the typed dependency-injection mechanism.- WSGI middleware does not plug into the ASGI surface directly. Replace it with the equivalent Veloce or ASGI middleware.
- Sessions are signed cookies by default with an optional server-side backend. The signing key lives in
app.config["SECRET_KEY"], matching Flask.
Mounting a WSGI app¶
Veloce ships no WSGIMiddleware. Starlette and FastAPI expose one to run a
WSGI application (Flask, Django) inside the ASGI app; Veloce has no equivalent
class. Instead, wrap the WSGI app in any ASGI-to-WSGI bridge (for example
a2wsgi) and mount the resulting ASGI app
with app.mount. A mounted non-Veloce ASGI
app is dispatched at the ASGI layer: the prefix is stripped from the request
path and moved onto root_path, so the WSGI app sees a root-relative request.
from a2wsgi import WSGIMiddleware
from flask import Flask
from veloce import Veloce
flask_app = Flask(__name__)
@flask_app.get("/legacy")
def legacy():
return "served by Flask"
app = Veloce()
app.mount("/wsgi", WSGIMiddleware(flask_app))
if __name__ == "__main__":
app.run(port=8000)
No lifespan fan-out to ASGI mounts
A mounted non-Veloce ASGI app receives only http and websocket scopes —
Veloce does not drive the ASGI lifespan cycle into it. A bridged WSGI
app must not depend on ASGI startup/shutdown events. Mount prefixes must not
overlap, and a mounted app owns its whole subtree — a native route registered
under the same prefix is unreachable.
CORS private-network access¶
Veloce's CORSMiddleware implements the
Private Network Access (PNA)
preflight handshake, which is not part of Starlette's CORS middleware. Pass
allow_private_network=True (default False); when a preflight carries
Access-Control-Request-Private-Network: true, the response echoes
Access-Control-Allow-Private-Network: true. The grant is only emitted for that
specific preflight signal, not on every response.
from veloce import Veloce
from veloce.middleware import CORSMiddleware
app = Veloce()
app.add_middleware(
CORSMiddleware(
allow_origins=["https://app.example.com"],
allow_private_network=True,
)
)
if __name__ == "__main__":
app.run(port=8000)
Note
PNA only applies to preflighted cross-origin requests from a more-public
context to a more-private one. Leave allow_private_network at its False
default unless a browser actually sends the request-private-network signal.