Well, this post has been a little while in the making. I’ve been hard at work figuring out best practices for my fledgling API, and learning a whole lot more about the process along the way. In the process, I’ve managed to deepen my understanding of sqlalchemy fairly significantly, improved my error handling, get in an increasingly good place for the future of this app.
Before we dive in: Error Handling
In the process of making my first few endpoints, I quickly realized that I was going to need a few additional error codes that provided more input than the default flask ones. Checking the documentation, I followed these recommendations for a quick-and-dirty implementation. My intention here was not to completely flesh out my error handling, but instead to provide something that I could use in the basic creation of my endpoints, so that I could put the errors in now and not have to worry about tracking them down later.
I created two errors: the first is essentially a carbon copy of the InvalidUsage
Exception outlined above, and the second is a more specific NotFoundException
for 404 errors.
As you can see, the NotFoundException
is a child of InvalidUsage
and customizes it for 404 responses.
I then created a simple error handler and initialized it as a blueprint.
And that’s it (for now). Like I said, nothing fancy, just something to use so that I didn’t feel like I was leaving huge holes in my endpoints. I plan to come back through later and add some other exceptions, such as an invalid schema.
Anyway, on to the main story.
Ingredient Schemas and Endpoints
First up is the Ingredient class. I decided to use marshmallow (specifically, the flask-marshmallow and marshmallow-sqlalchemy integrations) for my serializing and validating. I liked the idea of holding all of my validating, loading, and dumping into a series of classes, and after writing the code for a few of my endpoints, I’m confident I made the right decision.
The integration with flask-sqlalchemy makes serilization very easy; I just have to declare the schema as a child of SQLAlchemyAutoSchema
and marshmallow does most of the heavy lifting for me.
The two other piece of code here to note are the _links
field, which links to my endpoints for the ingredient (more on that in a moment), and the @post_load
function, which returns the actual object rather than a dictionary of attributes.
With the schema defined, I created my endpoints. The first is the simplest; a "/ingredients"
GET
call will return a list of all ingredients in the database.
Likewise, a POST
call to the same route inserts a new ingredient. Here I added a try/except block to catch several of the most common errors that might happen (errors that I’d tried to plan for in my creation of the database). An IntegrityError
means that the ingredient is already in the database, while a ValueError
means that the ingredient does not conform to database standards (just letters and spaces).
Finally, I added specific GET
and DELETE
calls. Here I did something a bit differently; I wanted people to be able to identify the ingredient either by name or by id (though I figure that identifying by name will be more common). To achieve this, I created two decorators for each route with a common variable name identifier
, and wrote a simple function that returns an ingredient for the id
or the name
. This prevented a lot of double code.
(Note also that I replace the “-“ sumbols with spaces; that way “whole milk” can be written as either “whole-milk” or the automatic encoding “whole%20milk.”)
… and those are my ingredient endpoints! Definitely the simplest among them, as each ingredient is just a single atomic value that can’t be changed. They can be added and deleted (although I’m going to restrict who can delete them), but otherwise they’re kind of inert.
RecipeLine Schemas and Endpoints
Next I built endpoints for the RecipeLine objects. This might not seem strictly necessary at first glance, but remember that I need to add ways for individual ingredients to be added to and taken away from recipe lines, so it was necessary to have a way to get to an individual line.
First I created a new schema for the RecipeLine object. This schema has a Nested
field that returns the schemas for the ingredients that make up the line.
My GET
and DELETE
calls are essentially the same as with my ingredients: they identify a line by its id
attribute and return/delete it as necessary. Currently, I’m using the “/lines” route. I toyed with the idea of nesting routes with recipes (i.e., “/recipe/recipe-id/lines/line-id”), but at the moment the identity of the recipe a line is part of isn’t needed to get the line, and I wanted to avoid unnecessary routes.
My next endpoint, however, is a bit different. It’s a PUT
call to an individual line, and is used to set the ingredients on the line. In creating this method, I knew that I wanted several features. I wanted to be able to input a list of ingredients, and I wanted the program to automatically add any of the ingredients in the list to the database if they were not already there. It would then link the ingredients to the line, raising the database’s validator error if the ingredient in question was not on the line.
In order to do this, I created a new schema, RecipeLineUpdateSchema
, that checks the PUT
data and validates it as ingredients. If so, it “cleans” the ingredient by making it all lower case and removing dashes, and checks the database to see if the ingredients are there. If not, it adds them. Finally, it returns a list of all the ingredients, ready to be added to the recipe line.
This schema is then used to validate the input in my actual route:
What I like about this approach is how it simplifies my routes. Because most of the logic is done in other classes, my endpoints end up looking much cleaner. I may push this further in the future, however, and abstract them away to a series of nice looking functions.
Recipe Schema and Endpoints
Finally, we come to the third schema/endpoint grouping that I have created. The recipe schema is essentially the same as the two previous schemas (including a Nested
field for the RecipeLine objects). There is one critical difference, however: I wanted to be able to easily access all the ingredients in all the lines of the recipe–essentially have a list of all the ingredients needed to make the recipe. But the Recipe objects don’t actually know all their ingredients–the lines do. This meant I would need some join
statements for my database.
Honestly, it took me longer than I care to admit to get this one working, but I persisted and I feel like I have a much better grasp on SQLAlchemy now. With my last attempt, dealing with the database was more or less like a black box; I could ask for simple things and otherwise tried to leave it alone. This time around, I have a much better concept of how it actually works.
I query the Ingredient class and create a join with the RecipeLine class, based on the RecipeLines that are associate with an ingredient (this eliminates ingredients that are not in any RecipeLine). Then I join that with the Recipe table; I don’t need to specify the join here because SQLAlchemy automatically defaults to a many-to-one relaitonship. This would, I believe, eliminate any Recipe without lines, although that isn’t really a situation I expect will be encounted.
Then, this query is filtered by the id of the current recipe (that’s the data["id"]
call), resulting in only ingredients that are in the given recipe. The final .all()
just returns them as a list.
Here’s the call in the entire schema:
(NOTE: Some of this information is no longer current. Please see [this]/2020/05/09/Schemas-Part-2.html for an updated look at how I’m filtering ingredients.)
Next, I created some endpoints for the recipe, including a GET
for all recipes and for an individual recipe, and a POST
to add an additional recipe.
Another great thing about using Marshmallow is that the nested schemas allow me to create Recipes, RecipeLines, and Ingredients all in one, if I want. For example, a valid POST
to create a new recipe, lines, and ingredients could look like:
This is the format that I hope to initalize new recipes with on the frontend, but I’m getting ahead of myself. Plus, there are still some bugs to work out in this area.
Filtering Recipes by Ingredient
One last area to note here before I close out this post. There will be times when I user might want to search for a recipe based on the ingredients in it. Maybe they have some extra eggs or something and want to know what to do with them. Because of this, and because I would need how to add filters anyway, I created a query for the main recipe route that filters by ingredients. Ingredients would be requested in the format “/recipes?ingredients=eggs,bacon,whatever-you-want” and the route would return a list of all recipes in the database with these ingredients. This necessitated another lengthy join statement, but this one didn’t take as long; I think I’m getting the hang of it.
More difficult was filtering by all of the different ingredients. At first, the base in_
SQLAlchemy command functioned as an “OR” statement, returning every recipe that had, say, eggs OR bacon, which is cool but not what I wanted for this command (maybe later though). Ultimately, I used set
s, creating a simple __hash__
in the Recipe model class, building a set for each requested ingredient, and then finding the intersection of all the sets.
Currently, this code is in the main route of the “/recipes” GET
method, but I plan to change that. Right now, I’m just glad it works.
Conclusions
This has been a very long post, and, especially towards the end of this work, I felt that I was maybe not structuring as well as I could be, and there are certainly areas to go back to and build on. But still, I’m quite pleased with what I’ve accomplished here. I’ve made great progress on the endpoints and vastly expanded my knowlege of how APIs work. Plus, I think I’m writing much better code. I’ll have to post some to Reddit soon to rid myself of that notion.
Until next time, signing off.