Building Flask REST App with Flask-Restful+Auth+MongoDB

anurag kumar
10 min readJun 16, 2021

Create a complete web API with Python, Flask, JWT, and MongoDB using sustainable coding practices.

Before we start: This guide is aimed towards beginners / intermediate level. So, a bit of python knowledge and object-oriented programming fundamentals are required. Creating an API with a database backend is a great programming project, but it can be tough to get started since there are so many concepts to work with. This guide is meant to help you build your first API using Python Flask-Restful, Auth, and MongoDB, from start to finish. Hopefully, this guide will help take the edge off.

What Is This App about?

Here is a small product requirements document for the app- Doubt-Bag:

Doubt-Bag

This app provides a platform for users to ask doubts, tag those questions to multiple topics and in turn, other users can comment on that question for further clarification and if they think they know the solution, they can answer the question. Further, if someone has doubts regarding the solution, people can post a comment for that solution. Lastly, if people are satisfied or dissatisfied with the solution, They can upvote or downvote the solution. The author of the question if he/she finds the answer satisfactory they can accept the answer.

API Requirements:

  • Signup for new users, Login for current users.
  • Forgot password and reset password implementation.
  • In all the features, for editing/deleting content, We must check if the author is the one who posted or not.
  • View all questions- Public view.
  • Post question with tagged topics and question heading- Login required.
  • View specific questions that will contain the question heading, question body, Tagged topics, comments on those questions, and solutions with votes- Login required.
  • The question can be only deleted which has no solution.
  • Post comment to any question- Login required.
  • If the question is deleted then comment must also be deleted.
  • Post answers to any question- Login required
  • Delete answer- Login required
  • Users can upvote or downvote an answer- Login required
  • If the author of the question finds the answer satisfactory, they only can accept the answer.
  • Post comment to any answer- Login required.
  • If any answer is deleted then comment must also be deleted.

Ok, let’s get started!

Grab the GitHub Doubt-Bag code:

git clone https://github.com/akanuragkumar/Doubt-Bag.git

Project Structure:

Doubt-Bag/                        
├── databases
│ ├── db.py
│ └── models.py
├── resources
│ ├── answer.py
│ ├── auth.py
│ ├── comment.py
│ ├── comment_answer.py
│ ├── errors.py
│ ├── question.py
│ ├── reset_password.py
│ ├── routes.py
│ └── votes.py
├── tests
│ ├── __init__.py
│ ├── Basecase.py
│ ├── test_create_question.py
│ ├── test_get_questions.py
│ ├── test_login.py
│ └── test_signup.py
├── templates/email
│ ├── reset_password.html
│ └── reset+password.txt

├── services
│ └── mail_service.py
├── .env
├── .env.test
├── app.py
├── requirements.txt
└── run.py

Quickstart

To work in a sandboxed Python environment it is recommended to install the app in a Python virtualenv.

  1. Install dependencies
$ cd /path/to/Doubt-Bag
$ pip install -r requirements.txt

2. .env FILE contains environment variable for MongoDB, JWT, and mail- server. Export this file to use this an environment variable for our app.

$ export ENV_FILE_LOCATION=./.env

3. Run the app

Start SMTP server in the terminal with:

$ python -m smtpd -n -c DebuggingServer localhost:1025

This will create an SMTP server for testing our email feature.

Then open another terminal and run:

$ python run.py

View app at http://127.0.0.1:5000/<endpoints>

4. Run the Test-cases

Test Isolation

Test isolation is one of the most important concepts in testing. Usually, when we are writing tests, we test one business logic. The idea of test isolation is that one of your tests should not in any way affect another test.

Suppose that you have created a user in one test and you are testing login on another test. To follow test isolation you cannot depend on the user-created in a user creation test, but should create the user right in the test where you are going to test login. Why? Because your login test might run before your user creation test this makes your test fail.

Also, if we do not delete our user which we created on the previous test run, and we try to run the test again, our test fails because the user is already there.
So, we should always test a feature from an empty state and for that easiest way is to delete all the collections in our database.

Before running our first test make sure to export the environment variable ENV_FILE_LOCATION with the location to the test env file.

$ export ENV_FILE_LOCATION=./.env.test# for running single unit test case
$ python -m unittest tests/test_signup.py
# running all unit test cases together
$ python -m unittest --buffer

Tech-stack used for Doubt bag:

Exception Handling to make flask application more resilient to errors

  • Handle exceptions in our flask application using exception chaining.
  • Send the error message and status code based on the exception that occurred.

Reset Password

Password reset flow diagram

  1. /forget: This endpoint takes the email of the user whose account needs to be changed. This endpoint then sends the email to the user with the link which contains reset token to reset the password.

In the ForgotPassword resource, we first get the user based on the email provided by the client. We are then using create_access_token() to create a token-based on user.id and this token expires in 24 hours. We are then sending the email to the client. The email contains both HTML and text format information.

2. /reset: This endpoint takes reset_token sent in the email and the new password.

In ResetPassword resource, we first get the user based on user id from the reset_token and then reset the password of the user based on the password provided by the user. Finally, a reset success email is sent to the user.

Need for run.py

If we initialize run in app.py and run our app we will encounter:

ImportError: cannot import name 'initialize_routes' from 'resources.routes'

This is because of the circular dependency problem in python. In our reset_password.py, we import send_mail which is importing app from app.py whereas app is not yet defined on our app.py.

Circular dependency

To solve this issue we create another file run.py in our root directory, which will be responsible for running our app. Also, we need to initialize our routes/view functions after we have initialized our app.

Database Schema:

class Question(db.Document):
heading = db.StringField(required=True, unique=True)
topics = db.ListField(db.StringField(), required=True)
question_body = db.StringField(required=True)
added_by = db.ReferenceField('User')
class Comments(db.Document):
comment = db.StringField(required=True)
question = db.ReferenceField('Question', reverse_delete_rule=CASCADE)
added_by = db.ReferenceField('User')
class Answers(db.Document):
answer = db.StringField(required=True)
vote = db.IntField()
is_accepted = db.BooleanField(default=False)
question = db.ReferenceField('Question')
added_by = db.ReferenceField('User')
class AnswerComments(db.Document):
comment = db.StringField(required=True)
answer = db.ReferenceField('Answers', reverse_delete_rule=CASCADE)
added_by = db.ReferenceField('User')
class User(db.Document):
email = db.EmailField(required=True, unique=True)
password = db.StringField(required=True, min_length=6)
questions = db.ListField(db.ReferenceField('Question', reverse_delete_rule=db.PULL))
comments = db.ListField(db.ReferenceField('Comments', reverse_delete_rule=db.PULL))
answers = db.ListField(db.ReferenceField('Answers', reverse_delete_rule=db.PULL))
answers_comment = db.ListField(db.ReferenceField('AnswerComments', reverse_delete_rule=db.PULL))
def hash_password(self):
self.password = generate_password_hash(self.password).decode('utf8')
def check_password(self, password):
return check_password_hash(self.password, password)
User.register_delete_rule(Question, 'added_by', db.CASCADE)
User.register_delete_rule(Comments, 'added_by', db.CASCADE)
User.register_delete_rule(Answers, 'added_by', db.CASCADE)
User.register_delete_rule(AnswerComments, 'added_by', db.CASCADE)

We have created a one-many relationship between user and question, comments, answers, answer_comment. That means a user can have one or more questions and a question can only be created by one user. Here reverse_delete_rule in the question field of User represents that a question should be pulled from the user document if the question is deleted.
Similarly, User.register_delete_rule(Question, 'added_by', db.CASCADE) creates another delete rule which means if a user is deleted then the question created by the user is also deleted.

Also, I have made reverse delete rule — cascade for references from parent to child table like if the question is deleted then comments associated with it would also be deleted.

API end-points:

Auth

  1. POST /api/auth/signup
application/json - [{"email": "akanuragkumar712@gmail.com","password": "itssocool"}]

response

{"Success": "You successfully signed up!","id": <object_id>}

2. POST /api/auth/login

application/json - [{"email": "akanuragkumar712@gmail.com","password": "itssocool"}]

response

{"token": <token>}

3. POST /api/auth/forget

application/json - [{"email": "akanuragkumar712@gmail.com"]

response

a mail will be send with reset password token

4. POST /api/auth/reset

application/json - [{"reset_token": <reset_token>,"password": "thisisalsocool"]

response

new password is set for the account and now the user has to login again.

To use authorization header in Postman follow the steps:
1) Go to the Authorization tab.
2) Select the Bearer Token form TYPE dropdown.
3) Paste the token you got earlier from /login
4) Finally, send the request.

Question

  1. POST /api/question
application/json - [{"topics": ["general","metal ability","awareness"],"question_body": "what is Doubt-bag 3?","heading": "what is Doubt-bag 3?"}]

response

{"Success": "Your question has been posted","id": <object_id>}

2. GET /api/question/<id>

response

{"question": [{"_id": {"$oid": <object_id>}, "heading": "what is Doubt-bag?", "topics": ["general", "metal ability", "awareness"], "question_body": "what is Doubt-bag?", "added_by": {"$oid": <object_id>}}], "comments": [{"_id": {"$oid": <object_id>"}, "comment": "This is a very good question", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>}}, {"_id": {"$oid": <object_id>}, "comment": "This is a very good question-1(duplicate)", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>}}], "answers": [{"_id": {"$oid": <object_id>}, "answer": "This is a store house of all doubts people can have ranging to different topic.", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>}}, {"_id": {"$oid": <object_id>}, "answer": "This is a store house of all doubts people can have ranging to different topic.(duplicate)", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>}}]}

3. PUT /api/question/<id>

application/json - [{"topics": ["science","metal ability","awareness"],"question_body": "what is Doubt-bag 3?","heading": "what is Doubt-bag 3?"}]

response

{"Success": "Your question has been updated"}

4. DELETE /api/question/<id>

response

{"Success": "Your question has been deleted"}

Comments

  1. POST /api/comments?question_id=<id>
application/json - [{"comment" : "This is a very good question-1(duplicate)"}]

response

{"Success": "Your comment has been posted","id": <object_id>}

2. GET /api/comment/<id>

response

[{"comment" : "This is a very good question-1(duplicate)", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>}]

3. PUT /api/comments/<id>

application/json - [{"comment" : "This is a very good question-1"}]

response

{"Success": "Your comment has been updated"}

4. DELETE /api/comments/<id>

response

{"Success": "Your comment has been deleted"}

Answers

  1. POST /api/answers?question_id=<id>
application/json - [{"answer" : "This is a very good question-1(duplicate)"}]

response

{"Success": "Your answer has been posted","id": <object_id>}

2. GET /api/answers/<id>

response

[{"answer" : "This is a very good question-1(duplicate)", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>},{"answer_comment" : "This is a very good question-1","added_by": {"$oid": <object_id>}]

3. PUT /api/answers/<id>

application/json - [{"answer" : "This is a very good question-1"}]

response

{"Success": "Your answer has been updated"}

4. DELETE /api/answer/<id>

response

{"Success": "Your answer has been deleted"}

Answer_comment

  1. POST /api/answer_comment?answer_id=<id>
application/json - [{"answer_comment" : "This is a very good question-1(duplicate)"}]

response

{"Success": "Your comment has been posted","id": <object_id>}

2. GET /api/answer_comment/<id>

response

[{"comment" : "This is a very good question-1(duplicate)", "question": {"$oid": <object_id>}, "added_by": {"$oid": <object_id>}]

3. PUT /api/answer_comment/<id>

application/json - [{"answer_comment" : "This is a very good question-1"}]

response

{"Success": "Your comment has been updated"}

4. DELETE /api/answer_comment/<id>

response

{"Success": "Your comment has been deleted"}

Vote and Accept of Answer

  1. PUT /api/upvote?answer_id=<id>

response

{'Success': 'Answer is upvoted'}

2. PUT /api/downvote?answer_id=<id>

response

{'Success': 'Answer is downvoted'}

3. PUT /api/accepted?answer_id=<id>

response

{'Success': 'Answer is accepted'}

Improvements:

There are lots of scopes where you can try to improve/add the features. Here are a few of them.

  • Only 1 upvote/downvote is allowed per user. Here you will have to store users in Answer collection who have voted the answer and while voting this validation should be there.
  • Store all topics in a separate collection and validate the topics during post request of questions. Also, get requests on topics should be enabled.
  • The search for questions via question body or tagged topics.
  • Store versions of questions and answers.
  • Handling exceptions and all edge cases for failure points in API.
  • Adding unit test cases for both happy as well as unhappy path.

Conclusion:

In this project, we built what was asked from us in the product requirements document. The topics which we have covered are:

  • How to create Document schema using Mongoengine.
  • How to perform CRUD operation using Mongoengine.
  • How to create REST APIs using Flask-restful
  • Python spread operator, which spreads the dictionary. i.e: Question(**body)
  • How to hash user password using flask-bcrypt
  • How to create JSON token using flask-jwt-extended
  • How to protect API endpoints from unauthorized access.
  • How to implement authorization so that only the user who added the content can delete/update the content.
  • How to handle exceptions in our flask application using exception chaining and send the error message and status code based on the exception occurred.
  • How to create a token for resetting user password
  • How to send email to the user using Flask-mail
  • How to reset the user password
  • How to avoid circular dependency in Flask.
  • What test isolation is and why we should isolate our tests cases
  • How to test Flask REST APIs with unittest

Hopefully, you can now set up your own custom REST APIs using Flask-Restful, Auth, and MongoDB. Thanks for going through this article, don’t forget to leave a few claps if you like it 😉.

--

--

anurag kumar

Graduate from The National Institute of Engineering. Currently, I am working as a backend developer in a FinTech firm.