Static Files and Templates

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.

Serving Static Files

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

Jinja2 Templates

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},
    )

The Template (templates/dashboard.html)

<!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) }}.

Returning Raw HTML

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.

Quick Questions and Answers

Q: Should a FastAPI app render HTML, or should I split frontend and backend?

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.

Q: Why does 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".

Q: How do I use template responses inside routers without global state?

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.

Conclusion

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.