Build Your Own Forum with FastAPI: Step 3 - HTML Template
James Reed
Infrastructure Engineer · Leapcell

In the previous article, we introduced a PostgreSQL database to our forum, achieving persistent data storage, so that data is no longer lost even if the server restarts.
Now we can confidently make more improvements. However, you may have noticed that all of our current interface styles (HTML) are written directly in main.py
. Does this mean that for every new feature in the future, we have to stuff more HTML into main.py
?
This is not only troublesome to write, but it also leads to a large number of HTML strings being mixed into the Python code, making the code difficult to read and maintain.
To solve this problem, this article will introduce the Jinja2 template engine to separate the backend logic (Python) from the frontend presentation (HTML), making the project structure clearer and easier to maintain.
Step 1: Install Jinja2
The officially recommended template engine for FastAPI is Jinja2. Make sure your virtual environment is activated, then run the following command:
pip install jinja2
Step 2: Create a templates directory and files
To better organize the project, we need to create a directory specifically for storing HTML template files.
In your project's root directory, create a new folder named templates
.
fastapi-forum/
├── templates/ <-- New directory
│ └── posts.html <-- New template file
├── main.py
├── database.py
├── models.py
└── venv/
Next, we will move the HTML code from the generate_html_response
function in main.py
to the new templates/posts.html
file and modify it using Jinja2 syntax.
templates/posts.html
<!DOCTYPE html> <html> <head> <title>My FastAPI Forum</title> <style> body { font-family: sans-serif; margin: 2em; } input, textarea { width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; } button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; } button:hover { background-color: #0056b3; } </style> </head> <body> <h1>Welcome to My Forum</h1> <h2>Create a New Post</h2> <form action="/api/posts" method="post"> <input type="text" name="title" placeholder="Post title" required /><br /> <textarea name="content" rows="4" placeholder="Post content" required></textarea><br /> <button type="submit">Post</button> </form> <hr /> <h2>Post list</h2> {% for post in posts %} <div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;"> <h3>{{ post.title }} (ID: {{ post.id }})</h3> <p>{{ post.content }}</p> </div> {% endfor %} </body> </html>
The main changes are in the Post list
section:
- Use
{% for post in posts %}
and{% endfor %}
to replace the Python for loop. - Use double curly brace syntax like
{{ post.title }}
to dynamically insert variables.
The posts
variable will be passed in by our FastAPI backend when rendering the template.
Step 3: Configure and use templates in FastAPI
Now that the HTML has been separated, we need to modify main.py
to tell FastAPI how to find and use these template files.
main.py
(Final version)
from fastapi import FastAPI, Form, Depends, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from typing import List import models from database import get_db app = FastAPI() # 1. Configure Jinja2Templates templates = Jinja2Templates(directory="templates") # --- Routes --- @app.get("/", response_class=RedirectResponse) def read_root(): return RedirectResponse(url="/posts", status_code=303) # Route for displaying the page @app.get("/posts", response_class=HTMLResponse) async def view_posts(request: Request, db: AsyncSession = Depends(get_db)): # Query all posts from the database result = await db.execute(select(models.Post).order_by(desc(models.Post.id))) posts = result.scalars().all() # 2. Render using the template return templates.TemplateResponse("posts.html", {"request": request, "posts": posts}) @app.post("/api/posts") async def create_post( title: str = Form(...), content: str = Form(...), db: AsyncSession = Depends(get_db) ): # Create a new Post object new_post = models.Post(title=title, content=content) # Add to the database session db.add(new_post) # Commit and save to the database await db.commit() # Refresh the object to get the newly generated ID await db.refresh(new_post) return RedirectResponse(url="/posts", status_code=303)
What core changes did we make?
- Imported
Jinja2Templates
fromfastapi.templating
. - Created a template engine instance with
templates = Jinja2Templates(directory="templates")
, telling it that the template files are stored in thetemplates
directory. - Deleted the previous
generate_html_response
function used for concatenating HTML strings, and instead of returning anHTMLResponse
, we now calltemplates.TemplateResponse()
. TemplateResponse
accepts these parameters: the template filename ("posts.html"
) and a context dictionary containing all the data to be passed to the template. We passed therequest
object and theposts
list queried from the database.
Step 4: Run and verify
Restart your uvicorn server:
uvicorn main:app --reload
Open your browser and go to http://127.0.0.1:8000
. You will find that the page looks exactly the same as before, and all functions are working normally.
However, the internal structure of the project is completely different now. Your Python code is now only responsible for handling data and logic, while the HTML code focuses on displaying the content. This makes it much easier and more efficient to modify page styles or add new features in the future.
Deploy the project online
Same as the first tutorial, you can deploy the results of this step online for your friends to experience the changes and progress of the project.
A simple deployment solution is to use Leapcell.
If you have deployed before, just push the code to your Git repository, and Leapcell will automatically redeploy the latest code for you.
If you have not used Leapcell's deployment service, you can refer to the tutorial in this article.
Summary
Congratulations! You have successfully integrated the Jinja2 template engine into your FastAPI project.
The current forum allows anyone to post anonymously, but this can't really be considered a forum. A real community not only needs to have different users, but each user also needs to have their own identity.
In the next article, we will add a user system to the forum, implementing user registration and login functions, allowing each user to access the forum with their own identity.