Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d9ddf7dc5 | ||
|
|
b189ae66e7 | ||
|
|
4156eb2db3 | ||
|
|
3f6559b2db | ||
|
|
71d4e053a7 | ||
|
|
2dec331afb | ||
|
|
64f43df3e3 | ||
|
|
6ccba2be9f | ||
|
|
2b735528ab | ||
|
|
e6170549f0 |
@@ -1,6 +1,6 @@
|
||||
# fastapi-vue-setup
|
||||
|
||||
Tool to create or patch FastAPI project with a Vue frontend, with integrated build and development systems. The Python package will not need any JS runtime because it includes a prebuilt Vue frontend in it. For development (Vite and FastAPI auto reloads) and building the package one of npm, deno or bun is required (npm recommended due to bugs in deno and bun).
|
||||
Tool to create or patch FastAPI project with a Vue frontend, with integrated build and development systems. The Python package will not need any JS runtime because it includes a prebuilt Vue frontend in it. For development (Vite and FastAPI auto reloads) and building the package one of npm, deno or bun is required (node is recommended due to bugs in deno and bun).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -61,17 +61,18 @@ Options:
|
||||
|
||||
## Port Configuration
|
||||
|
||||
The tool uses three distinct ports:
|
||||
In development, you access the Vite dev server at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5180. Ports and hosts of Vite and FastAPI are configurable by `devserver.py` arguments.
|
||||
|
||||
| Port | Purpose | Used by |
|
||||
| ---- | --------------------- | ------------------------------ |
|
||||
| 5173 | Vite dev server (HMR) | `npm run dev` via devserver.py |
|
||||
| 5180 | FastAPI in dev mode | uvicorn via devserver.py |
|
||||
| 5080 | Production server | `uv run my-app` |
|
||||
In production, FastAPI serves both the API and static files at `http://localhost:5080`. Configurable by `host:port` argument with defaults set in `__main__.py`
|
||||
|
||||
In development, you access the app at `http://localhost:5173`. Vite proxies `/api/*` requests to FastAPI at port 5180. Configurable with `devserver.py` arguments.
|
||||
## Main CLI
|
||||
|
||||
If your project didn't already have `__main__.py`, we create one that runs the FastAPI app with richer configuration than what the FastAPI CLI offers. Running your module starts it in production mode, and optionally host:port may be given as argument to specify where it listens.
|
||||
|
||||
If you are running behind a reverse proxy like [Caddy](https://caddyserver.com/) on localhost, your app will trust the proxy headers it sends. However, if you need to configure another proxy host or IP, set `FORWARDED_ALLOW_IPS` env variable before running the server.
|
||||
|
||||
The devserver script depends on this CLI entry for running the backend. You will have to modify the `devserver.py` script if your app has its own incompatible main module. Note that we set FastAPI debug mode and Uvicorn reload when configured via `FASTAPI_VUE_BACKEND_URL` env variable (set by `devserver.py`), while for normal production use these stay disabled. The same variable also controls static files serving (disabled in dev mode).
|
||||
|
||||
In production, FastAPI serves both the API and static files at `http://localhost:5080`. Configurable by `host:port` argument.
|
||||
|
||||
## Vite Plugin Configuration
|
||||
|
||||
@@ -96,26 +97,27 @@ The plugin reads environment `FASTAPI_VUE_BACKEND_URL` (default: `http://localho
|
||||
|
||||
## Project Structure
|
||||
|
||||
After patching, your project will have:
|
||||
|
||||
```
|
||||
my-app/
|
||||
├── frontend/ # Vue application
|
||||
├── frontend/ # Vue application
|
||||
│ ├── src/
|
||||
│ ├── vite.config.ts # Builds to ../my_app/frontend-build
|
||||
│ ├── vite-plugin-fastapi.js
|
||||
│ ├── vite.config.js
|
||||
│ └── package.json
|
||||
├── my_app/ # Python package
|
||||
├── my_app/ # Python module (files included in sdist)
|
||||
│ ├── __init__.py
|
||||
│ ├── __main__.py # CLI entry point
|
||||
│ ├── app.py # FastAPI application
|
||||
│ └── frontend-build/ # Built frontend (gitignored)
|
||||
│ ├── __main__.py # CLI entrypoint
|
||||
│ ├── app.py # FastAPI application
|
||||
│ └── frontend-build/ # Built frontend (gitignored)
|
||||
├── scripts/
|
||||
│ ├── devserver.py # Development server
|
||||
│ └── fastapi-vue/ # Build utilities
|
||||
└── pyproject.toml # Project configuration
|
||||
│ ├── devserver.py # CLI dev server (only in source tree)
|
||||
│ └── fastapi-vue/
|
||||
│ ├── build-frontend.py
|
||||
│ └── util.py
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
The script finds your existing fastpi app module and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection.
|
||||
The project directory tree looks roughly like this after project creation or patching. The script finds your existing app module and other files and patches them with minimal changes to enable the Vue-FastAPI interconnection. New Python and Vue projects are created automatically if none exist.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -130,6 +132,6 @@ uv build
|
||||
uv run my-app
|
||||
```
|
||||
|
||||
# Frontend serving
|
||||
## Frontend serving
|
||||
|
||||
Your FastAPI app will use [fastapi-vue](https://git.zi.fi/LeoVasanko/fastapi-vue) to serve the frontend file. Refer to that package's documentation for further configuration.
|
||||
Your FastAPI app will use [fastapi-vue](https://git.zi.fi/LeoVasanko/fastapi-vue) to serve the frontend files. Refer to that package's documentation for further configuration.
|
||||
|
||||
+540
-228
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -9,7 +9,7 @@ description = "Tool to create or patch FastAPI+Vue projects with integrated buil
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"tomli-w>=1.0.0",
|
||||
"tomlkit>=0.12.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,30 +1,54 @@
|
||||
import sys
|
||||
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
from fastapi_vue.hostutil import parse_endpoint
|
||||
from uvicorn import Config, Server
|
||||
|
||||
from .APP_MODULE import APP_VAR
|
||||
|
||||
DEFAULT_PORT = 5080
|
||||
|
||||
|
||||
def run_server(endpoints: list[dict], *, proxy="", devmode=False):
|
||||
conf: dict[str, object] = {"app": "MODULE_NAME.APP_MODULE:APP_VAR"}
|
||||
if proxy:
|
||||
conf["proxy_headers"] = True
|
||||
conf["forwarded_allow_ips"] = proxy
|
||||
if devmode:
|
||||
conf["reload"] = True
|
||||
conf["reload_dirs"] = ["MODULE_NAME"]
|
||||
APP_VAR.debug = True
|
||||
|
||||
if len(endpoints) > 1:
|
||||
# Run separate servers for multiple endpoints
|
||||
async def serve_all():
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for ep in endpoints:
|
||||
tg.create_task(Server(Config(**conf, **ep)).serve())
|
||||
|
||||
asyncio.run(serve_all())
|
||||
else:
|
||||
uvicorn.run(**conf, **endpoints[0])
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the FastAPI application using uvicorn."""
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
endpoint = sys.argv[1]
|
||||
if ":" in endpoint:
|
||||
host, port = endpoint.rsplit(":", 1)
|
||||
host = host or "localhost"
|
||||
port = int(port)
|
||||
else:
|
||||
host = "localhost"
|
||||
port = int(endpoint)
|
||||
else:
|
||||
host = "localhost"
|
||||
port = 5080
|
||||
|
||||
uvicorn.run(
|
||||
"MODULE_NAME.app:app",
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="info",
|
||||
parser = argparse.ArgumentParser(description="Run the MODULE_NAME server.")
|
||||
parser.add_argument(
|
||||
"endpoint",
|
||||
nargs="?",
|
||||
help=(
|
||||
f"Endpoint (default: localhost:{DEFAULT_PORT}). "
|
||||
"Forms: host:port | :port | [ipv6]:port | ip | host | unix:/path.sock"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
proxy = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
|
||||
devmode = bool(os.getenv("FASTAPI_VUE_FRONTEND_URL"))
|
||||
endpoints = parse_endpoint(args.endpoint, DEFAULT_PORT)
|
||||
run_server(endpoints, proxy=proxy, devmode=devmode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env -S uv run
|
||||
# auto-upgrade@fastapi-vue-setup - remove this if you modify this file
|
||||
"""Run Vite development server for frontend and FastAPI backend with auto-reload.
|
||||
|
||||
Usage:
|
||||
@@ -16,17 +17,15 @@ Environment:
|
||||
import argparse
|
||||
import asyncio
|
||||
import contextlib
|
||||
import ipaddress
|
||||
import os
|
||||
from pathlib import Path
|
||||
from sys import stderr
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi_vue.hostutil import parse_endpoint
|
||||
|
||||
exec((Path(__file__).parent / "fastapi-vue/util.py").read_text("UTF-8")) # noqa: S102
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_VITE_PORT = 5173
|
||||
DEFAULT_BACKEND_PORT = 5180
|
||||
FRONTEND_PATH = Path(__file__).parent.parent / "frontend"
|
||||
@@ -45,45 +44,8 @@ BUN_BUG = """\
|
||||
"""
|
||||
|
||||
|
||||
def parse_endpoint(
|
||||
value: str | None, default_port: int = DEFAULT_VITE_PORT
|
||||
) -> tuple[str | None, int, bool]:
|
||||
"""Parse an endpoint for Vite or backend.
|
||||
|
||||
Returns (host, port, all_ifaces).
|
||||
"""
|
||||
if not value:
|
||||
return DEFAULT_HOST, default_port, False
|
||||
|
||||
# Port only (numeric) -> localhost:port
|
||||
if value.isdigit():
|
||||
return DEFAULT_HOST, int(value), False
|
||||
|
||||
# Leading colon :port -> bind all interfaces
|
||||
if value.startswith(":") and value != ":":
|
||||
port_part = value[1:]
|
||||
if not port_part.isdigit():
|
||||
raise SystemExit(f"Invalid port in '{value}'")
|
||||
return None, int(port_part), True
|
||||
|
||||
# Unbracketed IPv6 (cannot safely contain a port)
|
||||
if value.count(":") > 1 and not value.startswith("["):
|
||||
try:
|
||||
ipaddress.IPv6Address(value)
|
||||
except ValueError as e:
|
||||
raise SystemExit(f"Invalid IPv6 address '{value}': {e}") from e
|
||||
return value, default_port, False
|
||||
|
||||
# Use urllib.parse for everything else
|
||||
parsed = urlparse(f"//{value}")
|
||||
host = parsed.hostname or DEFAULT_HOST
|
||||
port = parsed.port or default_port
|
||||
|
||||
return host, port, False
|
||||
|
||||
|
||||
def resolve_frontend_tools(
|
||||
vite_host: str | None, vite_port: int, all_ifaces: bool
|
||||
vite_port: int, all_ifaces: bool
|
||||
) -> tuple[list[str], list[str], str]:
|
||||
"""Resolve frontend install and dev commands.
|
||||
|
||||
@@ -114,12 +76,15 @@ def resolve_frontend_tools(
|
||||
}
|
||||
|
||||
install_cmd = [tool, *install_args[name]]
|
||||
dev_cmd = [tool, *dev_args[name], "--port", str(vite_port)]
|
||||
dev_cmd = [
|
||||
tool,
|
||||
*dev_args[name],
|
||||
"--clearScreen=false",
|
||||
f"--port={vite_port}",
|
||||
]
|
||||
|
||||
if all_ifaces:
|
||||
dev_cmd.append("--host")
|
||||
elif vite_host:
|
||||
dev_cmd.extend(["--host", vite_host])
|
||||
|
||||
if name == "bun":
|
||||
stderr.write(BUN_BUG)
|
||||
@@ -127,13 +92,15 @@ def resolve_frontend_tools(
|
||||
return install_cmd, dev_cmd, name
|
||||
|
||||
|
||||
async def wait_for_backend(backend_host: str, backend_port: int):
|
||||
async def wait_for_backend(host: str, port: int):
|
||||
"""Wait for the backend to be ready by polling the health endpoint."""
|
||||
max_attempts = 50
|
||||
url = f"http://{host}:{port}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
await client.get(f"http://{backend_host}:{backend_port}", timeout=1.0)
|
||||
await client.get(url, timeout=1.0)
|
||||
stderr.write("✓ Backend ready!\n")
|
||||
return True
|
||||
except httpx.RequestError:
|
||||
@@ -163,23 +130,29 @@ async def _terminate_process(proc: asyncio.subprocess.Process, name: str) -> Non
|
||||
|
||||
|
||||
async def run_devserver(
|
||||
vite_host: str | None,
|
||||
vite_port: int,
|
||||
all_ifaces: bool,
|
||||
backend_host: str,
|
||||
backend_port: int,
|
||||
) -> None:
|
||||
"""Run the development server with install, backend, and frontend."""
|
||||
install_cmd, dev_cmd, tool_name = resolve_frontend_tools(
|
||||
vite_host, vite_port, all_ifaces
|
||||
)
|
||||
install_cmd, dev_cmd, tool_name = resolve_frontend_tools(vite_port, all_ifaces)
|
||||
|
||||
# Tell the backend where the Vite dev server is
|
||||
os.environ["FASTAPI_VUE_FRONTEND_URL"] = (
|
||||
f"http://{vite_host or 'localhost'}:{vite_port}"
|
||||
)
|
||||
os.environ["FASTAPI_VUE_FRONTEND_URL"] = f"http://localhost:{vite_port}"
|
||||
# Tell Vite where the backend is (for proxying /api requests)
|
||||
os.environ["FASTAPI_VUE_BACKEND_URL"] = f"http://{backend_host}:{backend_port}"
|
||||
|
||||
backend_cmd = [
|
||||
"uvicorn",
|
||||
"MODULE_NAME.app:app",
|
||||
"--host",
|
||||
backend_host,
|
||||
"--port",
|
||||
str(backend_port),
|
||||
"--reload",
|
||||
]
|
||||
|
||||
cwd = str(Path(__file__).parent.parent)
|
||||
frontend_cwd = str(FRONTEND_PATH)
|
||||
|
||||
@@ -197,15 +170,6 @@ async def run_devserver(
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Start backend (concurrent with install)
|
||||
backend_cmd = [
|
||||
"uvicorn",
|
||||
"MODULE_NAME.app:app",
|
||||
"--host",
|
||||
backend_host,
|
||||
"--port",
|
||||
str(backend_port),
|
||||
"--reload",
|
||||
]
|
||||
stderr.write(f">>> {' '.join(backend_cmd)}\n")
|
||||
backend_proc = await asyncio.create_subprocess_exec(*backend_cmd, cwd=cwd)
|
||||
|
||||
@@ -287,15 +251,26 @@ def main():
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
vite_host, vite_port, all_ifaces = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
|
||||
backend_host, backend_port, _ = parse_endpoint(args.backend, DEFAULT_BACKEND_PORT)
|
||||
# Backend host defaults to localhost (never None)
|
||||
backend_host = backend_host or DEFAULT_HOST
|
||||
# parse_endpoint returns list of dicts with host/port or uds keys
|
||||
# Multiple entries means bind all interfaces (IPv4 + IPv6)
|
||||
vite_endpoints = parse_endpoint(args.frontend, DEFAULT_VITE_PORT)
|
||||
backend_endpoints = parse_endpoint(args.backend, DEFAULT_BACKEND_PORT)
|
||||
|
||||
# Vite doesn't support unix sockets
|
||||
if "uds" in vite_endpoints[0]:
|
||||
stderr.write("┃ ⚠️ Unix sockets not supported for frontend\n")
|
||||
raise SystemExit(1)
|
||||
if "uds" in backend_endpoints[0]:
|
||||
stderr.write("┃ ⚠️ Unix sockets not supported for backend\n")
|
||||
raise SystemExit(1)
|
||||
|
||||
vite_port = vite_endpoints[0]["port"]
|
||||
all_ifaces = len(vite_endpoints) > 1
|
||||
backend_host = backend_endpoints[0]["host"]
|
||||
backend_port = backend_endpoints[0]["port"]
|
||||
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(
|
||||
run_devserver(vite_host, vite_port, all_ifaces, backend_host, backend_port)
|
||||
)
|
||||
asyncio.run(run_devserver(vite_port, all_ifaces, backend_host, backend_port))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -51,10 +51,9 @@ def find_build_tool():
|
||||
"npm": ("install",),
|
||||
"bun": ("--bun", "install"),
|
||||
}
|
||||
# Use build-only for deno to avoid npm-run-all2 issues with run-p
|
||||
# (run-p tries to spawn npm which doesn't exist in deno)
|
||||
# Run vite directly for deno to avoid npm-run-all2/run-p issues
|
||||
build = {
|
||||
"deno": ("task", "build-only"),
|
||||
"deno": ("run", "-A", "npm:vite", "build"),
|
||||
"npm": ("run", "build"),
|
||||
"bun": ("--bun", "run", "build"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user