Beanie - An βobject document mapperβ (ODM) that allows you to model your MongoDB using python.
This blog post provides a working example of a webapp that uses all three technologies π!
Motivation
I wanted to create a web app that uses FastAPI, MongoDB, and Beanie. But, I could not find any really good examples that used all three. This blog post is to demonstrate what I learned while building a web app using these three tools. I have tried to make the example simple enough that it can easily be implemented by others, but also complex enough that it is interesting, and could be used to bootstrap a real project for someone else.
Now that you have all of the dependencies installed you are ready to start coding! Below is the complete code. Take a moment to review the code and see if you can tell what is going on. Then will we will walk through each part in detail and explain what is happening.
main.py: This is where all of our python code lives.templates/_layout.html: This template is the base template which all others will inherit from. It contains common elements such as the navigation bar and footer. This tutorial will not dive deep into Jinja2 templates, so just take a quick look at this code and then copy it into your project.templates/index.html: The template for the home page.templates/dogs.html: The templates for the dogs page.templates/dog_profile.html: The templates for the dog profile page.
Step 1: Define your models with Beanie
One of the cool parts about using Beanie is that you can define your models using Pydantic. In our example app there will be two two classes: Breed and Dog. In terms of MongoDB each one of these python classes is a collection. Each instance of a class is a document.
Note that there are a few differences between the Beanie models defined above, and a βnormalβ Pydantic model:
Each class inherits from beanie.Document as opposed to pydantic.BaseModel. By inheriting from beanie.Document the model will know how to interact with MongoDB.
The attribute Dog.breed is a special kind of attribute in Beanie. The Link type tells Beanie to create a relationship between Dog and Breed. The Dog.breed attribute will contain a reference to a Breed document.
Step 2: Create demo data
In order for our app to be interesting we need to fill the MongoDB database with some demo data. The code below creates five documents: two dog breeds, and three dogs. To define a new document you create a new instance of a Document class. Then each document is inserted into the database.
Note
The function is async. This is because beanie only supports async interactions with MongoDB. Therefore, whenever we are interacting with the database it must be async. This could change in the future, but as of March 2022 beanie is ONLY async.
Step 3: Setup FastAPI and MongoDB database
The next task is to set up our FastAPI app, and initialize our MongoDB database.
Lets dive into the key parts:
Create a FastAPI app as your normally would. What may look a little bit less familiar are the highlighted lines above.
First we mount the directory we created named static to our app. This will allow FastAPI to access the files in this directory from the app.
Second we tell FastAPI where to find our template files. This template files define the UI of our website using the templating language Jinja2.
Next, we define a function that tells that app what do do on start up. This function will be called every time the app starts up (or restarts).
Note the function is async. This is because we will be interacting with the MongoDB database.
First we use motor to connect to the MongoDB database. In this case, we are running MongoDB on our local computer. If you were connecting to a cloud instance of MongoDB you would need to change the connection string.
Next we get a list of all the database names in MongoDB. We do this to check if the βdogsβ database has been created yet. If it has not been created, we will insert our demo data. If it has already been created, we will not insert any demo data.
Note
In a βrealβ production app you would probably not have any logic here to check if the database has been created already. We put the logic in this app simply because it is a demo app.
Lastly we call the init_beanie function. The key bit here is that we pass a list of Document objects. This tells Beanie how to interact with MongoDB database.
Step 4: Home page
With all our set up complete, now things can start to get fun! All that is left to do is define the routes in our app. Each webpage in our app needs a corresponding function that tells the app what content to send to the web browser. Lets take a look at our homepage function:
Lets break down the key parts:
The first argument in @app.get is the URL path. Every time a web browser visits the homepage (http://127.0.0.1:8000/) this function will be called.
Notice that we have defined a response_class. By declaring response_class=HTMLResponse the docs UI will be able to know that the response will be HTML.
Our homepage is pretty simple and does not have any query parameters or user input. If you are used to using FastAPI for APIs or using flask you are probably surprised to see request: Request. When rendering a template in FastAPI you must send the request object (https://fastapi.tiangolo.com/advanced/templates/?h=template#using-jinja2templates).
When you want to return an HTML page, you must return templates.TemplateResponse. Remember we defined templates further app in main.py and told FastAPI where the templates are saved.
templates.TemplateResponse will always take two arguments:
The location of the template, relative to the path that you defined above (e.g. templates = Jinja2Templates(directory="templates")).
The context which is a dictionary. It must always contain "request": request. For our homepage, there is no other data to pass along, but as you will see in the next pages additional information will be passed to the context. Basically the context must contain any data that your template expects to receive.
Step 5: Breeds page
Our next view will be a little bit more interesting, here we will pass some additional data into our template to dynamically render HTML for each breed.
The first few lines look pretty similar to the homepage. Again we must define response_class=HTMLResponse and request: Request. What is new this time is the additional key value pair that we have passed into the context.
Lets take a closer look at the template to see what is going on:
I wonβt dive into the Jinja2 syntax in this blog post, but hopefully it is easy enough to see what is happening. The template is expecting a variable named breeds. It then loops over that variable and for each breed creates a Bootstrap Card component.
Because the template is expecting a variable named breeds, we must pass it into the context.
Step 6: Dogs page
In our next view we add an additional layer of complexity. The /dogs view includes an optional query parameter breed_id. If a breed_id is provided the page will only render all the dogs of that specific breed:
Otherwise, it will render all of the dogs.
Lets take a closer look at the function:
In the function definition we have provided an argument named breed_id.
FastAPI is really good at figuring out what each argument means. In this case, it automatically determines that the breed_id is a query parameter. It also knows that it is optional because we have used the Optional type. See https://fastapi.tiangolo.com/tutorial/query-params/#query-parameters for an explanation on how FastAPI decides if an argument is a query parameter or path parameter:
When you declare other function parameters that are not part of the path parameters, they are automatically interpreted as βqueryβ parameters.
In this code snippet we apply the logic to determine which dogs to get from the database. If no breed_id is specified we get all of the dogs.
Lastly, as with the above views we create our context dictionary. This context has three key value pairs:
The required request object that must always be returned.
A list of dog objects.
An optional breed object.
Note that the template applies conditional logic to only render the breeds name if the breed is not None.
Step 7: Dog page
In our web app each dog has itβs very own profile page. Here we take a new approach and use a path parameter:
Like we said above, FastAPI is really smart at determining if an argument is a path parameter or a query parameter.
In this case, FastAPI knows that it is a path parameter because we have defined {dog_id} in our views url path, and we have used the same string dog_id in our function definition. Because the two values are the same, FastAPI knows they related to one another.
Just like before, we query the database, and then return our context dictionary. This time the context only includes the required request object and a single dog object. The template will render a nice profile page for each dog.
Wrap up
Congratulations! You just built a fully functional and asynchronous web app using FastAPI, Beanie, and MongoDB π. This app was pretty basic, but is a good starting point for developing something more complex and interesting. Here are a few ideas on how you could extend the app:
Refactor the code into more than one file (e.g. not all the code needs to live in main.py).
Add a forms so that the users can add new breeds and new dogs.
Further learning
Check out these useful resources for learning more about MongoDB, FastAPI, and Beanie.