Vacation time, an excellent time to learn something new, so I figured, why not build a recipe bot that recommends recipes? For that I needed a way to store recipes that the bot could retrieve. And I figured, why not try to build that in Python?
In this article I will show you a quick way to build a REST API in Python using the Django framework. I will also show you some of the things that I ran into while building the REST API so you don't have to get caught up in those kind of problems.
Getting started
Django is a Python framework for building websites. It's a framework for perfectionists with deadlines, according to the website. That sounds promising.
To get started with Django you need the Django package installed on your computer alongside Python 3. I'm assuming you already have Python installed. To install Django execute the following command:
pip install django
After you've installed Django, let's create a new project and get started with our REST API. To do that, you need to execute the command:
django-admin startproject recipebot
Of course if you want to build something else, you can replace the name with whatever you're building.
A project has several files in it. The most important file is the manage.py
file in the root of the project. This file is used to manage the different aspects of your project.
A Django project is built out of several apps, modules if you will. Each app contains a small set of related logic. You can use logic from different apps to build your project.
The project layout typically looks like this:
recipebot
|-- recipebot
|--<app>
|-- <app files...>
|--<app>
|-- <app files...>
The project folder contains a folder with the name of the project directly underneath it. Each app in your project gets its own folder under the project root.
Create a new app in Django
To create a new app you use the manage.py file in the root of your project. Execute the following command to create a new app:
python manage.py startapp api
When you execute this command you end up with a new folder within the project folder that contains the files for your app:
- models.py
- views.py
- admin.py
- apps.py
- tests.py
- migrations/_init_.py
An app typically has a few models in it and views that provide a way to render those models on the website. As with any good quality app there is support for tests. You can use migrations to create and/or upgrade your database. Finally there's even the possibility to create an admin page.
To use the app, you need to modify the settings.py
file in the project so that it includes the app in the INSTALLED_APP
setting.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api',
]
As you can see, the INSTALLED_APPS
setting defines several apps. There's the admin app, that automatically adds an admin section to your project and several apps that provide useful utilities.
A project not only has your own apps in it, but you can use apps from third parties as well. We'll use this later on when we turn our project into a REST API.
Create a few models
As I mentioned, an app has several models. You can skip these if you don't want the default model behavior offered by Django. But I found these quite nice given that I have relational data.
Let's create a few models. In the models.py add a set of classes for our recipe bot:
from django.db import models
class Recipe(models.Model):
name = models.CharField(max_length=150)
description = models.TextField()
cooking_instructions = models.TextField()
slug = models.SlugField()
preparation_time = models.IntegerField(help_text='Preparation time in minutes')
cooking_time = models.IntegerField(help_text='Cooking time in minutes')
created = models.DateTimeField()
modified = models.DateTimeField()
class Ingredient(models.Model):
name = models.CharField(max_length=150)
amount = models.IntegerField()
measurement = models.CharField(max_length=150)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE,
related_name='ingredients')
There's quite a few things happening in this code, so let's go over them one by one.
Define a model
You define a model as a python class, which derives from django.db.Model
. The model can have several properties. For example, name and description are properties on the model.
Django comes with CharField
, TextField
and IntegerField
for example. These define standard behavior for database fields. For example, you can configure the maximum length of a CharField
and a help text.
Define a relationship
As your data is typically relational, you need to define relationships between models. Django uses the ForeignKey
for this. You add a property of the type ForeignKey
and provide the model class it refers to.
You need to define on_delete
behavior for each foreign key. You can either use PROTECT
or CASCADE
behavior for foreign keys. When you use CASCADE
the child (Ingredient in my sample) gets deleted when the parent (the recipe) is deleted. When you use PROTECT
the parent cannot be removed without the child being removed first.
Notice that I used related_name
in the foreign key definition. This gives the reverse relation property a name. I defined the foreign key on Ingredient
, but Django automatically defines ingredients
as a property on Recipe
, because I used the related_name
setting.
Rendering models
Now that you have serializers for the models, let's define a set of views for the models using those serializers.
In order to render any data over HTTP you need to define views in Django. Views render the data. Normally a view would look like this:
from django.http import JsonResponse
from . import models
def recipe_list(request):
result = []
for recipe in Recipe.objects.all():
result.append({ 'name': recipe.name, 'slug': recipe.slug })
return JsonResponse(result)
This function takes the incoming request as argument. It retrieves recipes, which it returns in a JsonResponse
.
Django has so called manager objects attached to the model classes you create that allow you to execute SQL queries. The all()
query returns all objects of a given type from the database.
To use the recipe_list
view you need to include it in the urls.py
of the project. You can do this by creating a new file urls.py
within the app.
from django.urls import path
urlpatterns = [
path('recipes', views.recipe_list)
]
Next include the urls from the api
app in the main project using the following piece of code in the urls.py
of the project:
from django.urls import path, include
urlpatterns = [
path('api', include('api.urls'))
]
This tells the project to include all urls from the api
app under the path /api/
. Ultimately when you get an url like /api/recipes
the recipes_list
function gets called by Django to render the response.
Notice that the Django URL dispatcher doesn't distinguish between GET, POST or PUT when calling a function that is attached to a URL. You need to do that yourself.
This amounts to a lot of boilerplate code when trying to build a REST API with Django. Because one function has to handle POST and GET.
Convert the website into a REST API
Django in its vanilla form doesn't really support REST endpoints. Because of the amount of boilerplate you need to build. It expects you to render either HTML or a very basic JSON response. It's quite hard to build a proper REST API in that way.
Luckely there's a framework that you can add to Django that makes building REST APIs with Django a breeze: Django Rest Framework
Install the Django Rest Framework
To install the Django Rest Framework execute the following command:
pip install djangorestframework
Next add the app rest_framework
to the list of installed apps in the settings.py
file of your project:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'rest_framework',
'api',
]
Add serializers for your models
To render a model in a REST response you need to define a serializer for the model. The serializer reads data from your model instances and converts it to JSON. It also reads and validates incoming data before updating or creating model instances.
You can use the serializers to shape the data in requests and responses. So although your model has a certain shape, you can change that shape so that it better fits your REST interface.
Let's define serializers for our models. Add a new file to the api
app with the name serializers.py
and add the following content to it:
from rest_framework import serializers
from . import models
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = models.Recipe
fields = ('name', 'amount', 'measurement')
class RecipeSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True, read_only=True)
class Meta:
model = models.Recipe
fields = ('slug', 'name', 'description', 'cooking_instructions',
'cooking_time', 'preparation_time',
'created', 'modified')
Derive your serializer from ModelSerializer
. A serializer needs a nested class Meta
which defines meta behavior for the serializer. The serializer needs to know about your model, so assign the property model with the name of the model that the serializer is for. Next define the fields that the serializer should serialize or deserialize.
When you use ModelSerializer
with the right Meta
class definition, you will get the definition of how each field in the model should be processed for free. In the RecipeSerializer
however you need to define the ingredients property explicitly.
The ingredients
property won't get serialized by default, because it's a reverse relation lookup of the foreign key in Ingredient
. If you want the ingredients to be included in the recipe you need to tell the serializer that.
I defined the ingredients
property using the IngredientSerializer with the setting many=True
and read_only=True
. Now when you request a recipe from the API you get the ingredients included in the response. However, because its a read-only serializer you are not able to include ingredients when you POST a new recipe to the API.
By using Serializers you remove the boilerplate code that is needed to read in data from incoming requests and serializing data to JSON. Thus removing some of the work you had to do in your views.
Create viewsets for your models
The views itself in vanilla Django also contained a lot of boilerplate code. In the Django Rest Framework this is solved by creating viewsets. Let's define a couple for our recipe API in the views.py
file of the api
app.
from rest_framework import viewsets
from . import serializers
from . import models
class RecipeViewSet(viewsets.ModelViewSet):
serializer_class = serializers.RecipeSerializer
queryset = models.Recipe.objects.all()
A viewset contains all logic required to handle POST, PUT, PATCH, DELETE and GET requests. You don't need to build all that boilerplate when you use a viewset.
Notice that I called the all()
method on the Recipe manager object. The return type of this method is a lazy queryset. It doesn't really retrieve anything until I iterate over it. That's why you can call it here and assign its result to the queryset.
The queryset
is used in the various request handling methods within the viewset. This works, because you can call create, save and destroy on the queryset to manipulate the data.
The serializer_class
ensures that all incoming data is handled properly and that output data is correctly rendered.
Bind the viewset to a URL
As you've seen before you need to connect views to a URL in order to use them. This is no different for the viewsets. However for the viewsets to work correctly you need to use a router.
Django Rest Framework uses routers to handle requests with viewsets. You can define a router in the urls.py
file of the api app as follows:
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register('recipes', views.RecipeViewSet)
urlpatterns = [
path('', include(router.urls))
]
First you create a new instance of DefaultRouter
and register the recipes path with it using the RecipeViewSet
.
After you've setup the router you can include its urls in the urlpatterns with the include
statement that we've used before to include the api
urls in the project urlpatterns.
Run your application
When you have defined models, serializers and viewsets you can run your project and check if things work the way you expect. To do this you need to invoke the following command:
python manage.py runserver
This will start the server on http://localhost:8000/, you can open a webbrowser to view your project. Navigate to http://localhost:8000/api/recipes to get the recipes in your application. You will be greeted with a page that looks like this:
One of the neat things about the Django Rest Framework is that it not only makes building REST APIs easier, it also includes nice little things like the HTML interface to try the API from the browser.
Tips and tricks
There were a few gotchas that I ran into while building my recipebot API:
- How does one show a different set of fields in a list of recipes versus the details of a single recipe?
- How do you use the slug in the URL instead of a technical ID?
Showing different fields for different actions
I wanted to return a short summary of each recipe when the user requests /api/recipes/
url, but a full recipe when the user requests /api/recipes/1
.
It turns out to be quite easy to show a summary in a list versus the full model in a details action.
Remember, the viewset has a property serializer_class. We used it earlier to setup a serializer for the viewset. You can also use the get_serializer_class method instead. In this method you can simply ask for the action that is being executed and return a different serializer:
from rest_framework import viewsets
from . import views
from . import models
class RecipeViewSet(viewsets.ModelViewSet):
queryset = models.Recipe.objects.all()
def get_serializer_class(self):
if self.action == 'list':
return serializers.RecipeSummarySerializer
return serializers.RecipeSerializer
How to use a slug instead of technical ID for lookups
Technical IDs are perfect for uniquely identifying objects in the database. They are a nightmare when you're building something that needs to be used by humans.
To solve the problem you can inslude a SlugField
in your model and use that as a lookup field in the viewset. Notice that I had one in my Recipe
model already.
Now to use it in a viewset, simply set the lookup_field
property on the viewset:
from rest_framework import viewsets
from . import views
from . import models
class RecipeViewSet(viewsets.ModelViewSet):
queryset = models.Recipe.objects.all()
serializer_class = serializers.RecipeSerializer
lookup_field = 'slug'
This extra step makes the API much more friendly towards humans. While still keeping the technical ID in the database.
Final thoughts
Django with the Django Rest Framework is a great combination for building REST APIs, they've put quite a bit of effort into reducing the amount of boilerplate.
Do keep in mind though that although I've shown a pretty complete sample of what you can expect there's quite a bit more to it.
If you're interested in the code I'm working on, it's on Github: https://github.com/wmeints/recipebot