Frontend (SPA)
Nexios provides built-in support for serving single-page applications (SPAs) like React, Vue, Svelte, or any other frontend framework. The FrontendApp class serves your built static files and automatically falls back to a configurable HTML file for unknown paths — so client-side routing works seamlessly.
How It Works
- You build your frontend (e.g.
npm run build), which produces a directory of static files (typicallydist/). - Nexios serves those files directly when the path matches an existing file.
- For any other path (like
/dashboardor/settings), Nexios falls back to yourindex.html(or a custom fallback), letting your JavaScript router handle the routing.
Quick Start
The simplest way to serve a frontend is via the app.frontend() convenience method:
from nexios import NexiosApp
app = NexiosApp()
# API routes registered FIRST take precedence
@app.get("/api/health")
async def health(request, response):
return response.json({"status": "ok"})
# Serve the SPA — unknown paths fall back to index.html
app.frontend("/", directory="./dist")With this setup:
/api/health→ returns JSON (API route matched first)/js/app.js→ serves the file fromdist/js/app.js/dashboard→ servesdist/index.html(SPA fallback)/settings/profile→ also servesdist/index.html
Using FrontendApp Directly
For more control, you can instantiate FrontendApp directly and mount it as a route:
from nexios import NexiosApp
from nexios.frontend import FrontendApp
from nexios.routing import Group
app = NexiosApp()
spa = FrontendApp(
directory="./dist",
fallback="auto",
cache_control="public, max-age=3600",
)
# Mount at root
app.add_route(Group(path="/", app=spa))Fallback Behaviour
The fallback parameter controls what happens when a requested file is not found in the directory:
| Value | Behaviour |
|---|---|
"auto" (default) | Tries 404.html first, then index.html |
"app.html" | Falls back to the specified file (e.g. app.html) |
None or False | No fallback — returns a 404 JSON response |
"auto" Mode
spa = FrontendApp(directory="./dist", fallback="auto")Nexios looks for a fallback file in this order:
404.html— if present, serves this for unknown routesindex.html— otherwise, serves this as the SPA entry point
This is useful if you want a distinct error page for invalid routes without overriding the main app shell.
Custom Fallback File
spa = FrontendApp(directory="./dist", fallback="app.html")All unknown paths will serve dist/app.html.
Disable Fallback
spa = FrontendApp(directory="./dist", fallback=None)Unknown paths return a 404 JSON response instead.
Route Precedence
API routes registered before frontend() take precedence. This is critical — it ensures your backend endpoints are always reachable and not intercepted by the SPA fallback:
from nexios import NexiosApp
app = NexiosApp()
# 1. API routes are registered first
@app.get("/api/health")
async def health(request, response):
return response.json({"status": "ok"})
@app.get("/api/users")
async def get_users(request, response):
return response.json([{"id": 1, "name": "Alice"}])
# 2. Frontend catches everything else
app.frontend("/", directory="./dist")If you reverse the order, the frontend fallback would catch all paths before the API routes get a chance to match.
Cache Control
Set a Cache-Control header on all served files (both direct file hits and fallback responses):
spa = FrontendApp(
directory="./dist",
fallback="auto",
cache_control="public, max-age=86400", # 24 hours
)Mounting at a Sub-path
You can serve the frontend from a sub-path instead of the root:
# All frontend files are served under /app/
app.frontend("/app", directory="./dist")Now your SPA is accessible at /app/, and files are served from /app/js/..., /app/css/..., etc. Unknown paths under /app/ still fall back to the configured HTML file.
Using FrontendApp directly:
spa = FrontendApp(directory="./dist", fallback="auto")
app.add_route(Group(path="/app", app=spa))Security
FrontendApp includes a path traversal protection that resolves all requested paths and ensures they stay within the configured directory. Requests attempting to use ../ or symlinks to escape the directory will receive a 404 response.
Only GET requests are allowed — POST, PUT, DELETE, and other methods return 405 Method Not Allowed.
Reference
FrontendApp
class FrontendApp(
directory: Union[str, Path],
fallback: Optional[Union[str, bool]] = "auto",
cache_control: Optional[str] = None,
)NexiosApp.frontend()
def app.frontend(
path: str = "/",
directory: Union[str, Path] = "dist",
fallback: Optional[Union[str, bool]] = "auto",
name: Optional[str] = None,
cache_control: Optional[str] = None,
) -> NoneBaseRouter.frontend()
Router and sub-applications also expose frontend() with the same signature, so you can mount a frontend on a sub-router if needed.
