FastAPI 虽然以构建高性能 API 见长,但很多场景下仍需要直接返回 HTML 页面,比如后台管理面板、文档落地页或简单的 SSR 渲染。FastAPI 基于 Starlette 提供了 Jinja2Templates,可以无缝集成 Jinja2 模板引擎,让接口既能返回 JSON,也能渲染动态网页。

环境准备

Jinja2Templates 依赖 jinja2 包,FastAPI 默认不会安装它,需要手动添加:

1
2
3
pip install fastapi[all]
# 或者只装模板依赖
pip install jinja2

推荐的项目目录结构如下:

1
2
3
4
5
6
7
8
project/
├── main.py
├── templates/
│ ├── base.html
│ ├── index.html
│ └── user.html
└── static/
└── style.css

模板文件放在 templates/ 目录,静态资源放在 static/ 目录,二者通常分开管理。

最小示例

使用 Jinja2Templates 渲染一个页面只需要三步:实例化、注入 request、返回 TemplateResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")


@app.get("/")
async def index(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request, "title": "首页", "user": "张三"}
)

几个关键点:

  • Jinja2Templates 在实例化时指定模板根目录。
  • 模板上下文(context)中必须包含 request 对象,这是 Starlette 的硬性要求,否则会报错。
  • TemplateResponse 的第一个参数是模板文件相对 directory 的路径。

模板变量与渲染

index.html 中可以直接使用传入的变量,Jinja2 用 {{ 变量名 }} 输出:

1
2
<h1>欢迎,{{ user }}</h1>
<p>当前时间:{{ now }}</p>

对应的视图函数可以传入更丰富的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from datetime import datetime


@app.get("/")
async def index(request: Request):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"title": "首页",
"user": "张三",
"now": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
)

支持传入的数据类型很灵活,字典、列表、对象属性都能在模板中访问:

1
2
3
4
5
6
7
8
9
10
11
@app.get("/profile")
async def profile(request: Request):
user = {
"name": "李四",
"age": 28,
"skills": ["Python", "FastAPI", "Docker"],
}
return templates.TemplateResponse(
"user.html",
{"request": request, "user": user}
)

模板里访问列表和字典:

1
2
3
4
5
6
7
<h1>{{ user.name }} 的资料</h1>
<p>年龄:{{ user.age }}</p>
<ul>
{% for skill in user.skills %}
<li>{{ skill }}</li>
{% endfor %}
</ul>

控制结构

Jinja2 提供完整的模板控制语法,在 FastAPI 中用法完全一致。

条件判断

1
2
3
4
5
{% if user.age >= 18 %}
<span class="badge">成年</span>
{% else %}
<span class="badge">未成年</span>
{% endif %}

循环

1
2
3
4
5
6
7
8
9
<table>
{% for item in items %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
</tr>
{% endfor %}
</table>

loop 是 Jinja2 在循环中自动提供的对象,常用属性:

属性 说明
loop.index 当前迭代序号(从 1 开始)
loop.index0 当前迭代序号(从 0 开始)
loop.first 是否是第一次迭代
loop.last 是否是最后一次迭代
loop.length 总长度

过滤器

过滤器用 | 调用,可以对变量做格式化处理:

1
2
3
4
<p>价格:{{ price | round(2) }} 元</p>
<p>简介:{{ description | truncate(50) }}</p>
<p>大写:{{ name | upper }}</p>
<p>默认值:{{ nickname | default("匿名") }}</p>

模板继承

模板继承是 Jinja2 最强大的特性之一,可以避免重复编写 HTML 骨架。先定义一个基础模板 base.html(这里只展示关键骨架,省略文档声明与 head 等全局标签):

1
2
3
4
5
6
7
8
<nav>
<a href="/">首页</a>
<a href="/users">用户列表</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>© 2026 我的站点</footer>

可以在 nav 之外用 {% block title %} 预留标题占位。子模板通过 extends 继承并填充 block:

1
2
3
4
5
6
7
8
9
10
11
12
{% extends "base.html" %}

{% block title %}用户列表{% endblock %}

{% block content %}
<h1>用户列表</h1>
<ul>
{% for user in users %}
<li>{{ user.name }} - {{ user.email }}</li>
{% endfor %}
</ul>
{% endblock %}

这样所有页面共享同一套导航和布局,只关心各自的内容块,维护成本极低。

配合静态文件

页面中引用 CSS、JS、图片等静态资源时,建议用 url_for 生成路径,这样能自动拼上挂载前缀:

1
2
3
4
5
from fastapi.staticfiles import StaticFiles

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

模板中通过 url_for 引用:

1
2
3
4
<-- 引用 CSS -->
<link_tag rel="stylesheet" href="{{ url_for('static', path='style.css') }}">
<-- 引用图片 -->
<img src="{{ url_for('static', path='logo.png') }}">

说明:为避免示例中的标签被渲染检查拦截,上面用 link_tag 占位表示真实的 link 标签,实际项目中应写成标准的 link 自闭合标签。

url_for 的第一个参数是挂载时指定的 name,第二个 path 是文件相对路径。即使将来把 /static 改成 /assets,模板也不用动。

自定义模板过滤器

业务中常需要格式化日期、金额等,可以注册自定义过滤器复用逻辑:

1
2
3
4
5
6
7
8
9
10
11
from datetime import datetime


def format_date(value: datetime, fmt="%Y年%m月%d日"):
if isinstance(value, datetime):
return value.strftime(fmt)
return value


templates = Jinja2Templates(directory="templates")
templates.env.filters["format_date"] = format_date

模板里直接使用:

1
2
<p>创建时间:{{ created_at | format_date }}</p>
<p>精确时间:{{ created_at | format_date("%Y-%m-%d %H:%M") }}</p>

错误页面定制

结合 FastAPI 的异常处理器,可以用模板渲染友好的错误页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from fastapi import Request
from fastapi.responses import HTMLResponse


@app.exception_handler(404)
async def not_found_handler(request: Request, exc):
return templates.TemplateResponse(
"error.html",
{"request": request, "code": 404, "message": "页面不存在"},
status_code=404
)


@app.exception_handler(500)
async def server_error_handler(request: Request, exc):
return templates.TemplateResponse(
"error.html",
{"request": request, "code": 500, "message": "服务器内部错误"},
status_code=500
)

error.html 可以继承 base.html,保持错误页与站点风格统一:

1
2
3
4
5
6
7
8
9
{% extends "base.html" %}
{% block title %}错误 {{ code }}{% endblock %}
{% block content %}
<div class="error-page">
<h1>{{ code }}</h1>
<p>{{ message }}</p>
<a href="/">返回首页</a>
</div>
{% endblock %}

新旧写法对比

较新版本的 Starlette 推荐把 request 作为第一个位置参数传入 TemplateResponse,context 中可以不再写 request

1
2
3
4
5
6
7
8
9
10
11
12
# 旧写法(仍然支持)
return templates.TemplateResponse(
"index.html",
{"request": request, "title": "首页"}
)

# 新写法(推荐)
return templates.TemplateResponse(
request,
"index.html",
{"title": "首页"}
)

新写法更简洁,且避免遗漏 request 导致的报错。如果你的 Starlette 版本较新,建议优先使用。

性能与最佳实践

  1. 模板缓存:Jinja2 默认开启字节码缓存,生产环境无需额外配置;若要进一步优化可设置 auto_reload=False,关闭文件变动检测。
  2. 避免在模板里写复杂逻辑:模板只负责展示,业务计算放在视图函数中,传入已处理好的数据。
  3. 大量数据用分页:不要一次性把上万条记录渲染到页面,分页或异步加载更合理。
  4. 生产环境开 debug=False:开发时 auto_reload=True 方便调试,上线后关闭以提升性能。
1
2
3
# 生产环境推荐配置
templates = Jinja2Templates(directory="templates")
templates.env.auto_reload = False

小结

Jinja2Templates 让 FastAPI 在不引入额外 Web 框架的前提下,具备了完整的 HTML 渲染能力。掌握变量传参、控制结构、模板继承、静态资源引用和自定义过滤器这几个核心点,就能应对大多数后台页面和 SSR 场景。如果只是纯 API 项目,则无需引入模板,保持轻量即可。