Integrating Jinja2 Templates and Handling Form Requests in FastAPI
Jinja2 Template Engine
FastAPI, as a Python web framework, does not include a built-in HTML template engine. This flexibility allows developers to use any template engine, with Jinja2 being the officially recommended choice.
pip install jinja2
Basic Setup
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
# Serve static files from the 'static' directory
app.mount("/static", StaticFiles(directory="static"), name="static")
# Initialize Jinja2 templates from the 'templates' directory
templates = Jinja2Templates(directory="templates")
@app.get("/items/{item_id}", response_class=HTMLResponse)
def read_item(request: Request, item_id: str):
return templates.TemplateResponse(
"item_detail.html",
{"request": request, "item_id": item_id}
)
Important Notes:
- You can also import
Jinja2Templatesfromstarlette.templating. FastAPI provides the same functionality for developer convenience. - In
TemplateResponse, the first parameter is the HTML file path, and the second is a dictionary of data to pass to the template. The dictionary must include therequestobject.
Response Class Fundamentals
By default, FastAPI uses JSONResponse for responses. You can specify different response types using the response_class parameter.
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/simple/{item_id}", response_class=HTMLResponse)
def simple_html(item_id: str):
return f"""
<html>
<head>
<title>Item Details</title>
</head>
<body>
<h1>Item ID: {item_id}</h1>
</body>
</html>
"""
Template Data Passing
@app.get("/students", response_class=HTMLResponse)
def display_students(request: Request):
single_student = {
'name': 'Alex',
'age': 25,
'gender': 'Male'
}
student_roster = [
{'name': 'Alex', 'age': 25, 'gender': 'Male'},
{'name': 'Jamie', 'age': 22, 'gender': 'Female'},
{'name': 'Taylor', 'age': 23, 'gender': 'Non-binary'}
]
student_directory = {
'stu_001': {'name': 'Alex', 'age': 25, 'gender': 'Male'},
'stu_002': {'name': 'Jamie', 'age': 22, 'gender': 'Female'},
'stu_003': {'name': 'Taylor', 'age': 23, 'gender': 'Non-binary'}
}
# Pass single student using dictionary unpacking
return templates.TemplateResponse(
"single_student.html",
{"request": request, **single_student}
)
# Pass list of students
return templates.TemplateResponse(
"student_list.html",
{"request": request, 'students': student_roster}
)
# Pass dictionary of students
return templates.TemplateResponse(
"student_dict.html",
{"request": request, 'student_data': student_directory}
)
Jinja2 Template Syntax
Jinja2 uses three main syntax elements:
- Control Structures (logic):
{% %} - Variable Output:
{{ }} - Comments:
{# #}
Template Expressions
- Variables passed from FastAPI (e.g.,
{{ name }}) - Python basic types: strings, numbers, lists, tuples, dictionaries, booleans
- Operations:
- Arithmetic:
{{ 2 + 3 }} - Comparison:
{{ 2 > 1 }} - Logical:
{{ False and True }}
- Arithmetic:
- Filters (
|) and tests (is) - Function calls:
{{ current_time() }} - List indexing:
{{ items[1] }} - Membership:
{{ 1 in [1,2,3] }} - String concatenation (
~):{{ "Hello " ~ name ~ "!" }} - None handling:
{{ name or "" }}
Control Statements
Conditional Statements:
{% if name and name == 'admin' %}
<h1>Admin Console</h1>
{% elif name %}
<h1>Welcome {{ name }}!</h1>
{% else %}
<h1>Please Login</h1>
{% endif %}
Loop Statemetns:
{% for student in students %}
{{ student }}
{% endfor %}
Filter Functions
Filters are transformation functions that modify variables. They can be thought of as Jinja2's built-in functions.
Common Filters
| Filter Name | Description |
|---|---|
| safe | Render value without escaping |
| capitalize | Convert first character to uppercase, others to lowercase |
| lower | Convert to lowercase |
| upper | Convert to uppercase |
| title | Capitalize each word |
| trim | Remove leading/trailing whitespace |
| striptags | Remove all HTML tags before rendering |
| join | Join multiple values into a string |
| replace | Replace string values |
| round | Round numbers (default) or control with parameters |
| int | Convert value to integer |
String Filters
<body>
{# Display default string when variable is undefined #}
<p>{{ name | default('No name') }}</p>
{# Capitalize first letter #}
<p>{{ 'hello world' | capitalize }}</p>
{# Convert to lowercase #}
<p>{{ 'XML' | lower }}</p>
{# Trim whitespace #}
<p>{{ ' hello ' | trim }}</p>
{# Reverse string #}
<p>{{ 'hello' | reverse }}</p>
{# Format output #}
<p>{{ '%s is %d' | format("Number", 99) }}</p>
{# Disable HTML auto-escaping #}
<p>{{ '<em>name</em>' | safe }}</p>
{% autoescape false %}
{# HTML escape #}
<p>{{ '<em>name</em>' | escape }}</p>
{% endautoescape %}
</body>
Numeric Filters
{# Round to nearest integer #}
<p>{{ 12.98 | round }}</p>
{# Round down to 2 decimal places #}
<p>{{ 12.8888 | round(2, 'floor') }}</p>
{# Absolute value #}
<p>{{ -12 | abs }}</p>
List Filters
{# First element #}
<p>{{ [1,2,3] | first }}</p>
{# Last element #}
<p>{{ [1,2,3] | last }}</p>
{# List length #}
<p>{{ [1,2,3,4,5] | length }}</p>
{# Sum of list #}
<p>{{ [1,2,3,4,5] | sum }}</p>
{# Sort list (ascending by default) #}
<p>{{ [3,2,1,5,4] | sort }}</p>
{# Join list elements #}
<p>{{ [1,2,3,4,5] | join(' | ') }}</p>
{# Convert all elements to uppercase #}
<p>{{ ['alex','bob','ada'] | upper }}</p>
Dicsionary Filters
{% set user_data=[{'name':'Tom','gender':'M','age':20},
{'name':'John','gender':'M','age':18},
{'name':'Mary','gender':'F','age':24},
{'name':'Bob','gender':'M','age':31},
{'name':'Lisa','gender':'F','age':19}] %}
{# Sort by attribute in descending order #}
<ul>
{% for user in user_data | sort(attribute='age', reverse=true) %}
<li>{{ user.name }}, {{ user.age }}</li>
{% endfor %}
</ul>
{# Group by attribute #}
<ul>
{% for group in user_data|groupby('gender') %}
<li>{{ group.grouper }}<ul>
{% for user in group.list %}
<li>{{ user.name }}</li>
{% endfor %}</ul></li>
{% endfor %}
</ul>
{# Extract and join specific attributes #}
<p>{{ user_data | map(attribute='name') | join(', ') }}</p>
Test Functions
Test functions return boolean values and are used with the is keyword to test variables or expressions.
{# Check if variable is defined #}
{% if name is defined %}
<p>Name is: {{ name }}</p>
{% endif %}
{# Check if all characters are uppercase #}
{% if name is upper %}
<h2>"{{ name }}" is all uppercase.</h2>
{% endif %}
{# Check if variable is None #}
{% if name is none %}
<h2>Variable is None.</h2>
{% endif %}
{# Check if variable is a string #}
{% if name is string %}
<h2>{{ name }} is a string.</h2>
{% endif %}
{# Check if number is even #}
{% if 2 is even %}
<h2>Variable is an even number.</h2>
{% endif %}
{# Check if variable is iterable #}
{% if [1,2,3] is iterable %}
<h2>Variable is iterable.</h2>
{% endif %}
{# Check if variable is a dictionary #}
{% if {'name':'test'} is mapping %}
<h2>Variable is a dictionary.</h2>
{% endif %}
Template Inheritance
Template inheritance helps avoid code duplication for common elements like headers, footers, and navigation.
Parent Template (base.html):
<body>
Header Content
{% block header %}
{% endblock %}
Main Content Area
{% block content %}
{% endblock %}
Footer Content
{% block footer %}
{% endblock %}
</body>
Child Template:
{% extends "base.html" %}
{% block content %}
{{ super() }}
<h1>Custom Content</h1>
{% endblock %}
{% block header %}
{{ super() }}
<h1>Custom Header</h1>
{% endblock %}
Handling Form Requests
1. Processing Employee Information Form
@app.get("/employee-form", response_class=HTMLResponse)
def show_employee_form(request: Request):
return templates.TemplateResponse(
"employee_form.html",
{"request": request}
)
@app.post("/submit-employee")
def submit_employee(
name: str = Form(),
gender: str = Form(),
phone: str = Form(),
email: str = Form()
):
print(f"Name: {name}")
print(f"Gender: {gender}")
print(f"Phone: {phone}")
print(f"Email: {email}")
return 'Submission Successful'
2. Handling File Uploads with Form Data
from typing import Union
from fastapi import UploadFile
@app.post("/submit-employee-with-photo")
def submit_employee_with_photo(
name: str = Form(),
gender: str = Form(),
phone: str = Form(),
email: str = Form(),
photo: Union[UploadFile, None] = None
):
print(f"Name: {name}")
print(f"Gender: {gender}")
print(f"Phone: {phone}")
print(f"Email: {email}")
if photo:
print(f"Uploaded file: {photo.filename}")
# Save the uploaded file
with open('employee_photo.jpg', 'wb') as file_buffer:
file_buffer.write(photo.file.read())
return 'Submission Successful'