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, Requestfrom fastapi.templating import Jinja2Templatesapp = 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 StaticFilesapp = 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 datetimedef 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 Requestfrom 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 版本较新,建议优先使用。
性能与最佳实践
模板缓存 :Jinja2 默认开启字节码缓存,生产环境无需额外配置;若要进一步优化可设置 auto_reload=False,关闭文件变动检测。
避免在模板里写复杂逻辑 :模板只负责展示,业务计算放在视图函数中,传入已处理好的数据。
大量数据用分页 :不要一次性把上万条记录渲染到页面,分页或异步加载更合理。
生产环境开 debug=False :开发时 auto_reload=True 方便调试,上线后关闭以提升性能。
1 2 3 templates = Jinja2Templates(directory="templates" ) templates.env.auto_reload = False
小结 Jinja2Templates 让 FastAPI 在不引入额外 Web 框架的前提下,具备了完整的 HTML 渲染能力。掌握变量传参、控制结构、模板继承、静态资源引用和自定义过滤器这几个核心点,就能应对大多数后台页面和 SSR 场景。如果只是纯 API 项目,则无需引入模板,保持轻量即可。