HomeLearnQuickstartGetting Started with MongoDB and Tornado

Getting Started with MongoDB and Tornado

Published: Mar 17, 2021

  • Atlas
  • MongoDB
  • Python

By Aaron Bassett

Rate this article
Python badge

Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. Because Tornado uses non-blocking network I/O, it is ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user.

Tornado also makes it very easy to create JSON APIs, which is how we're going to be using it in this example. Motor, the Python async driver for MongoDB, comes with built-in support for Tornado, making it as simple as possible to use MongoDB in Tornado regardless of the type of server you are building.

In this quick start, we will create a CRUD (Create, Read, Update, Delete) app showing how you can integrate MongoDB with your Tornado projects.

#Prerequisites

  • Python 3.9.0
  • A MongoDB Atlas cluster. Follow the "Get Started with Atlas" guide to create your account and MongoDB cluster. Keep a note of your username, password, and connection string as you will need those later.

#Running the Example

To begin, you should clone the example code from GitHub.

1git clone git@github.com:mongodb-developer/mongodb-with-tornado.git

You will need to install a few dependencies: Tornado, Motor, etc. I always recommend that you install all Python dependencies in a virtualenv for the project. Before running pip, ensure your virtualenv is active.

1cd mongodb-with-tornado
2pip install -r requirements.txt

It may take a few moments to download and install your dependencies. This is normal, especially if you have not installed a particular package before.

Once you have installed the dependencies, you need to create an environment variable for your MongoDB connection string.

1export DB_URL="mongodb+srv://<username>:<password>@<url>/<db>?retryWrites=true&w=majority"

Remember, anytime you start a new terminal session, you will need to set this environment variable again. I use direnv to make this process easier.

The final step is to start your Tornado server.

1python app.py

Tornado does not output anything in the terminal when it starts, so as long as you don't have any error messages, your server should be running.

Once the application has started, you can view it in your browser at http://127.0.0.1:8000/. There won't be much to see at the moment as you do not have any data! We'll look at each of the end-points a little later in the tutorial, but if you would like to create some data now to test, you need to send a POST request with a JSON body to the local URL.

1curl -X "POST" "http://localhost:8000/" \
2 -H 'Accept: application/json' \
3 -H 'Content-Type: application/json; charset=utf-8' \
4 -d $'{
5 "name": "Jane Doe",
6 "email": "jdoe@example.com",
7 "gpa": "3.9"
8 }'

Try creating a few students via these POST requests, and then refresh your browser.

#Creating the Application

All the code for the example application is within app.py. I'll break it down into sections and walk through what each is doing.

#Connecting to MongoDB

One of the very first things we do is connect to our MongoDB database.

1client = motor.motor_tornado.MotorClient(os.environ["MONGODB_URL"])
2db = client.college

We're using the async motor driver to create our MongoDB client, and then we specify our database name college.

#Application Routes

Our application has four routes:

  • POST / - creates a new student.
  • GET / - view a list of all students or a single student.
  • PUT /{id} - update a student.
  • DELETE /{id} - delete a student.

Each of the routes corresponds to a method on the MainHandler class. Here is what that class looks like if we only show the method stubs:

1class MainHandler(tornado.web.RequestHandler):
2
3 async def get(self, **kwargs):
4 pass
5
6 async def post(self):
7 pass
8
9 async def put(self, **kwargs):
10 pass
11
12 async def delete(self, **kwargs):
13 pass

As you can see, the method names correspond to the different HTTP methods. Let's walk through each method in turn.

#POST - Create Student

1async def post(self):
2 student = tornado.escape.json_decode(self.request.body)
3 student["_id"] = str(ObjectId())
4
5 new_student = await self.settings["db"]["students"].insert_one(student)
6 created_student = await self.settings["db"]["students"].find_one(
7 {"_id": new_student.inserted_id}
8 )
9
10 self.set_status(201)
11 return self.write(created_student)

Note how I am converting the ObjectId to a string before assigning it as the _id. MongoDB stores data as BSON, but we're encoding and decoding our data from JSON strings. BSON has support for additional non-JSON-native data types, including ObjectId, but JSON does not. Because of this, for simplicity, we convert ObjectIds to strings before storing them.

The route receives the new student data as a JSON string in the body of the POST request. We decode this string back into a Python object before passing it to our MongoDB client. Our client is available within the settings dictionary because we pass it to Tornado when we create the app. You can see this towards the end of the app.py.

1app = tornado.web.Application(
2 [
3 (r"/", MainHandler),
4 (r"/(?P<student_id>\w+)", MainHandler),
5 ],
6 db=db,
7)

The insert_one method response includes the _id of the newly created student. After we insert the student into our collection, we use the inserted_id to find the correct document and write it to our response. By default, Tornado will return an HTTP 200 status code, but in this instance, a 201 created is more appropriate, so we change the HTTP response status code with set_status.

#GET - View Student Data

We have two different ways we may wish to view student data: either as a list of all students or a single student document. The get method handles both of these functions.

1async def get(self, student_id=None):
2 if student_id is not None:
3 if (
4 student := await self.settings["db"]["students"].find_one(
5 {"_id": student_id}
6 )
7 ) is not None:
8 return self.write(student)
9 else:
10 raise tornado.web.HTTPError(404)
11 else:
12 students = await self.settings["db"]["students"].find().to_list(1000)
13 return self.write({"students": students})

First, we check to see if the URL provided a path parameter of student_id. If it does, then we know that we are looking for a specific student document. We look up the corresponding student with find_one and the specified student_id. If we manage to locate a matching record, then it is written to the response as a JSON string. Otherwise, we raise a 404 not found error.

If the URL does not contain a student_id, then we return a list of all students.

Motor's to_list method requires a max document count argument. For this example, I have hardcoded it to 1000; but in a real application, you would use the skip and limit parameters in find to paginate your results.

It's worth noting that as a defence against JSON hijacking, Tornado will not allow you to return an array as the root element. Most modern browsers have patched this vulnerability, but Tornado still errs on the side of caution. So, we must wrap the students array in a dictionary before we write it to our response.

#PUT - Update Student
1async def put(self, student_id):
2 student = tornado.escape.json_decode(self.request.body)
3 await self.settings["db"]["students"].update_one(
4 {"_id": student_id}, {"$set": student}
5 )
6
7 if (
8 updated_student := await self.settings["db"]["students"].find_one(
9 {"_id": student_id}
10 )
11 ) is not None:
12 return self.write(updated_student)
13
14 raise tornado.web.HTTPError(404)

The update route is like a combination of the create student and the student detail routes. It receives the id of the document to update student_id as well as the new data in the JSON body.

We attempt to $set the new values in the correct document with update_one, and then check to see if it correctly modified a single document. If it did, then we find that document that was just updated and return it.

If the modified_count is not equal to one, we still check to see if there is a document matching the id. A modified_count of zero could mean that there is no document with that id, but it could also mean that the document does exist, but it did not require updating because the current values are the same as those supplied in the PUT request.

Only after that final find fails, we raise a 404 Not Found exception.

#DELETE - Remove Student
1async def delete(self, student_id):
2 delete_result = await db["students"].delete_one({"_id": student_id})
3
4 if delete_result.deleted_count == 1:
5 self.set_status(204)
6 return self.finish()
7
8 raise tornado.web.HTTPError(404)

Our final route is delete. Again, because this is acting upon a single document, we have to supply an id, student_id in the URL. If we find a matching document and successfully delete it, then we return an HTTP status of 204 or No Content. In this case, we do not return a document as we've already deleted it! However, if we cannot find a student with the specified student_id, then instead, we return a 404.

#Wrapping Up

I hope you have found this introduction to Tornado with MongoDB useful. Now is a fascinating time for Python developers as more and more frameworks—both new and old—begin taking advantage of async.

If you would like to know more about how you can use MongoDB with Tornado and WebSockets, please read my other tutorial, Subscribe to MongoDB Change Streams Via WebSockets.

If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.

Rate this article

More from this series

Python Web Tutorials
  • Getting Started with MongoDB and FastAPI
  • Getting Started with MongoDB and Sanic
  • Getting Started with MongoDB and Starlette
  • Getting Started with MongoDB and Tornado
© 2021 MongoDB, Inc.

About

  • Careers
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2021 MongoDB, Inc.