Part 2 - Building RESTful Endpoints from Django Models
In part one of this tutorial, we laid the groundwork for building RESTful APIs in Django by manually creating endpoints without utilising models. In this second part of the tutorial, we'll dive into the practical aspect of building RESTful APIs by focusing on a specific model: the Course
model.
The Course
model serves as a good example for our API development journey. We'll explore its attributes such as title
, course_id
, description
, and price
, understanding how to manipulate and expose this data.
First, let's create a new Django app named "courses," by running the following command in our backend project directory:
python manage.py startapp courses
After running the command, Django will create a new directory named "courses" within your project directory. Inside this directory, you'll find various files and folders that Django generates for the new app.
We also need to add this new app to your project settings:
Next, in our courses app models.py we need to create a Django model to represent our Course
entity, so let's edit our courses models.py file as follows:
from django.db import models
class Course(models.Model):
title = models.CharField(max_length=120)
course_id = models.CharField(max_length=20)
description = models.TextField(blank=True, null=True)
price = models.DecimalField(
max_digits=15,
decimal_places=2,
default=99.99
)
def __str__(self):
return self.title
@property
def discount_price(self):
"""
Calculate and return the discounted price after
applying a 50% discount.
"""
# Define the discount rate
discount_rate = 0.5 # 50% discount
# Calculate the discounted price
discounted_price = float(self.price) * (1 - discount_rate)
# Round the discounted price to two decimal places
discounted_price = round(discounted_price, 2)
# Format the discounted price with two decimal places using f-string
return f"{discounted_price:.2f}"
This simple model provides a basic structure for storing information about courses, including their title, unique identifier, description, and price. Additionally, it offers a convenient method to calculate and retrieve the discounted price of a course.
The
@property
decorator in Python is used to define properties or attributes of a class that behave like regular attributes but are computed dynamically. When you apply@property
to a method within a class, it allows you to access the method as if it were an attribute rather than a method call.
As always, when creating a new model in Django we need to run the makemigrations
command followed by the migrate
command. These commands create migrations for your model changes and apply those migrations to your database, respectively.
python manage.py makemigrations
python manage.py migrate
You can also make the Course model accessible and manageable through the Django admin site, allowing administrators to view, add, edit, and delete Course instances directly from the admin interface. To do this, edit the courses app admin.py file as follows:
from django.contrib import admin
from .models import Course
# Register your models here.
admin.site.register(Course)
To create a superuser and view the Course database in the Django admin Run the command:
python
manage.py
createsuperuser
and Follow the prompts to enter a username, email, and password for the superuser.Then Start your Django development server if it's not already running:
python
manage.py
runserver
.
Open a web browser and go to the admin URL: http://127.0.0.1:8000/admin/
and log in using the credentials of the superuser you created.
We can use the admin site and navigate to "Courses" to view, add, edit, or delete Course instances. However in this tutorial I will be using the Django shell to add and retrieve courses.
The Django shell is an excellent tool for beginners to gain hands-on experience with Django's features and deepen their understanding of web development with Django. The Django shell is an interactive command-line interface shell environment that allows you to write Python statements from the command line as though they're being executed from within the Django Web Framework.
To start the Django shell, navigate to the root directory of your Django project and run the following command:
python manage.py shell
Once the shell starts, you'll see a \>>> prompt similar to the Python shell, but with Django's environment loaded. You can now interactively execute Python code and interact with your Django project's components.
Now we have the shell started, lets add some courses to the database. Before we can add a course we must import our Course model by executing the following command:
from courses.models import Course
We can use the Django Shell to add some courses to our database as follows:
# Create and save the first course
course1 = Course.objects.create(
title="Introduction to Programming II",
course_id="CM1010",
description="Learning object oriented programming with JavaScript",
price=100.00
)
# Create and save the second course
course2 = Course.objects.create(
title="Algorithms and Data Structures I",
course_id="CM1035",
description="Mastering algorithmic programming techniques.",
price=200.00
)
# Create and save the third course
course3 = Course.objects.create(
title="Graphics Programming",
course_id="CM2030",
description="Learning computer graphics with the p5.js library",
price=300.00
)
# Create and save the fourth course
course4 = Course.objects.create(
title="Advanced Web Development",
course_id="CM3035",
description="Full stack web development with django. The web framework for perfectionists with deadlines.",
price=400.00
)
We can verify that the courses were added to the database by querying the Course
model by running the Course.objects.all()
command as follows:
>>> # Retrieve all courses
>>> Course.objects.all()
<QuerySet [<Course: Introduction to Programming II>,
<Course: Algorithms and Data Structures I>,
<Course: Graphics Programming>,
<Course: Advanced Web Development>]>
>>>
We can also use the objects.get()
method to retrieve the course based on a specific attribute, such as the course_id
or title
. For example, to retrieve a course with a specific course_id
, you can use:
# Retrieve a course by course_id
>>> Course.objects.get(course_id="CM1010")
If you want to retrieve a course based on another attribute, such as title
, you can modify the query accordingly:
# Retrieve a course by course_id
>>> Course.objects.get(title="Advanced Web Development")
To get a random course from the database using the Django ORM, you can leverage the
order_by('?')
method followed byfirst()
. This method will shuffle the queryset and return the first item, effectively giving you a random record.
Here's how you can do it:
# Get a random course
>>> Course.objects.order_by('?').first()
NOTE: you can use
Course.objects.all().order_by('?').first()
to achieve the same result. This query retrieves allCourse
objects from the database, shuffles them randomly, and then selects the first one.However, it's important to note that
Course.objects.all()
retrieves all records from the database, which can be inefficient and consume a lot of memory if you have a large dataset. UsingCourse.objects.order_by('?').first()
directly fetches a single random record without first retrieving all records, making it more efficient for large datasets.
Now we can retrieve a random course from our database, lets defines a Django view function called api_random
that handles HTTP requests. When a request is made to this endpoint, the function retrieves a random instance of the Course
model from the database, it then constructs a JSON response containing the data of the randomly selected course (if any) and returns it using the JsonResponse
class. If there are no courses in the database, it returns an empty JSON object.
Inside our api app's views.py we need to first imports the Course
model from the models
module within the courses
app. This allows us to access and work with the Course
model in our api app.
from courses.models import Course
def api_random(request, *args, **kwargs):
model_data = Course.objects.order_by("?").first()
data = {}
if model_data:
data['id'] = model_data.id
data['title'] = model_data.title
data['course_id'] = model_data.course_id
data['description'] = model_data.description
data['price'] = model_data.price
return JsonResponse(data)
We also need to update our api app's urls.py as follows:
from django.urls import path
from .views import hello_world, api_random
urlpatterns = [
path('', hello_world, name='hello_world'),
path('random/', api_random, name='random'),
]
Now we have implemented a random RESTful endpoint in our Django application, let's modify our client.py to fetch data about a random course from our specified API endpoint http://127.0.0.1:8000/api/random/ and print the status code and JSON data returned by the server.
import requests
# Base URL
BASE_URL = 'http://localhost:8000/api'
# RANDOM COURSE URL
RANDOM_COURSE_URL = f'{BASE_URL}/random/'
response = requests.get(RANDOM_COURSE_URL)
# Check if the request was successful (status code 200)
if response.status_code == 200:
# Print the response data
print('\n======== Status Code ========\n')
print(f'Connected status code is: {response.status_code}')
print('\n======== JSON ========\n')
print(f'JSON is: {response.json()}')
else:
# Print an error message
print(f'Error: {response.status_code}')
Now, every time we run our client.py script we sends a GET request to the /api/random/ endpoint to retrieve data about a random course:
We can also use a web browser to interact with the API endpoint directly without running the script. Enter the URL of the API endpoint in the address bar. In our case, it would be http://localhost:8000/api/random/ and Press Enter to send the GET request to our RESTful API endpoint. Try refreshing the browser several times. Pressing the refresh button in the browser will trigger a new GET request to the API server, resulting in the retrieval and display of potentially different data about a random course in the browser window.
Let's look at out api_random
view again:
from courses.models import Course
def api_random(request, *args, **kwargs):
model_data = Course.objects.order_by("?").first()
data = {}
if model_data:
data['id'] = model_data.id
data['title'] = model_data.title
data['course_id'] = model_data.course_id
data['description'] = model_data.description
data['price'] = model_data.price
return JsonResponse(data)
using model_to_dict
from django.forms.models
can make our code cleaner by reducing redundancy and improving readability. model_to_dict
converts a model instance to a dictionary,
from django.forms.models import model_to_dict
def api_random(request, *args, **kwargs):
model_data = Course.objects.all().order_by("?").first()
data = {}
if model_data:
data = model_to_dict(model_data)
return JsonResponse(data)
Using
model_to_dict
reduces the need for manually specifying each field, which can make the code cleaner, more concise, and less error-prone.
Using the model_to_dict
function we can also explicitly declare which fields we want to use as follows:
from django.forms.models import model_to_dict
def api_random(request, *args, **kwargs):
model_data = Course.objects.all().order_by("?").first()
data = {}
if model_data:
#we can also declare the fields we want to use
data = model_to_dict(
model_data,
fields=[
'course_id',
'title',
'price'
]
)
return JsonResponse(data)
Using the fields
parameter with model_to_dict
allows us to include only the necessary fields from our Django model in the dictionary, giving us control over the data returned by our API endpoints. This can be useful for optimising responses and ensuring that only relevant data is exposed through your APIs:
If you remember looking back at our Courses model we used a @property
decorator for our discount_price
method:
from django.db import models
class Course(models.Model):
title = models.CharField(max_length=120)
course_id = models.CharField(max_length=20)
description = models.TextField(blank=True, null=True)
price = models.DecimalField(
max_digits=15,
decimal_places=2,
default=99.99
)
def __str__(self):
return self.title
@property
def discount_price(self):
"""
Calculate and return the discounted price after applying a 50% discount.
"""
# Define the discount rate
discount_rate = 0.5 # 50% discount
# Calculate the discounted price
discounted_price = float(self.price) * (1 - discount_rate)
# Round the discounted price to two decimal places
discounted_price = round(discounted_price, 2)
# Format the discounted price with two decimal places using f-string
return f"{discounted_price:.2f}"
When we use
@property
, we are defining a getter method. This means that when we access the property, the method gets called automatically to return the value.
Let's say in my view I also want to add discount_price
so that it is included in the JSON response:
from django.forms.models import model_to_dict
def api_random(request, *args, **kwargs):
model_data = Course.objects.all().order_by("?").first()
data = {}
if model_data:
#we can also declare the fields we want to use
data = model_to_dict(
model_data,
fields=[
'course_id',
'title',
'price',
'discount_price' # This is a property, not a database field
]
)
return JsonResponse(data)
You may expect by refactoring my code as shown above and then running the client.py script to see the added field being returned in the JSON, but this does not happen!
The reason why the discount_price
property is not being included in the JSON response when using model_to_dict
is that model_to_dict
does not include model properties by default. It only includes the fields that are directly stored in the database. Properties, like discount_price
, are methods defined on the model class and are not part of the database schema.
This is one of the limitations of trying to manually implement a RESTFul endpoint without using the Django Rest Framework's serializers.
The
model_to_dict
function is designed to convert model instances into dictionaries, but it only includes fields that are defined in the model's database schema. Properties and methods are not included because they are not part of the actual data stored in the database.
Without DRF's serializers, you need to manually convert your model instances to dictionaries and ensure that all necessary fields, including properties created with the @property
decorator, are included in the dictionary. This method is more manual and error-prone than using DRF serializers.
Using Django Rest Framework (DRF) serializers can significantly simplify the process of including custom properties like discount_price
in your serialized output. DRF serializers are designed to handle such cases elegantly and provide a more structured and maintainable approach.
In the next part of this tutorial, we will start exploring the Django Rest Framework and see how DRF helps us avoid the need to manually add custom properties, and help us create robust, maintainable APIs.
Resources