loading...
Cover image for Flask Rest API -Part:1- Using MongoDB with Flask

Flask MongoDB Flask Rest API -Part:1- Using MongoDB with Flask

paurakhsharma profile image Paurakh Sharma Humagain Updated on ・7 min read

Part 1: Using MongoDB with Flask

Howdy! In the last Part of the series, we learned how to create a basic CRUD REST API functionality using python list. But that's not how the real-world applications are built, because if your server is restarted or god forbids crashes then you are gonna lose all the information stored in your server. To solve those problems (and many others) database is used. So, that's what we are gonna do. We are going to use MongoDB as our database.

If you are just starting from this part, you can find all the code we wrote till now here.

Before we start make sure you have installed MongoDB in your system. If you haven't already you can install for Linux, Windown and macOS.

There are mainly to popular libraries which makes working with MongoDB easier:

1) Pymongo is a low-level Python wrapper around MongoDB, working with Pymongo is similar to writing the MongoDB query directly.
Here is the simple example of updating the name of a movie whose id matches the given id using Pymongo.

db['movies'].update({'_id': id},
                    {'$set': {'name': 'My new title'}})

Pymongo doesn't use any predefined schema so it can make full use of Schemaless nature of MongoDB.

2) MongoEngine is an Object-Document Mapper, which uses a document schema that makes working with MongoDB clear and easier.
Here is the same example using mongoengine.

Movies.objects(id=id).update(name='My new title')

Mongoengine uses predefined schema for the fields in the database which restricts it from using the Schemaless nature of MongoDB.

As we can see both sides have their advantages and disadvantages. So, choose the one that fits your project well. In this series we are going to learn about Mongoengine, please do let me know in the comment section below if you want me to cover Pymongo as well.

To work better with Mongoengine in our Flask application there is a great Flask extension called Flask-Mongengine.

So, let's get started by installing flask-mongoengine.

pipenv install flask-mongoengine

Note: Since flask-mongoengine is built on top of mongoengine it gets installed automatically while installing flask-mongoengine, also mongoengine is build on top of pymongo so, it also gets installed

Now, let's create a new folder inside movie-bag. I am gonna call it database. Inside database folder create a file named db.py. Also, create another file and name it models.py

Let's see how files/folder looks like now.

movie-bag
│   app.py
|   Pipfile
|   Pipfile.lock   
└───database
    │   db.py
    └───models.py

Now, let's dive into the interesting part.
First of all, let's initialize our database by adding the following code to our db.py

#~movie-bag/database/db.py

from flask_mongoengine import MongoEngine

db = MongoEngine()

def initialize_db(app):
    db.init_app(app)

Here we have imported MongoEngine and created the db object and we have defined a function initialize_db() which we are gonna call from our app.py to initialize the database.

Let's write the following code in our movie.py inside models directory

#~movie-bag/database/models.py
from .db import db

class Movie(db.Document):
    name = db.StringField(required=True, unique=True)
    casts = db.ListField(db.StringField(), required=True)
    genres = db.ListField(db.StringField(), required=True)

What we just created is a document for our database. So, that the users cannot add other fields then what are defined here.
Here we can see the Movie document has three fields:
1) name: is a field of type String, we also have two constraints in this field.
- required which means the user cannot create a new movie without giving its title.
- unique which means the movie name must be unique and cannot be repeated.

2) casts: is a field of type list which contains the values of type String

3) genres: same as casts

Finally, we can initialize the database in our app.py and change our view functions (functions handling our API request) to use the Movie document we defined earlier.

#~movie-bag/app.py

-from flask import Flask, jsonify, request
+from flask import Flask, request, Response
+from database.db import initialize_db
+from database.models import Movie

 app = Flask(__name__)

-movies = [
-    {
-        "name": "The Shawshank Redemption",
-        "casts": ["Tim Robbins", "Morgan Freeman", "Bob Gunton", "William Sadler"],
-        "genres": ["Drama"]
-    },
-    {
-       "name": "The Godfather ",
-       "casts": ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"],
-       "genres": ["Crime", "Drama"]
-    }
-]
+app.config['MONGODB_SETTINGS'] = {
+    'host': 'mongodb://localhost/movie-bag'
+}
+
+initialize_db(app)

-@app.route('/movies')
-def hello():
-    return jsonify(movies)

+@app.route('/movies')
+def get_movies():
+    movies = Movie.objects().to_json()
+    return Response(movies, mimetype="application/json", status=200)

-@app.route('/movies', methods=['POST'])
-def add_movie():
-    movie = request.get_json()
-    movies.append(movie)
-    return {'id': len(movies)}, 200

+@app.route('/movies', methods=['POST'])
+    body = request.get_json()
+    movie = Movie(**body).save()
+    id = movie.id
+    return {'id': str(id)}, 200

-@app.route('/movies/<int:index>', methods=['PUT'])
-def update_movie(index):
-    movie = request.get_json()
-    movies[index] = movie
-    return jsonify(movies[index]), 200

+@app.route('/movies/<id>', methods=['PUT'])
+def update_movie(id):
+    body = request.get_json()
+    Movie.objects.get(id=id).update(**body)
+    return '', 200

-@app.route('/movies/<int:index>', methods=['DELETE'])
-def delete_movie(index):
-    movies.pop(index)
-    return 'None', 200

+@app.route('/movies/<id>', methods=['DELETE'])
+def delete_movie(id):
+    Movie.objects.get(id=id).delete()
+    return '', 200

 app.run()

Wow! that a lot of changes, let's go step by step with the changes.

-from flask import Flask, jsonify, request
+from flask import Flask, request, Response
+from database.db import initialize_db
+from database.models.movie import Movie

Here we removed jsonify as we no longer need and added Response which we use to set the type of response. Then we import initialize_db form db.py which we defined earlier to initialize our database. And lastly, we imported the Movie document form movie.py

+app.config['MONGODB_SETTINGS'] = {
+    'host': 'mongodb://localhost/movie-bag'
+}
+
+db = initialize_db(app)

Here we set the configuration for our mongodb database. Here the host is in the format <host-url>/<database-name>. Since we have installed mongodb locally so we can access it from mongodb://localhost/ and we are gonna name our database movie-bag.
And at the last, we initialize our database.

+@app.route('/movies')
+def get_movies():
+    movies = Movie.objects().to_json()
+    return Response(movies, mimetype="application/json", status=200)
+

Here we get all the objects from Movie document using Movies.objects() and convert them to JSON using to_json(). At last, we return a Response object, where we defined our response type to application/json.

+@app.route('/movies', methods=['POST'])
+    body = request.get_json()
+    movie = Movie(**body).save()
+    id = movie.id
+    return {'id': str(id)}, 200

In the POST request we first get the JSON that we send and a request. And then we load the Movie document with the fields from our request with Movie(**body). Here ** is called the spread operator which is written as ... in JavaScript (if you are familiar with it.). What it does is like the name suggests, spreads the dict object.

So, that Movie(**body) becomes

Movie(name="Name of the movie",
    casts=["a caste"],
    genres=["a genre"])

At last, we save the document and get its id which we return as a response.

+@app.route('/movies/<id>', methods=['PUT'])
+def update_movie(id):
+    body = request.get_json()
+    Movie.objects.get(id=id).update(**body)
+    return '', 200

Here we first find the Movie document matching the id sent in the request and then update it. Here also we have applied the spread operator to pass the values to the update() function.

+@app.route('/movies/<id>', methods=['DELETE'])
+def delete_movie(id):
+    Movie.objects.get(id=id).delete()
+    return '', 200

Similar to the update_movie() here we get the Movie document matching given id and delete it from the database.

Oh, I just remembered that we haven't added the API endpoint to GET only one document from our server.
Let's add it:
Add the following code right above app.run()

@app.route('/movies/<id>')
def get_movie(id):
    movies = Movie.objects.get(id=id).to_json()
    return Response(movies, mimetype="application/json", status=200)

Now you can get the single movie from API endpoint /movies/<valid_id>.

To run the server make sure you are at movie-bag directory.

Then run

pipenv shell
python app.py

To activate the virtual environment in your terminal and start the server.

Wow! Congratulations on making this far. To test the APIs, use Postman as we used in the previous part of this series.

You might have noticed that if we send invalid data to our endpoint e.g: without a name, or other fields we get an unfriendly error in the form of HTML. If we try to get the movie document with id that doesn't exist in the database then also we get an unfriendly error in the form of HTML response. Which is not an excepted behavior of a nicely build API. We will learn how we can handle such errors in the later parts of the series.

What we learned from this part of the series?

  • Difference between Pymongo and Mongoengine.
  • How to create Document schema using Mongoengine.
  • How to perform CRUD operation using Mongoengine.
  • Python spread operator.

You can find the complete code of this part here

In the next part, we are going to learn how to better structure your flask application using Blueprint. And also how to create REST APIs faster, following best practices with the minimal setup using flask-restful

Until then happy coding 😊

Discussion

pic
Editor guide
Collapse
ikhrome profile image
Ivan Khromov

Helpful note to everyone, who wants MongoEngine to serialize JSON like this:

{
    "id": "5f0b54728a42acf154e2082d",
    // the rest of document
}

not this:

{
    "_id": {
        "$oid": "5f0b54728a42acf154e2082d"
    }
    // ... the rest of document
}

Install MongoEngine GoodJSON library and extend Movie class like this:

from .db import db
import mongoengine_goodjson as gj

class Movie(gj.Document):
#the rest of code here ...
Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

Wow, that's cool. Thank you.
I was doing manual normalization, but this looks awesome.

Collapse
therealibukun profile image
the-real-ibukun

Really helpful,
Some section of the code is omitted here, saved by the complete code on github though.
Thanks again.

Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

Thank you so much ❤️
Can you point me what is missing so that I can add it here.

Collapse
therealibukun profile image
the-real-ibukun

Missing function definition add_movie()

@app.route('/movies', methods=['POST'])
body = request.get_json()
movie = Movie(**body).save()
id = movie.id
return {'id': str(id)}, 200

.movie not required

from database.models.movie import Movie

Apart from that it's ready to run 😃

Collapse
engmsilva profile image
Marcelo Ribeiro da Silva

Great article,

I tried to make some changes to the parameters of the fields in the document, but it doesn't seem to have any effect

I removed the "required" parameter from the fields and there was no effect. How can changes be made to an existing document?

Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

It's probably because when you give constrains to the field like required, mongodb creates an index for the field. So, to make the change take place you either have to delete the index or easier thing will be to delete the database (don't do that in production 😉)

Please let me know if deleting the index or the database solves your issue.

Collapse
engmsilva profile image
Marcelo Ribeiro da Silva

I had already tried to delete the bank and the index, but I was looking for an alternative to a document that cannot be deleted.

Collapse
boredomdenied profile image
boredomdenied

I'm getting this error:

TypeError: TopLevelDocumentMetaclass object argument after ** must be a mapping, not NoneType

update

has been resolved by adding force=True

body = request.get_json(force=True)
Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

Awesome 👍 Glad that you got it to work. I am not sure how you got this problem. Maybe something to do with your request body?

Collapse
keshavadk profile image
keshavadk

Thank you for this clear explanation.

Can you make the "Flask REST API using Pymongo"?

Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

Using pymongo is almost similar to Mongoengine, mongoengine only makes it easier to do stuffs....
I will try to make one article to show how to use Flask with Pymongo.

Collapse
keshavadk profile image
keshavadk

Ok. Thank you.

Collapse
rohansawant profile image
Rohan Sawant

Nice!

Embedding git diffs, into blog posts? Hmm, that's something I had never thought of before.

I am borrowing that some time!

🤗

Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

Yeah, I thought it would make following the code changes easier 😃

Collapse
mishra_amrish profile image
Amrish Mishra

I wish to have configuration structure using PyMongo

Collapse
thebadcoder profile image
TheBadCoder

Can you please explain-
initialize_db(app)

Collapse
paurakhsharma profile image
Paurakh Sharma Humagain Author

initialize_db() is a function that we created in movie-bag/database/db.py

Which starts the database connection :)