Build a Cocktail API with Beanie and MongoDB
Rate this tutorial
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 , , and —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 . It looked like exactly what I was looking for.
I have a collection of documents that looks a bit like this:
I was pleased to see that I could define a
DocumentMeta 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,
update_forward_refs method can be used later to glue together the references. I could have just re-ordered the class definitions, but I preferred this approach.
The code above defines an event 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.
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.
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:
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
If not, you can run it directly by running:
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
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:
I had to add the
name field as an "autocomplete" field type.
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:
$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!
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.