DEV Community

Cover image for Build a Perfect Blog with FastAPI: Comment System
Leapcell
Leapcell

Posted on

Build a Perfect Blog with FastAPI: Comment System

In the previous article, we implemented full user login and session management for our FastAPI blog. Now, the server can "remember" a user's login status and protect pages that require authentication.

Since we can now distinguish between logged-in users and guests, it's the perfect time to add interactive features to our blog.

In this article, we will add a fundamental yet crucial feature to our blog: a comment system.

Specifically, we will implement the following functionalities:

  • Display a list of comments below each post.
  • Allow logged-in users to post comments on articles.

Step 1: Create the Data Model for Comments

Just like our Post and User models, our Comment model needs its own data model and relationships with Post and User.

1. Create the Comment Model

Open the models.py file, add the Comment model, and update the User and Post models to establish a bi-directional relationship.

# models.py
import uuid
from datetime import datetime
from typing import Optional, List
from sqlmodel import Field, SQLModel, Relationship

class User(SQLModel, table=True):
    id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
    username: str = Field(unique=True, index=True)
    password: str

    # Add a one-to-many relationship with Comment
    comments: List["Comment"] = Relationship(back_populates="user")

class Post(SQLModel, table=True):
    id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
    title: str
    content: str
    createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False)

    # Add a one-to-many relationship with Comment
    comments: List["Comment"] = Relationship(back_populates="post")

class Comment(SQLModel, table=True):
    id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True)
    content: str
    createdAt: datetime = Field(default_factory=datetime.utcnow, nullable=False)

    # Define foreign keys, linking to the Post and User tables
    postId: uuid.UUID = Field(foreign_key="post.id")
    userId: uuid.UUID = Field(foreign_key="user.id")

    # Define many-to-one relationships
    post: Post = Relationship(back_populates="comments")
    user: User = Relationship(back_populates="comments")
Enter fullscreen mode Exit fullscreen mode

Here, we've done three things:

  1. Created the Comment model, which includes content, createdAt, and the foreign keys postId and userId that point to the post and user tables.
  2. Used Relationship in the Comment model to define its "many-to-one" relationship with Post and User.
  3. Added the reverse "one-to-many" relationship comments to the User and Post models. This allows us to easily retrieve all comments for a Post object or all comments made by a User object.

Because we configured the create_db_and_tables function in main.py, it will automatically detect the SQLModel models and create or update the corresponding database tables when the application starts. We don't need to execute SQL manually.

If you need to execute SQL manually, and your database is created on Leapcell,

Leapcell

you can easily execute SQL statements through its graphical interface. Just go to the Database management page on the website, paste the above statements into the SQL interface, and execute them.

ImageP0

Step 2: Implement the Business Logic for Comments

Next, we will create functions to handle comment creation and queries.

Create a new file comments_service.py in the project's root directory to store the business logic related to comments.

# comments_service.py
import uuid
from typing import List
from sqlmodel import Session, select
from models import Comment

def get_comments_by_post_id(post_id: uuid.UUID, session: Session) -> List[Comment]:
    """Finds all comments for a given post ID and sorts them by creation time in ascending order."""
    statement = select(Comment).where(Comment.postId == post_id).order_by(Comment.createdAt)
    comments = session.exec(statement).all()
    return comments

def create_comment(content: str, user_id: uuid.UUID, post_id: uuid.UUID, session: Session) -> Comment:
    """Creates a new comment."""
    new_comment = Comment(content=content, userId=user_id, postId=post_id)
    session.add(new_comment)
    session.commit()
    session.refresh(new_comment)
    return new_comment
Enter fullscreen mode Exit fullscreen mode

The get_comments_by_post_id function is used to fetch all comments under a post. create_comment is used to save a new comment to the database. Because we have correctly set up the Relationship in our models, we can later conveniently access the commenter's username in the template using comment.user.username.

Step 3: Create Routes for Submitting and Displaying Comments

Now, we need to integrate the comment functionality into the post page. This requires two parts: a backend route to receive user-submitted comments, and an update to the post detail page route to display the comments.

1. Create the Comment Route

Create a new file comments.py in the routers folder:

# routers/comments.py
import uuid
from fastapi import APIRouter, Depends, Form
from fastapi.responses import RedirectResponse
from sqlmodel import Session

from database import get_session
import comments_service
from auth_dependencies import login_required

router = APIRouter()

@router.post("/posts/{post_id}/comments")
def create_comment_for_post(
    post_id: uuid.UUID,
    content: str = Form(...),
    user: dict = Depends(login_required), # Dependency injection to ensure the user is logged in
    session: Session = Depends(get_session)
):
    # The user ID from the session is a string, needs to be converted to a UUID type
    user_id = uuid.UUID(user["id"])

    comments_service.create_comment(
        content=content, user_id=user_id, post_id=post_id, session=session
    )

    # After a successful comment, redirect back to the post page
    return RedirectResponse(url=f"/posts/{post_id}", status_code=302)
Enter fullscreen mode Exit fullscreen mode

This route only handles POST requests. We use the login_required dependency we created earlier to protect it, ensuring only logged-in users can post comments. After a comment is successfully created, the page redirects back to the original post detail page.

2. Update the Main Application and Post Routes

First, let's mount the comment router we just created in main.py.

# main.py
# ... other imports
from routers import posts, users, auth, comments # import the comments router

# ...

app = FastAPI(lifespan=lifespan)

# ...

# Include routers
app.include_router(posts.router)
app.include_router(users.router)
app.include_router(auth.router)
app.include_router(comments.router) # Mount the comments router
Enter fullscreen mode Exit fullscreen mode

Next, modify the get_post_by_id function in routers/posts.py so that when it renders the post detail page, it also fetches and passes all the comments for that post.

# routers/posts.py
# ... imports ...
from auth_dependencies import get_user_from_session
import comments_service # Import the comment service

# ... router and templates definition ...

# ... other routes ...

@router.get("/posts/{post_id}", response_class=HTMLResponse)
def get_post_by_id(
    request: Request, 
    post_id: uuid.UUID, 
    session: Session = Depends(get_session),
    user: dict | None = Depends(get_user_from_session)
):
    post = session.get(Post, post_id)
    # Get all comments for this post
    comments = comments_service.get_comments_by_post_id(post_id, session)

    # Pass post, user, and comments together to the template
    return templates.TemplateResponse(
        "post.html", 
        {
            "request": request, 
            "post": post, 
            "title": post.title, 
            "user": user,
            "comments": comments # New
        }
    )
Enter fullscreen mode Exit fullscreen mode

Step 4: Update the Frontend View

The final step is to modify the template file to display the comment list and the comment form.

Open templates/post.html, and add the following code below the post content and above the back link:

<div class="post-content">{{ post.content | replace('\n', '<br>') | safe }}</div>
</article>

<section class="comments-section">
  <h3>Comments</h3>

  <div class="comment-list">
    {% if comments %}
      {% for comment in comments %}
      <div class="comment-item">
        <p class="comment-content">{{ comment.content }}</p>
        <small>
          By <strong>{{ comment.user.username }}</strong> on {{ comment.createdAt.strftime('%Y-%m-%d') }}
        </small>
      </div>
      {% endfor %}
    {% else %}
      <p>No comments yet. Be the first to comment!</p>
    {% endif %}
  </div>

  {% if user %}
  <form action="/posts/{{ post.id }}/comments" method="POST" class="comment-form">
    <h4>Leave a Comment</h4>
    <div class="form-group">
      <textarea name="content" rows="4" placeholder="Write your comment here..." required></textarea>
    </div>
    <button type="submit">Submit Comment</button>
  </form>
  {% else %}
  <p><a href="/auth/login">Login</a> to leave a comment.</p>
  {% endif %}
</section>

<a href="/" class="back-link">&larr; Back to Home</a>
{% include "_footer.html" %}
Enter fullscreen mode Exit fullscreen mode
  • We use {% for comment in comments %} to loop through and display all the comments. Through comment.user.username, we can directly show the commenter's username.
  • We use {% if user %} to check if the user is logged in. If they are, the comment form is displayed; otherwise, a login link is shown.

To make the page look better, you can add some styles to public/css/style.css:

/* ... other styles ... */
.comments-section {
  margin-top: 3rem;
  border-top: 1px solid #eee;
  padding-top: 2rem;
}
.comment-list .comment-item {
  background: #f9f9f9;
  border: 1px solid #ddd;
  padding: 1rem;
  border-radius: 5px;
  margin-bottom: 1rem;
}
.comment-content {
  margin-top: 0;
}
.comment-item small {
  color: #666;
}
.comment-form {
  margin-top: 2rem;
}
.comment-form textarea {
  width: 100%;
  padding: 0.5rem;
  margin-bottom: 1rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}
Enter fullscreen mode Exit fullscreen mode

Running and Testing

Now, restart your application:

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

Open your browser and navigate to any post's detail page. You will see the new comments section.

ImageP1

Type something into the comment box and submit it. After the page refreshes, you will see the comment you just posted in the comment list.

ImageP2

Congratulations, you have successfully added a comment system to your blog!

Of course, the current comment functionality is still quite basic. In the next article, we will continue to enhance this feature by implementing logic for authors to reply to comments, taking the blog's interactivity to the next level.


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)