Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Python
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Pythonchevron-right

Build a Cocktail API with Beanie and MongoDB

Mark Smith6 min read • Published May 09, 2022 • Updated Oct 01, 2024
FastApiAtlasSearchPython
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
I have a MongoDB collection containing cocktail recipes that I've made during lockdown.
Recently, I've been trying to build an API over it, using some technologies I know well. I wasn't very happy with the results. Writing code to transform the BSON that comes out of MongoDB into suitable JSON is relatively fiddly. I felt I wanted something more declarative, but my most recent attempt—a mash-up of Flask, MongoEngine, and Marshmallow—just felt clunky and repetitive. I was about to start experimenting with building my own declarative framework, and then I stumbled upon an introduction to a brand new MongoDB ODM called Beanie. It looked like exactly what I was looking for.
The code used in this post borrows heavily from the Beanie post linked above. I've customized it to my needs, and added an extra endpoint that makes use of MongoDB Atlas Search, to provide autocompletion, for a GUI I'm planning to build in the future.
You can find all the code on GitHub.
Note: The code here was written for Beanie 1.26.0. Check out the Beanie Changelog
I have a collection of documents that looks a bit like this:
1{
2 "_id": "5f7daa158ec9dfb536781b0a",
3 "name": "Hunter's Moon",
4 "ingredients": [
5 {
6 "name": "Vermouth",
7 "quantity": {
8 "quantity": "25",
9 "unit": "ml"
10 }
11 },
12 {
13 "name": "Maraschino Cherry",
14 "quantity": {
15 "quantity": "15",
16 "unit": "ml"
17 }
18 },
19 {
20 "name": "Sugar Syrup",
21 "quantity": {
22 "quantity": "10",
23 "unit": "ml"
24 }
25 },
26 {
27 "name": "Lemonade",
28 "quantity": {
29 "quantity": "100",
30 "unit": "ml"
31 }
32 },
33 {
34 "name": "Blackberries",
35 "quantity": {
36 "quantity": "2",
37 "unit": null
38 }
39 }
40 ]
41}
The promise of Beanie and FastAPI—to just build a model for this data and have it automatically translate the tricky field types, like ObjectId and Date between BSON and JSON representation—was very appealing, so I fired up a new Python project, and defined my schema in a models submodule like so:
1class Cocktail(Document):
2 class Settings:
3 name = "recipes"
4
5 name: str
6 ingredients: List["Ingredient"]
7 instructions: List[str]
8
9
10class Ingredient(BaseModel):
11 name: str
12 quantity: Optional["IngredientQuantity"]
13
14
15class IngredientQuantity(BaseModel):
16 quantity: Optional[str]
17 unit: Optional[str]
18
19
20class IngredientAggregation(BaseModel):
21 """ A model for an ingredient count. """
22
23 id: str = Field(None, alias="_id")
24 total: int
I was pleased to see that I could define a Settings inner class and override the collection name. It was a feature that I thought should be there, but wasn't totally sure it would be.
The other thing that was a little bit tricky was to get Cocktail to refer to Ingredient, which hasn't been defined at that point. Fortunately, I can just use the name of the class in a string, and things will be glued together later.
The beaniecocktails package, defined in the __init__.py file, contains mostly code for initializing FastAPI, Motor, and Beanie:
1# ... some code skipped
2
3async def app_lifespan(app: FastAPI):
4 # startup code goes here:
5 client: AsyncIOMotorClient = AsyncIOMotorClient(
6 Settings().mongodb_url,
7 )
8 await init_beanie(client.get_default_database(), document_models=[Cocktail])
9 app.include_router(cocktail_router, prefix="/v1")
10
11 yield
12
13 # shutdown code goes here:
14 client.close()
15
16app = FastAPI(lifespan=app_lifespan)
17
18# I'm using pydantic_settings to load the MongoDB connection string from an environment variable,
19# or it defaults to a local installation.
20class Settings(BaseSettings):
21 mongodb_url: str = "mongodb://localhost:27017/cocktails"
The code above defines a lifespan handler for the FastAPI app startup. It connects to MongoDB, configures Beanie with the database connection, and provides the Cocktail model I'll be using to Beanie.
The last line adds the cocktail_router to FastAPI. It's an APIRouter that's defined in the routes submodule.
So now it's time to show you the routes file—this is where I spent most of my time. I was amazed by how quickly I could get API endpoints developed.
1# ... imports skipped
2
3cocktail_router = APIRouter()
The cocktail_router is responsible for routing URL paths to different function handlers which will provide data to be rendered as JSON. The simplest handler is probably:
1@cocktail_router.get("/cocktails/", response_model=List[Cocktail])
2async def list_cocktails():
3 return await Cocktail.find_all().to_list()
This handler takes full advantage of these facts: FastAPI will automatically render Pydantic instances as JSON; and Beanie Document models are defined using Pydantic. Cocktail.find_all() returns an iterator over all the Cocktail documents in the recipes collection. FastAPI can't deal with these iterators directly, so the sequence is converted to a list using the to_list() method.
If you have the Just task runner installed, you can run the server with:
1just run
If not, you can run it directly by running:
1uvicorn beaniecocktails:app --reload
And then you can test the endpoint by pointing your browser at "http://localhost:8000/v1/cocktails/".
I drink too many
cocktails
I drink too many cocktails
A similar endpoint for just a single cocktail is neatly encapsulated by two methods: one to look up a document by _id and raise a "404 Not Found" error if it doesn't exist, and a handler to route the HTTP request. The two are neatly glued together using the Depends declaration that converts the provided cocktail_id into a loaded Cocktail instance.
1async def get_cocktail(cocktail_id: PydanticObjectId) -> Cocktail:
2 """ Helper function to look up a cocktail by id """
3
4 cocktail = await Cocktail.get(cocktail_id)
5 if cocktail is None:
6 raise HTTPException(status_code=404, detail="Cocktail not found")
7 return cocktail
8
9@cocktail_router.get("/cocktails/{cocktail_id}", response_model=Cocktail)
10async def get_cocktail_by_id(cocktail: Cocktail = Depends(get_cocktail)):
11 return cocktail
Now for the thing that I really like about Beanie: its integration with MongoDB's Aggregation Framework. Aggregation pipelines can reshape documents through projection or grouping, and Beanie allows the resulting documents to be mapped to a Pydantic BaseModel subclass.
Using this technique, an endpoint can be added that provides an index of all of the ingredients and the number of cocktails each appears in:
1# models.py:
2
3class IngredientAggregation(BaseModel):
4 """ A model for an ingredient count. """
5
6 id: str = Field(None, alias="_id")
7 total: int
8
9# routes.py:
10
11@cocktail_router.get("/ingredients", response_model=List[IngredientAggregation])
12async def list_ingredients():
13 """ Group on each ingredient name and return a list of `IngredientAggregation`s. """
14
15 return await Cocktail.aggregate(
16 aggregation_query=[
17 {"$unwind": "$ingredients"},
18 {"$group": {"_id": "$ingredients.name", "total": {"$sum": 1}}},
19 {"$sort": {"_id": 1}},
20 ],
21 projection_model=IngredientAggregation,
22 ).to_list()
The results, at "http://localhost:8000/v1/ingredients", look a bit like this:
1[
2 {"_id":"7-Up","total":1},
3 {"_id":"Amaretto","total":2},
4 {"_id":"Angostura Bitters","total":1},
5 {"_id":"Apple schnapps","total":1},
6 {"_id":"Applejack","total":1},
7 {"_id":"Apricot brandy","total":1},
8 {"_id":"Bailey","total":1},
9 {"_id":"Baileys irish cream","total":1},
10 {"_id":"Bitters","total":3},
11 {"_id":"Blackberries","total":1},
12 {"_id":"Blended whiskey","total":1},
13 {"_id":"Bourbon","total":1},
14 {"_id":"Bourbon Whiskey","total":1},
15 {"_id":"Brandy","total":7},
16 {"_id":"Butterscotch schnapps","total":1},
17]
I loved this feature so much, I decided to use it along with MongoDB Atlas Search, which provides free text search over MongoDB collections, to implement an autocomplete endpoint.
The first step was to add a search index on the recipes collection, in the MongoDB Atlas web interface:
Create a new search index on the recipes collection
Create a new search index on the `recipes` collection
I had to add the name field as an "autocomplete" field type.
The name field must be of type "autocomplete"
The name field must be of type 'autocomplete'
I waited for the index to finish building, which didn't take very long, because it's not a very big collection. Then I was ready to write my autocomplete endpoint:
1@cocktail_router.get("/cocktail_autocomplete", response_model=List[str])
2async def cocktail_autocomplete(fragment: str):
3 """ Return an array of cocktail names matched from a string fragment. """
4
5 return [
6 c["name"]
7 for c in await Cocktail.aggregate(
8 aggregation_query=[
9 {
10 "$search": {
11 "autocomplete": {
12 "query": fragment,
13 "path": "name",
14 }
15 }
16 }
17 ]
18 ).to_list()
19 ]
The $search aggregation stage specifically uses a search index. In this case, I'm using the autocomplete type, to match the type of the index I created on the name field. Because I wanted the response to be as lightweight as possible, I'm taking over the serialization to JSON myself, extracting the name from each Cocktail instance and just returning a list of strings.
The results are great!
Pointing my browser at http://localhost:8000/v1/cocktail_autocomplete?fragment=fi gives me ["Imperial Fizz","Vodka Fizz"], and http://localhost:8000/v1/cocktail_autocomplete?fragment=ma gives me ["Manhattan","Espresso Martini"].
The next step is to build myself a React front end, so that I can truly call this a FARM Stack app.

Wrap-Up

I was really impressed with how quickly I could get all of this up and running. Handling of ObjectId instances was totally invisible, thanks to Beanie's PydanticObjectId type, and I've seen other sample code that shows how BSON Date values are equally well-handled.
I need to see how I can build some HATEOAS functionality into the endpoints, with entities linking to their canonical URLs. Pagination is also something that will be important as my collection grows, but I think I already know how to handle that.
I hope you enjoyed this quick run-through of my first experience using Beanie. The next time you're building an API on top of MongoDB, I recommend you give it a try!
If this was your first exposure to the Aggregation Framework, I really recommend you read our documentation on this powerful feature of MongoDB. Or if you really want to get your hands dirty, why not check out our free MongoDB University Aggregation course?
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.

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Upgrade Fearlessly with the MongoDB Stable API


Mar 05, 2024 | 5 min read
Tutorial

Building a Restaurant Locator Using Atlas, Neurelo, and AWS Lambda


Apr 02, 2024 | 8 min read
Tutorial

How to Build a RAG System Using Claude 3 Opus And MongoDB


Aug 28, 2024 | 15 min read
Quickstart

Introduction to Multi-Document ACID Transactions in Python


Sep 11, 2024 | 10 min read
Table of Contents
  • Wrap-Up