HomeLearnQuickstartGetting Started with MongoDB and FastAPI

Getting Started with MongoDB and FastAPI

Published: Mar 16, 2021

  • Atlas
  • MongoDB
  • Python
  • ...

By Aaron Bassett

Rate this article
Python badge

FastAPI is a modern, high-performance, easy-to-learn, fast-to-code, production-ready, Python 3.6+ framework for building APIs based on standard Python type hints. While it might not be as established as some other Python frameworks such as Django, it is already in production at companies such as Uber, Netflix, and Microsoft.

FastAPI is async, and as its name implies, it is super fast; so, MongoDB is the perfect accompaniment. In this quick start, we will create a CRUD (Create, Read, Update, Delete) app showing how you can integrate MongoDB with your FastAPI 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-fastapi.git

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

1uvicorn app:app --reload
Screenshot of terminal running FastAPI

Once the application has started, you can view it in your browser at http://127.0.0.1:8000/docs.

Screenshot of browser and swagger UI

Once you have had a chance to try the example, come back and we will walk through the code.

#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_asyncio.AsyncIOMotorClient(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.

#The _id Attribute and ObjectIds

1class PyObjectId(ObjectId):
2 @classmethod
3 def __get_validators__(cls):
4 yield cls.validate
5
6 @classmethod
7 def validate(cls, v):
8 if not ObjectId.is_valid(v):
9 raise ValueError("Invalid objectid")
10 return ObjectId(v)
11
12 @classmethod
13 def __modify_schema__(cls, field_schema):
14 field_schema.update(type="string")

MongoDB stores data as BSON. FastAPI encodes and decodes data as JSON strings. BSON has support for additional non-JSON-native data types, including ObjectId which can't be directly encoded as JSON. Because of this, we convert ObjectIds to strings before storing them as the _id.

#Database Models

Many people think of MongoDB as being schema-less, which is wrong. MongoDB has a flexible schema. That is to say that collections do not enforce document structure by default, so you have the flexibility to make whatever data-modelling choices best match your application and its performance requirements. So, it's not unusual to create models when working with a MongoDB database. Our application has two models, the StudentModel and the UpdateStudentModel.

1class StudentModel(BaseModel):
2 id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
3 name: str = Field(...)
4 email: EmailStr = Field(...)
5 course: str = Field(...)
6 gpa: float = Field(..., le=4.0)
7
8 class Config:
9 allow_population_by_field_name = True
10 arbitrary_types_allowed = True
11 json_encoders = {ObjectId: str}
12 schema_extra = {
13 "example": {
14 "name": "Jane Doe",
15 "email": "jdoe@example.com",
16 "course": "Experiments, Science, and Fashion in Nanophotonics",
17 "gpa": "3.0",
18 }
19 }

This is the primary model we use as the response model for the majority of our endpoints.

I want to draw attention to the id field on this model. MongoDB uses _id, but in Python, underscores at the start of attributes have special meaning. If you have an attribute on your model that starts with an underscore, pydantic—the data validation framework used by FastAPI—will assume that it is a private variable, meaning you will not be able to assign it a value! To get around this, we name the field id but give it an alias of _id. You also need to set allow_population_by_field_name to True in the model's Config class.

We set this id value automatically to an ObjectId string, so you do not need to supply it when creating a new student.

1class UpdateStudentModel(BaseModel):
2 name: Optional[str]
3 email: Optional[EmailStr]
4 course: Optional[str]
5 gpa: Optional[float]
6
7 class Config:
8 arbitrary_types_allowed = True
9 json_encoders = {ObjectId: str}
10 schema_extra = {
11 "example": {
12 "name": "Jane Doe",
13 "email": "jdoe@example.com",
14 "course": "Experiments, Science, and Fashion in Nanophotonics",
15 "gpa": "3.0",
16 }
17 }

The UpdateStudentModel has two key differences from the StudentModel:

  • It does not have an id attribute as this should never change.
  • All fields are optional, so you only need to supply the fields you wish to update.

#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

1@app.post("/", response_description="Add new student", response_model=StudentModel)
2async def create_student(student: StudentModel = Body(...)):
3 student = jsonable_encoder(student)
4 new_student = await db["students"].insert_one(student)
5 created_student = await db["students"].find_one({"_id": new_student.inserted_id})
6 return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_student)

The create_student route receives the new student data as a JSON string in a POST request. We have to decode this JSON request body into a Python dictionary before passing it 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.

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

1@app.get( "/", response_description="List all students", response_model=List[StudentModel] )
2async def list_students():
3 students = await db["students"].find().to_list(1000)
4 return 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.

1@app.get( "/{id}", response_description="Get a single student", response_model=StudentModel )
2async def show_student(id: str):
3 if (student := await db["students"].find_one({"_id": id})) is not None:
4 return student
5
6 raise HTTPException(status_code=404, detail=f"Student {id} not found")

The student detail route has a path parameter of id, which FastAPI 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
1@app.put("/{id}", response_description="Update a student", response_model=StudentModel)
2async def update_student(id: str, student: UpdateStudentModel = Body(...)):
3 student = {k: v for k, v in student.dict().items() if v is not None}
4
5 if len(student) >= 1:
6 update_result = await db["students"].update_one({"_id": id}, {"$set": student})
7
8 if update_result.modified_count == 1:
9 if (
10 updated_student := await db["students"].find_one({"_id": id})
11 ) is not None:
12 return updated_student
13
14 if (existing_student := await db["students"].find_one({"_id": id})) is not None:
15 return existing_student
16
17 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 don't want to update any fields with empty values; so, first of all, we iterate over all the items in the received dictionary and only add the items that have a value to our new document.

If, after we remove the empty values, there are no fields left to update, we instead look for an existing record that matches the id and return that unaltered. However, if there are values to update, we use update_one to $set the new values, and then return the updated document.

But if we get to the end of the function and we have not been able to find a matching document to update or return, then we raise a 404 error again.

#Delete Route
1@app.delete("/{id}", response_description="Delete a student")
2async def delete_student(id: str):
3 delete_result = await db["students"].delete_one({"_id": id})
4
5 if delete_result.deleted_count == 1:
6 return JSONResponse(status_code=status.HTTP_204_NO_CONTENT)
7
8 raise HTTPException(status_code=404, detail=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 FastAPI with MongoDB useful. If you would like to learn more, check out my post introducing the FARM stack (FastAPI, React and MongoDB) as well as the FastAPI documentation and this awesome list.

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
MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

© MongoDB, Inc.