# Getting Started with MongoDB and Sanic

    

Sanic is a Python 3.6+ async web server and web framework that's written to go fast. The project's goal is to provide a simple way to get up and running a highly performant HTTP server that is easy to build, to expand, and ultimately to scale.

Unfortunately, because of its name and dubious choices in ASCII art, Sanic wasn't seen by some as a serious framework, but it has matured. It is worth considering if you need a fast, async, Python framework.

In this quick start, we will create a CRUD (Create, Read, Update, Delete) app showing how you can integrate MongoDB with your Sanic 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.

``` shell
git clone git@github.com:mongodb-developer/mongodb-with-sanic.git
```

You will need to install a few dependencies: Sanic, 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.

``` shell
cd mongodb-with-sanic
pip 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.

``` shell
export MONGODB_URL="mongodb+srv://:@/?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 Sanic server.

``` shell
python app.py
```

Once the application has started, you can view it in your browser at . 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.

``` shell
curl -X "POST" "http://localhost:8000/" \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json; charset=utf-8' \
    -d '{
        "name": "Jane Doe",
        "email": "jdoe@example.com",
        "gpa": "3.9"
    }'
```

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.

### Setting Up Our App and MongoDB Connection

We're going to use the sanic-motor package to wrap our motor client for ease of use. So, we need to provide a couple of settings when creating our Sanic app.

``` python
app = Sanic(__name__)

settings = dict(
    MOTOR_URI=os.environ"MONGODB_URL"],
    LOGO=None,
)
app.config.update(settings)

BaseModel.init_app(app)

class Student(BaseModel):
    __coll__ = "students"
```

Sanic-motor's models are unlikely to be very similar to any other database models you have used before. They do not describe the schema, for example. Instead, we only specify the collection name.

### Application Routes

Our application has five routes:

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

#### Create Student Route

``` python
@app.route("/", methods=["POST"])
async def create_student(request):
    student = request.json
    student["_id"] = str(ObjectId())

    new_student = await Student.insert_one(student)
    created_student = await Student.find_one(
        {"_id": new_student.inserted_id}, as_raw=True
    )

    return json_response(created_student)
```

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

The `create_student` route receives the new student data as a JSON string in a `POST` request. Sanic will automatically convert this JSON string back into a Python dictionary which we can then pass to the sanic-motor wrapper.

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 return it in the `json_response`.

sanic-motor returns the relevant model objects from any `find` method, including `find_one`. To override this behaviour, we specify `as_raw=True`.

##### Read Routes

The application has two read routes: one for viewing all students, and the other for viewing an individual student.

``` python
@app.route("/", methods="GET"])
async def list_students(request):
    students = await Student.find(as_raw=True)
    return json_response(students.objects)
```

In our example code, we are not placing any limits on the number of students returned. In a real application, you should use sanic-motor's `page` and `per_page` arguments to paginate the number of students returned.

``` python
@app.route("/", methods=["GET"])
async def show_student(request, id):
    if (student := await Student.find_one({"_id": id}, as_raw=True)) is not None:
        return json_response(student)

    raise NotFound(f"Student {id} not found")
```

The student detail route has a path parameter of `id`, which Sanic passes as an argument to the `show_student` function. We use the `id` to attempt to find the corresponding student in the database. The conditional in this section is using an [assignment expression, a recent addition to Python (introduced in version 3.8) and often referred to by the incredibly cute sobriquet "walrus operator."

If a document with the specified `id` does not exist, we raise a `NotFound` exception which will respond to the request with a `404` response.

##### Update Route

``` python
@app.route("/", methods="PUT"])
async def update_student(request, id):
    student = request.json
    update_result = await Student.update_one({"_id": id}, {"$set": student})

    if update_result.modified_count == 1:
        if (
            updated_student := await Student.find_one({"_id": id}, as_raw=True)
        ) is not None:
            return json_response(updated_student)

    if (
        existing_student := await Student.find_one({"_id": id}, as_raw=True)
    ) is not None:
        return json_response(existing_student)

    raise NotFound(f"Student {id} not found")
```

The `update_student` route is like a combination of the `create_student` and the `show_student` routes. It receives the `id` of the document to update 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`. It could also mean that the document does exist but it did not require updating as the current values are the same as those supplied in the `PUT` request.

It is only after that final `find` fail when we raise a `404` Not Found exception.

##### Delete Route

``` python
@app.route("/", methods=["DELETE"])
async def delete_student(request, id):
    delete_result = await Student.delete_one({"_id": id})

    if delete_result.deleted_count == 1:
        return json_response({}, status=204)

    raise NotFound(f"Student {id} not found")
```

Our final route is `delete_student`. Again, because this is acting upon a single document, we have to supply an `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 id, then instead, we return a `404`.

## Wrapping Up

I hope you have found this introduction to Sanic with MongoDB useful. If you would like to find out [more about Sanic, please see their documentation. Unfortunately, documentation for sanic-motor is entirely lacking at this time. But, it is a relatively thin wrapper around the MongoDB Motor driver—which is well documented—so do not let that discourage you.

To see how you can integrate MongoDB with other async frameworks, check out some of the other Python posts on the MongoDB developer portal.

>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.