DEV Community

Cover image for Build Your Own Forum with FastAPI: Step 3 - HTML Template
Leapcell
Leapcell

Posted on

Build Your Own Forum with FastAPI: Step 3 - HTML Template

Cover

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

What core changes did we make?

  1. Imported Jinja2Templates from fastapi.templating.
  2. Created a template engine instance with templates = Jinja2Templates(directory="templates"), telling it that the template files are stored in the templates directory.
  3. Deleted the previous generate_html_response function used for concatenating HTML strings, and instead of returning an HTMLResponse, we now call templates.TemplateResponse().
  4. 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 the request object and the posts list queried from the database.

Step 4: Run and verify

Restart your uvicorn server:

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

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.

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.


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)