Skip to content

Chapter 18 · Template Rendering and Static Files ​

Chapter goals: Master Jinja2 template rendering, static file hosting, and "template + API" hybrid architecture and SPA frontend hosting solutions.

18.1 When You Need Server-Side Templates ​

Previous chapters covered pure JSON APIs. But many real projects need a "hybrid architecture":

  • Admin dashboards, report pages, email template rendering — HTML output directly from the backend;
  • Core business simultaneously exposes JSON APIs for frontend frameworks or third-party consumers.

FastAPI (via Starlette) is template-engine-agnostic: any template engine works. The most commonly used official option is Jinja2 (same engine as Flask).

Install the dependency:

bash
uv add jinja2
# or pip install jinja2

18.2 Jinja2Templates and TemplateResponse ​

The fixed four-step pattern for using templates:

  1. Import and create a Jinja2Templates object (global reuse);
  2. Declare a Request parameter in the path function;
  3. Render with templates.TemplateResponse(...);
  4. Read context variables in the template.
python
# main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()

# Create once, reuse globally; directory points to the template folder
templates = Jinja2Templates(directory="templates")


@app.get("/items/{id}", response_class=HTMLResponse)  # Tell the docs UI this returns HTML
def read_item(request: Request, id: int):
    # New signature: request is the first parameter, context passed separately
    return templates.TemplateResponse(
        request=request,
        name="item.html",           # Relative to the templates directory
        context={"id": id},          # Key-value pairs passed to the template
    )

Version difference

Before FastAPI 0.108.0 / Starlette 0.29.0, name was the first parameter and request had to be placed inside the context dict. Old tutorials online use TemplateResponse(name="item.html", context={"request": request, ...}) — for new projects, use the request-first signature above consistently to avoid deprecation warnings.

Template file templates/item.html:

html
<html>
  <head>
    <title>Item Details</title>
  </head>
  <body>
    <h1>Item ID: {{ id }}</h1>  <!-- {{ }} outputs a context variable -->
    <ul>
      {% for tag in ["a", "b"] %}   <!-- {% %} is Jinja2 statement syntax -->
        <li>{{ tag }}</li>
      {% endfor %}
    </ul>
  </body>
</html>

Visiting /items/42 renders Item ID: 42.

18.3 url_for in Templates ​

Templates can call url_for directly — parameters match the path operation function exactly, generating the real URL for the corresponding route — so changing the route prefix does not require updating template links:

html
<body>
  <a href="{{ url_for('read_item', id=id) }}">View {{ id }}</a>
  <!-- id=42 renders as <a href="/items/42">View 42</a> -->
</body>

The first parameter of url_for is the path operation function name (not the path); subsequent args are its parameters. This is the key to decoupling templates from routes.

18.4 StaticFiles: Mounting a Static Directory ​

CSS/JS/images and other static assets are hosted with StaticFiles, mounted under a path prefix:

python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# Mount a completely independent "sub-app" to handle all requests starting with /static
app.mount("/static", StaticFiles(directory="static"), name="static")

Now static/style.css is accessible at /static/style.css. The mounted sub-app is independent: it does not run through FastAPI's dependency injection or middleware (except CORS middleware applied at the main app level).

18.5 Hybrid Architecture: Template + API ​

A realistic project often combines both:

python
# Admin page (template) + API endpoint (JSON)
@app.get("/admin/dashboard")
async def admin_dashboard(request: Request):
    return templates.TemplateResponse(
        request=request,
        name="admin/dashboard.html",
        context={"title": "Admin Panel"},
    )


@app.get("/api/admin/stats")
async def admin_stats():
    return {"users": 1024, "orders": 5890}

The frontend template makes API calls via fetch/axios to the JSON endpoints — the classic hybrid pattern.

18.6 SPA Frontend Hosting ​

For a Vue/React single-page app, serve the built static files:

python
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path

app = FastAPI()

STATIC_DIR = Path("frontend/dist")


@app.get("/{full_path:path}")
async def spa_fallback(full_path: str):
    # If the file exists, serve it; otherwise serve index.html (SPA client-side routing)
    file_path = STATIC_DIR / full_path
    if file_path.exists() and file_path.is_file():
        return FileResponse(file_path)
    return FileResponse(STATIC_DIR / "index.html")

Mount static files first, then add the fallback route — order matters.

18.7 Chapter Summary ​

  • Jinja2Templates + TemplateResponse for server-side HTML rendering;
  • url_for in templates decouples links from route definitions;
  • StaticFiles + app.mount() for CSS/JS/image hosting;
  • Hybrid architecture: templates for admin pages, JSON APIs for frontend frameworks;
  • SPA fallback route serves index.html for client-side routing.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What is the correct TemplateResponse signature in modern FastAPI?

2. Why use url_for in templates instead of hardcoding URLs?

3. What is the purpose of the SPA fallback route?

4. Why must StaticFiles be mounted before the SPA fallback route?

🛠️ Hands-On Exercises ​

  1. Create a templates/ directory with an item.html Jinja2 template; render it at GET /items/{id} with url_for links.
  2. Mount a static/ directory with a CSS file; link it in the template via /static/style.css; verify the browser loads it.
  3. Build a simple Vue app (npm create vite@latest frontend), copy the dist folder to frontend/dist/, add the SPA fallback route, and verify client-side routing works.

Proceed to Chapter 19: Large Project Structure with APIRouter.