# Getting Started with MongoDB and Starlette

    

Starlette is a lightweight ASGI framework/toolkit, which is ideal for building high-performance asyncio services. It provides everything you need to create JSON APIs, with very little boilerplate. However, if you would prefer an async web framework that is a bit more "batteries included," be sure to read my tutorial on Getting Started with MongoDB and FastAPI.

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

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

``` shell
uvicorn app:app --reload
```

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.

### Connecting to MongoDB

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

``` python
client = motor.motor_asyncio.AsyncIOMotorClient(os.environ"MONGODB_URL"])
db = 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 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
async def create_student(request):
    student = await request.json()
    student["_id"] = str(ObjectId())
    new_student = await db["students"].insert_one(student)
    created_student = await db["students"].find_one({"_id": new_student.inserted_id})
    return JSONResponse(status_code=201, content=created_student)
```

Note how I am converting the `ObjectId` to a string before assigning it as the `_id`. MongoDB stores data as [BSON; Starlette encodes and decodes data as JSON strings. BSON has support for additional non-JSON-native data types, including `ObjectId`, but JSON does not.  Fortunately, MongoDB `_id` values don't need to be ObjectIDs. 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. The `request.json` function converts this JSON string back into a Python dictionary which we can then pass to our MongoDB client.

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 this in our `JSONResponse`.

Starlette returns an HTTP `200` status code by default, but in this instance, a `201` created is more appropriate.

##### Read Routes

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

``` python
async def list_students(request):
    students = await db"students"].find().to_list(1000)
    return JSONResponse(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.

``` python
async def show_student(request):
    id = request.path_params"id"]
    if (student := await db["students"].find_one({"_id": id})) is not None:
        return JSONResponse(student)

    raise HTTPException(status_code=404, detail=f"Student {id} not found")
```

The student detail route has a path parameter of `id`, which Starlette 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 an `HTTPException` with a status of `404`.

##### Update Route

``` python
async def update_student(request):
    id = request.path_params"id"]
    student = await request.json()
    update_result = await db["students"].update_one({"_id": id}, {"$set": student})

    if update_result.modified_count == 1:
        if (updated_student := await db["students"].find_one({"_id": id})) is not None:
            return JSONResponse(updated_student)

    if (existing_student := await db["students"].find_one({"_id": id})) is not None:
        return JSONResponse(existing_student)

    raise HTTPException(status_code=404, detail=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, 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.

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

##### Delete Route

``` python
async def delete_student(request):
    id = request.path_params["id"]
    delete_result = await db["students"].delete_one({"_id": id})

    if delete_result.deleted_count == 1:
        return JSONResponse(status_code=204)

    raise HTTPException(status_code=404, detail=f"Student {id} not found")
```

Our last 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`.

### Creating the Starlette App

``` python
app = Starlette(
    debug=True,
    routes=[
        Route("/", create_student, methods=["POST"]),
        Route("/", list_students, methods=["GET"]),
        Route("/{id}", show_student, methods=["GET"]),
        Route("/{id}", update_student, methods=["PUT"]),
        Route("/{id}", delete_student, methods=["DELETE"]),
    ],
)
```

The final piece of code creates an instance of Starlette and includes each of the routes we defined. You can see that many of the routes share the same URL but use different HTTP methods. For example, a `GET` request to `/{id}` will return the corresponding student document for you to view, whereas a `DELETE` request to the same URL will delete it.  So, be very thoughtful about the which HTTP method you use for each request!

## Wrapping Up

I hope you have found this introduction to Starlette 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 learn more and take your MongoDB and Starlette knowledge to the next level, check out Ado's very in-depth tutorial on how to [Build a Property Booking Website with Starlette, MongoDB, and Twilio.  Also, if you're interested in FastAPI (a web framework built upon Starlette), you should view my tutorial on getting started with the FARM stack: FastAPI, React, & MongoDB.

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