On this page
Guide to fastware Vite integration: dev() for combined server startup, ViteDevProxy middleware for backend-first routing, and SPA fallback.
#Vite Dev Mode
When building a frontend (React, Vue, Svelte, or plain TypeScript) alongside a fastware backend, you need both servers running during development -- Vite for hot module replacement (HMR) and fastware for your API routes and SSE endpoints. fastware provides two tools for this: the dev() function and the ViteDevProxy middleware.
#The problem
During development, you have 2 processes that need to coexist on different ports, each serving a distinct role. The frontend dev server handles asset transformation and hot module replacement, while the backend handles API routes, SSE broadcasting, and WebSocket endpoints:
- Vite dev server (port 5173) -- serves frontend assets, handles HMR via WebSocket, transforms TypeScript/JSX on the fly
- fastware server (port 8000) -- handles API routes, SSE broadcasting, WebSocket endpoints
The browser loads the page from one origin but needs to reach both. You could configure Vite's proxy, but that means your API is behind Vite and you lose fastware's middleware, error handling, and SSE support. fastware takes the opposite approach: the backend is the entry point, and unmatched requests are proxied to Vite.
#The dev() function
The simplest way to run both the Vite frontend and fastware backend servers together in a single process. It handles subprocess management, port readiness polling (up to 15 seconds), and proxy configuration automatically:
from fastware.dev import dev
dev(
"myapp:app",
vite_command="npm run dev",
vite_port=5173,
host="127.0.0.1",
port=8000,
)dev() does three things in order:
- Spawns Vite as a subprocess (
npm run devby default) - Waits for Vite to be ready (polls the port for up to 15 seconds)
- Wraps your app with
ViteDevProxyand starts the fastware server
When the fastware server shuts down (Ctrl+C), the Vite subprocess is terminated automatically.
#Parameters
| Parameter | Default | Description |
|---|---|---|
target | (required) | ASGI app -- either an import path string ("myapp:app") or a callable |
vite_command | "npm run dev" | Shell command to start the Vite dev server |
vite_port | 5173 | Port the Vite dev server listens on |
host | None | Bind address (falls back to env var) |
port | None | Bind port (falls back to env var) |
pid_path | None | Optional PID file path for process management |
name | "FASTWARE" | Application name for env var prefix and logging |
pre_serve | None | Optional callable invoked before server starts |
#ViteDevProxy middleware
Under the hood, dev() uses ViteDevProxy, a pure ASGI middleware class that intercepts HTTP and WebSocket requests. You can also use ViteDevProxy directly for more control over proxy behavior, backend prefix configuration, and middleware ordering in your ASGI stack, or to integrate with custom app factory patterns:
from fastware import Router, create_app, AppConfig
router = Router()
# ... register routes ...
app = create_app(router, config=AppConfig(
vite_dev_port=5173,
))Setting vite_dev_port in AppConfig enables ViteDevProxy as a built-in middleware layer.
#How routing works
ViteDevProxy uses a backend-first strategy for HTTP requests, which eliminates the need for API prefix configuration in most cases and keeps all middleware, error handling, and SSE support intact. This approach is the opposite of Vite's built-in proxy (which puts Vite first), and avoids the common problem of losing backend middleware when requests flow through the frontend server first. The 3-step routing logic is:
- Every HTTP request hits the fastware app first
- If the app returns 404 (no route matched), the request is proxied to Vite
- Vite serves the frontend asset (JS, CSS, HTML, images)
This means backend routes like /health, /api/users, or /events work without any prefix configuration -- they are handled by your fastware router directly. Only requests that don't match any route are forwarded to Vite.
For WebSocket connections, the proxy uses prefix-based routing because WebSocket upgrades cannot be retried:
- Paths matching the API prefix (
/apiby default) or backend prefixes go to the fastware app - Everything else is proxied to Vite (for HMR WebSocket connections)
#Configuring backend prefixes
By default, ViteDevProxy routes WebSocket connections on /events to the backend (in addition to the API prefix). The backend_prefixes list is checked for every WebSocket upgrade request; paths matching any prefix are handled by fastware while all others are proxied to Vite for HMR. If your SSE or WebSocket endpoints use different paths, configure them explicitly:
from fastware.middleware import ViteDevProxy
proxy = ViteDevProxy(
app,
vite_port=5173,
api_prefix="/api",
backend_prefixes=["/events", "/ws", "/sse"],
)The backend_prefixes parameter is a list of path prefixes that should be routed to the fastware app for WebSocket connections. HTTP requests to these paths go to the backend first regardless (because of the backend-first strategy).
#Production: SPA fallback
In production, Vite is not running. Instead, you build your frontend to static files (npm run build) and serve them with fastware's built-in SPA fallback, which routes unmatched GET requests to index.html for client-side navigation:
npm run build # outputs to dist/from pathlib import PathWith SPA fallback:
- Requests to
/assets/*serve files fromdist/assets/ - API routes (
/api/*) are handled by the router (404 if no match) - All other GET requests serve
dist/index.html, letting the frontend router handle client-side navigation
#Complete development setup
This example shows a complete development setup with 3 files: a fastware backend module serving API routes and SSE events via the Broadcaster, a dev entry point that runs both Vite and fastware together with hot reload, and a production entry point that serves pre-built static assets with SPA fallback routing:
backend (myapp.py):
from fastware import Router, Broadcaster, sse_route, create_app
broadcaster = Broadcaster(heartbeat_interval=30)
broadcaster.register_event("update")
router = Router()
router.add_route("GET", "/events", sse_route(broadcaster))
@router.get("/api/health")
async def health(request):
return {"status": "ok"}
@router.post("/api/items")
async def create_item(request):
item = request.json
broadcaster.broadcast("update", {"action": "created", "item": item})
return {"ok": True}
app = create_app(router)dev entry point (dev.py):
from fastware.dev import dev
if __name__ == "__main__":
dev("myapp:app", host="127.0.0.1", port=8000)production entry point (main.py):
from pathlib import Path
from fastware import AppConfig, create_app, serve
from myapp import router
app = create_app(router, config=AppConfig(
static_dir=Path("dist/assets"),
static_path="/assets",
spa_fallback=Path("dist/index.html"),
api_prefix="/api",
))
if __name__ == "__main__":
serve(app, foreground=True, host="0.0.0.0", port=8000)Run python dev.py during development, python main.py in production.
#API reference
See the dev() function reference for combined Vite and fastware server startup with subprocess management, and the ViteDevProxy class reference for fine-grained control over proxy routing, backend prefix configuration, and WebSocket connection handling:
#src.fastware.dev
Development mode combining Vite frontend dev server and fastware ASGI backend in a single command with hot reload and proxy routing.
#dev
def dev(target: str | Callable, *, vite_command: str='npm run dev', vite_port: int=5173, host: str | None=None, port: int | None=None, pid_path: Path | None=None, name: str='FASTWARE', pre_serve: Callable[[], None] | None=None) -> NoneStart Vite dev server + fastware ASGI server for development.
Spawns Vite as a subprocess, waits for it to be ready, then starts the fastware server with ViteDevProxy middleware. All frontend requests are proxied to Vite (with HMR), API requests are handled by the fastware router. Kills Vite on shutdown.
#ViteDevProxy
ASGI middleware that proxies unmatched requests to a Vite dev server.
Uses a backend-first routing strategy for HTTP: every request hits the backend first. If the backend returns 404 (no route matched), the request is proxied to Vite instead. This means backend routes like /health or /metrics work without being under an API prefix.
WebSocket upgrades cannot be retried, so they still use prefix-based routing: paths matching api_prefix or /events go to the backend; everything else is proxied to Vite (for HMR).
Args:
app: The inner ASGI application.vite_port: Port the Vite dev server is listening on.api_prefix: Path prefix for backend WebSocket routes (default
"/api"). Only used for WebSocket routing decisions.
#_is_api_request
def _is_api_request(self, path: str) -> boolReturn True if this path should go to the app, not be proxied.
#close
async def close(self) -> None#_proxy_http
async def _proxy_http(self, scope: Scope, send: Send, *, body: bytes=b'') -> NoneForward an HTTP request to the Vite dev server.
The body parameter contains the pre-captured request body (already consumed from receive by the backend during the try-first phase).
#_proxy_ws
async def _proxy_ws(self, scope: Scope, receive: Receive, send: Send) -> NoneBidirectional WebSocket proxy to Vite (for HMR).
Uses the websockets library if available. Falls back to closing the connection with an error code if not installed.