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:
uv add jinja2
# or pip install jinja218.2 Jinja2Templates and TemplateResponse
The fixed four-step pattern for using templates:
- Import and create a
Jinja2Templatesobject (global reuse); - Declare a
Requestparameter in the path function; - Render with
templates.TemplateResponse(...); - Read context variables in the template.
# 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>
<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:
<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:
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:
# 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:
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_forin 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.htmlfor 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
- Create a
templates/directory with anitem.htmlJinja2 template; render it atGET /items/{id}withurl_forlinks. - Mount a
static/directory with a CSS file; link it in the template via/static/style.css; verify the browser loads it. - Build a simple Vue app (
npm create vite@latest frontend), copy the dist folder tofrontend/dist/, add the SPA fallback route, and verify client-side routing works.
Proceed to Chapter 19: Large Project Structure with APIRouter.