8 Best Practices for Building FastAPI and MongoDB Applications
Rate this article
FastAPI is a modern, high-performance web framework for building APIs with Python 3.8 or later, based on type hints. Its design focuses on quick coding and error reduction, thanks to automatic data model validation and less boilerplate code. FastAPI’s support for asynchronous programming ensures APIs are efficient and scalable, while built-in documentation features like Swagger UI and ReDoc provide interactive API exploration tools.
FastAPI seamlessly integrates with MongoDB through the Motor library, enabling asynchronous database interactions. This combination supports scalable applications by enhancing both the speed and flexibility of data handling with MongoDB. FastAPI and MongoDB together are ideal for creating applications that manage potentially large amounts of complex and diverse data efficiently. MongoDB is a proud sponsor of the FastAPI project, so you can tell it's a great choice for building applications with MongoDB.
All the techniques described in this article are available on GitHub — check out the source code! With that out of the way, now we can begin…
FastAPI is particularly suitable for building RESTful APIs, where requests for data and updates to the database are made using HTTP requests, usually with JSON payloads. But the framework is equally excellent as a back end for HTML websites or even full single-page applications (SPAs) where the majority of requests are made via JavaScript. (We call this the FARM stack — FastAPI, React, MongoDB — but you can swap in any front-end component framework that you like.) It's particularly flexible with regard to both the database back-end and the template language used to render HTML.
There are actually two Python drivers for MongoDB — PyMongo and Motor — but only one of them is suitable for use with FastAPI. Because FastAPI is built on top of ASGI and asyncio, you need to use Motor, which is compatible with asyncio. PyMongo is only for synchronous applications. Fortunately, just like PyMongo, Motor is developed and fully supported by MongoDB, so you can rely on it in production, just like you would with PyMongo.
You can install it by running the following command in your terminal (I recommend configuring a Python virtual environment first!):
1 pip install motor[srv]
The
srv
extra includes some extra dependencies that are necessary for connecting with MongoDB Atlas connection strings.Once installed, you'll need to use the
AsyncIOMotorClient
in the motor.motor_asyncio
package.1 from fastapi import FastAPI 2 from motor.motor_asyncio import AsyncIOMotorClient 3 4 app = FastAPI() 5 6 # Load the MongoDB connection string from the environment variable MONGODB_URI 7 CONNECTION_STRING = os.environ['MONGODB_URI'] 8 9 # Create a MongoDB client 10 client = AsyncIOMotorClient(CONNECTION_STRING)
Note that the connection string is not stored in the code! Which leads me to…
It's very easy to accidentally commit secret credentials in your code and push them to relatively insecure places like shared Git repositories. I recommend making it a habit to never put any secret in your code.
When working on code, I keep my secrets in a file called
.envrc
— the contents get loaded into environment variables by a tool called direnv. Other tools for keeping sensitive credentials out of your code include envdir, a library like python-dotenv, and there are various tools like Honcho and Foreman. You should use whichever tool makes the most sense to you. Whether the file that keeps your secrets is called .env
or .envrc
or something else, you should add that filename to your global gitignore file so that it never gets added to any repository.In production, you should use a KMS (key management system) such as Vault, or perhaps the cloud-native KMS of whichever cloud you may be using to host your application. Some people even use a KMS to manage their secrets in development.
Although I initialized my database connection in the code above at the top level of a small FastAPI application, it's better practice to gracefully initialize and close your client connection by responding to startup and shutdown events in your FastAPI application. You should also attach your client to FastAPI's app object to make it available to your path operation functions wherever they are in your codebase. (Other frameworks sometimes refer to these as “routes” or “endpoints.” FastAPI calls them “path operations.”) If you rely on a global variable instead, you need to worry about importing it everywhere it's needed, which can be messy.
The snippet of code below shows how to respond to your application starting up and shutting down, and how to handle the client in response to each of these events:
1 from contextlib import asynccontextmanager 2 from logging import info @asynccontextmanager 3 async def db_lifespan(app: FastAPI): 4 # Startup 5 app.mongodb_client = AsyncIOMotorClient(CONNECTION_STRING) 6 app.database = app.mongodb_client.get_default_database() 7 ping_response = await app.database.command("ping") 8 if int(ping_response["ok"]) != 1: 9 raise Exception("Problem connecting to database cluster.") 10 else: 11 info("Connected to database cluster.") 12 13 yield 14 15 # Shutdown 16 app.mongodb_client.close() 17 18 19 app: FastAPI = FastAPI(lifespan=db_lifespan)
An ODM, or object-document mapper, is a library that converts between documents and objects in your code. It's largely analogous to an ORM in the world of RDBMS databases. Using an ODM is a complex topic, and sometimes they can obscure important things, such as the way data is stored and updated in the database, or even some advanced MongoDB features that you may want to take advantage of. Whichever ODM you choose, you should vet it highly to make sure that it's going to do what you want and grow with you.
If you're choosing an ODM for your FastAPI application, definitely consider using a Pydantic-based ODM, such as ODMantic or Beanie. The reason you should prefer one of these libraries is that FastAPI is built with tight integration to Pydantic. This means that if your path operations return a Pydantic object, the schema will automatically be documented using OpenAPI (which used to be called Swagger), and FastAPI also provides nice API documentation under the path "/docs". As well as documenting your interface, it also provides validation of the data you're returning.
1 class Profile(Document): 2 """ 3 A profile for a single user as a Beanie Document. 4 5 Contains some useful information about a person. 6 """ 7 8 # Use a string for _id, instead of ObjectID: 9 id: Optional[str] = Field(default=None, description="MongoDB document ObjectID") 10 username: str 11 birthdate: datetime 12 website: List[str] 13 14 class Settings: 15 # The name of the collection to store these objects. 16 name = "profiles" 17 18 # A sample path operation to get a Profile: 19 20 async def get_profile(profile_id: str) -> Profile: 21 """ 22 Look up a single profile by ID. 23 """ 24 # This API endpoint demonstrates using Motor directly to look up a single 25 # profile by ID. 26 profile = await Profile.get(profile_id) 27 if profile is not None: 28 return profile 29 else: 30 raise HTTPException( 31 status_code=404, detail=f"No profile with id '{profile_id}'" 32 )
The profile object above is automatically documented at the "/docs" path:
If you feel that working directly with the Python MongoDB driver, Motor, makes more sense to you, I can tell you that it works very well for many large, complex MongoDB applications in production. If you still want the benefits of automated API documentation, you can document your schema in your code so that it will be picked up by FastAPI.
As many FastAPI applications include endpoints that provide JSON data that is retrieved from MongoDB, it's important to remember that certain types you may store in your database, especially the ObjectID and Binary types, don't exist in JSON. FastAPI fortunately handles dates and datetimes for you, by encoding them as formatted strings.
There are a few different ways to handle ObjectID mappings. The first is to avoid them completely by using a JSON-compatible type (such as a string) for _id values. In many cases, this isn't practical though, because you already have data, or just because ObjectID is the most appropriate type for your primary key. In this case, you'll probably want to convert ObjectIDs to a string representation when converting to JSON, and do the reverse with data that's being submitted to your application.
If you're using Beanie, it automatically assumes that the type of your _id is an ObjectID, and so will set the field type to PydanticObjectId, which will automatically handle this serialization mapping for you. You won't even need to declare the id in your model!
If you specify the response type of your path operations, FastAPI will validate the responses you provide, and also filter any fields that aren't defined on the response type.
1 2 async def read_item(profile_id: str) -> Profile: 3 """ Use Beanie to look up a Profile. """ 4 profile = await Profile.get(profile_id) 5 return profile
If you're using Motor, you can still get the benefits of documentation, conversion, validation, and filtering by returning document data, but by providing the Pydantic model to the decorator:
1 2 3 4 5 async def read_item(profile_id: str) -> Mapping[str, Any]: 6 # This API endpoint demonstrates using Motor directly to look up a single 7 # profile by ID. 8 # 9 # It uses response_model (above) to tell FastAPI the schema of the data 10 # being returned, but it returns a dict directly, so that conversion and 11 # validation is done by FastAPI, meaning you don't have to copy values 12 # manually into a Profile before returning it. 13 profile = await app.profiles.find_one({"_id": profile_id}) 14 if profile is not None: 15 return profile
A common mistake people make when building RESTful API servers on top of MongoDB is to store the objects of their API interface in exactly the same way in their MongoDB database. This can work very well in simple cases, especially if the application is a relatively straightforward CRUD API.
In many cases, however, you'll want to think about how to best model your data for efficient updates and retrieval and aid in maintaining referential integrity and reasonably sized indexes. This is a topic all of its own, so definitely check out the series of design pattern articles on the MongoDB website, and maybe consider doing the free Advanced Schema Design Patterns online course at MongoDB University. (There are lots of amazing free courses on many different topics at MongoDB University.)
If you're working with a different data model in your database than that in your application, you will need to map values retrieved from the database and values provided via requests to your API path operations. Separating your physical model from your business model has the benefit of allowing you to change your database schema without necessarily changing your API schema (and vice versa).
Even if you're not mapping data returned from the database (yet), providing a Pydantic class as the
response_model
for your path operation will convert, validate, document, and filter the fields of the BSON data you're returning, so it provides lots of value! Here's an example of using this technique in a FastAPI app:1 # A Pydantic class modelling the *response* schema. 2 class Profile(BaseModel): 3 """ 4 A profile for a single user. 5 """ 6 id: Optional[str] = Field( 7 default=None, description="MongoDB document ObjectID", alias="_id" 8 ) 9 username: str 10 residence: str 11 current_location: List[float] 12 13 # A path operation that returns a Profile object as JSON: 14 15 16 17 18 async def get_profile(profile_id: str) -> Mapping[str, Any]: 19 # Uses response_model (above) to tell FastAPI the schema of the data 20 # being returned, but it returns a dict directly, so that conversion and 21 # validation is done by FastAPI, meaning you don't have to copy values 22 # manually into a Profile before returning it. 23 profile = await app.profiles.find_one({"_id": profile_id}) 24 if profile is not None: 25 return profile # Return BSON document (Mapping). Conversion etc will be done automatically. 26 else: 27 raise HTTPException( 28 status_code=404, detail=f"No profile with id '{profile_id}'" 29 )
My amazing colleagues have built an app generator to do a lot of these things for you and help get you up and running as quickly as possible with a production-quality, dockerized FastAPI, React, and MongoDB service, backed by tests and continuous integration. You can check it out at the Full-Stack FastAPI MongoDB GitHub Repository.
Hopefully, this article has provided you with the knowledge to build a FastAPI application that will scale in both the amount of code you write, and the data you need to store. We're always looking for more tricks and tips for building applications with FastAPI and MongoDB, so if you have some suggestions, why not open a ticket on GitHub and we can have a chat?
We love to know what you're building with FastAPI or any other framework — whether it's a hobby project or an enterprise application that's going to change the world. Let us know what you're building at the MongoDB Community Forums. It's also a great place to stop by if you're having problems — someone on the forums can probably help you out!
Top Comments in Forums
Alessio_RuggiAlessio Ruggilast quarter
I’m commenting only to point out that the link to github doesn’t seem to work