Although FastAPI is built for APIs, it can also serve static assets (CSS, JS, images) and render server-side HTML with Jinja2 — useful for admin pages, dashboards or simple frontends that live in the same service.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount() attaches a full ASGI sub-application at a path prefix — here /static/* is served directly from the static/ directory.StaticFiles(directory="static", html=True) additionally serves index.html for the directory root.pip install jinja2
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/dashboard/", response_class=HTMLResponse)
async def dashboard(request: Request):
stats = {"total_items": 42, "low_stock": 3}
return templates.TemplateResponse(
request=request,
name="dashboard.html",
context={"title": "Dashboard", "stats": stats},
)
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ title }}</h1>
<p>Total items: {{ stats.total_items }}</p>
{% if stats.low_stock > 0 %}
<p class="alert">{{ stats.low_stock }} items low on stock!</p>
{% endif %}
</body>
</html>
Jinja2 syntax: {{ variable }} for output, {% ... %} for logic (if/for/block), filters like {{ price | round(2) }}.
from fastapi.responses import HTMLResponse
@app.get("/hello/", response_class=HTMLResponse)
def hello():
return "<h1>Hello</h1>"
response_class=HTMLResponse makes the docs show a media type of text/html; without a model, the string returned is sent as-is.
A: For real products, prefer a separated SPA (React/Angular/Vue) consuming the API — independent deploys and scaling. Templates are great for server-rendered admin panels, emails, PDF reports or small internal tools where a second frontend would be overkill.
app.mount("/static", ...) fail with a runtime error?A: Usually because the directory does not exist relative to the working directory where Uvicorn was started. Create the folder or resolve the path from the file location: Path(__file__).parent / "static".
A: Create the Jinja2Templates instance once (e.g. in a core/templates.py module or via a dependency) and import it in your routers — creating one per request/module repeatedly wastes resources.
app.mount() with StaticFiles serves assets, and Jinja2Templates renders HTML pages — enough to give an API a small face (health dashboards, admin screens) without leaving the FastAPI process.