This article explains how to set up a voting route in a FastAPI application for handling likes and unlikes on posts.
In this lesson, we will set up the voting route for our FastAPI application. This endpoint handles likes and unlikes for posts based on the vote direction provided in the request. A vote direction of 1 indicates a “like” while a direction of 0 indicates that the user wants to remove their like.Below is an example payload that the route expects in the request body:
Copy
{ "post_id": 1432, "dir": 0}
The route is created at /vote and requires only the post ID and vote direction. The user ID is extracted from the JWT token, reducing the parameters needed in the request body.When a user votes, the logic is as follows:
If the vote direction is 1 and the user has already liked the post, the endpoint returns an HTTP 409 Conflict error.
If the vote direction is 0 and the user has not liked the post, the endpoint returns an HTTP 404 Not Found error.
Otherwise, the vote is either added or removed accordingly.
Create a new file vote.py in the routers folder. Start by copying the necessary import statements from one of the other routers:
Copy
from fastapi import FastAPI, Response, status, HTTPException, Depends, APIRouter
When you save the file, you might see server log messages similar to:
Copy
WARNING: WatchGodReload detected file change in '['C:\\Users\\sanje\\Documents\\Courses\\fastapi\\app\\routers\\vote.py.77e8e3b44d0396d5c89b3f4c1b82083.tmp']'. Reloading...INFO: Started server process [2776]INFO: Waiting for application startup.INFO: Application startup complete.
Next, create an instance of the router with the /vote prefix and the tag “Vote”:
This route uses the POST method to send data for creating or removing votes. The default status is set to 201 (Created). The initial function definition is:
Copy
@router.post("/", status_code=status.HTTP_201_CREATED)def vote(): # Logic for handling the vote will be implemented here pass
Since vote data is received in the request body, defining a Pydantic schema for validation is essential. The schema includes the post ID (an integer) and the vote direction. We use the Pydantic type conint(Le=1) to ensure that the maximum value is 1. Although this approach accepts negative numbers, it is acceptable for now. Future improvements can enforce strictly 0 or 1 values.Below is an example of our Pydantic schemas (including other models for context):
When updating your file, you might see similar logs:
Copy
WARNING: WatchGodReload detected file change in '['C:\\Users\\sanje\\Documents\\Courses\\fastapi\\app\\routers\\vote.py.77e8e3b44d039d5c89b3']'. Reloading...postgresINFO: Started server process [2776]INFO: Waiting for application startup.INFO: Application startup complete.
Update the vote function to require the vote data via the schema, a database session, and the currently authenticated user:
Copy
@router.post("/", status_code=status.HTTP_201_CREATED)def vote(vote: schemas.Vote, db: database.Session = Depends(database.get_db), current_user: int = Depends(oauth2.get_current_user)): # Check if the target post exists post = db.query(models.Post).filter(models.Post.id == vote.post_id).first() if not post: raise HTTPException(status_code=404, detail="Post not found") # Query to verify if the user has already voted on this post vote_query = db.query(models.Vote).filter( models.Vote.post_id == vote.post_id, models.Vote.user_id == current_user.id ) found_vote = vote_query.first() # If the vote direction is 1, the user wants to add a vote if vote.dir == 1: if found_vote: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"user {current_user.id} has already voted on post {vote.post_id}" ) new_vote = models.Vote(post_id=vote.post_id, user_id=current_user.id) db.add(new_vote) db.commit() return {"message": "successfully added vote"} else: # If the vote direction is 0, the user wants to remove their vote if not found_vote: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vote does not exist") vote_query.delete(synchronize_session=False) db.commit() return {"message": "successfully deleted vote"}
Always validate that the target post exists before processing any vote changes. This prevents unnecessary database operations and improves the robustness of your endpoint.
A simplified version of the vote logic is as follows:
Copy
if vote.dir == 1: if found_vote: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"user {current_user.id} has already voted on post {vote.post_id}") else: # Logic to add the vote goes here passelse: if not found_vote: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vote does not exist") else: # Logic to delete the vote goes here pass
A future enhancement is to include the number of votes as a field when retrieving post data. This allows the front-end application to display the like count without issuing an additional query. Implementing this typically involves advanced SQL and SQLAlchemy techniques such as join operations or subqueries.
If a user attempts to vote on a non-existent post, the endpoint returns a 404 error. This is managed by first querying the posts table using the provided post ID. If the post is not found, an HTTPException with a 404 status code is raised.The updated logic snippet reiterates how to handle this case:
Copy
@router.post("/", status_code=status.HTTP_201_CREATED)def vote(vote: schemas.Vote, db: database.Session = Depends(database.get_db), current_user: int = Depends(oauth2.get_current_user)): # Check if the target post exists post = db.query(models.Post).filter(models.Post.id == vote.post_id).first() if not post: raise HTTPException(status_code=404, detail="Post not found") vote_query = db.query(models.Vote).filter( models.Vote.post_id == vote.post_id, models.Vote.user_id == current_user.id ) found_vote = vote_query.first() if vote.dir == 1: if found_vote: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"user {current_user.id} has already voted on post {vote.post_id}" ) new_vote = models.Vote(post_id=vote.post_id, user_id=current_user.id) db.add(new_vote) db.commit() return {"message": "successfully added vote"} else: if not found_vote: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vote does not exist") vote_query.delete(synchronize_session=False) db.commit() return {"message": "successfully deleted vote"}
Below are images showing a code editor session and a Postman interface during testing. These images confirm that the changes have been applied correctly and help provide context.
By following these steps, you have implemented a robust voting feature in your FastAPI application. For more details on FastAPI and deployment, consider checking out the FastAPI Documentation and exploring additional resources such as Kubernetes Basics.Happy coding!