DEV Community

Cover image for Building a shopping cart using Django Rest Framework
Nick Langat
Nick Langat

Posted on

Building a shopping cart using Django Rest Framework

Hey there and welcome to yet another article where I will walk you through an exciting concept which is an integral piece of most, if not all, e-commerce websites and applications.
The shopping cart or basket is very crucial in that it allows customers to pick the products they want to purchase akin to real life where we use a trolley or a basket to hold the various items we would like to purchase. Similarly, we accomplish the same task virtually using a shopping cart.

This tutorial aims to walk you through a step by step process of building such shopping cart logic while leveraging the power of Django and Django Rest Framework. Without further do let's dive in, shall we?

PROJECT SET UP

We will start by navigating to a folder on our workspace where we will create the project folder. For me, that is the Desktop folder, so open a terminal window and navigate to that folder:

cd desktop
Enter fullscreen mode Exit fullscreen mode
mkdir drf_shopping_cart && cd drf_shopping_cart
Enter fullscreen mode Exit fullscreen mode

Above, we are navigating into the desktop folder on our machine and then creating a folder called drf_shopping_cart and then navigating into it as well. Next let's issue the following commands which I explain shortly:

virtualenv env
Enter fullscreen mode Exit fullscreen mode
source env/bin/activate
Enter fullscreen mode Exit fullscreen mode
pip install django djangorestframework pillow
Enter fullscreen mode Exit fullscreen mode

Here, we are creating a virtual environment to isolate our project's dependencies from the system wide configurations and settings and then we activate it and finally install our two main dependencies that is Django and DRF and the third dependency Pillow that is used for image processing in Python.
With those out of the way, issue the following commands which I explain in a bit:

django-admin startproject mysite .
Enter fullscreen mode Exit fullscreen mode
python manage.py startapp cart
Enter fullscreen mode Exit fullscreen mode

So the first command is used to create a new project and then the startapp command is used to create a new app inside our project. The cart app is where we shall spend a lot of our time writing code. The next vital step is to add our newly created cart app alongside DRF to the list of installed apps in the settings.py so go ahead and add it like so:

INSTALLED_APPS = [
    ....
    'cart',
    'rest_framework',
]
Enter fullscreen mode Exit fullscreen mode

SETTING UP THE MODELS

Now if you think about it, we need a database table that will contain product information like name, price, image e.t.c. We can achieve that by using a Django model such that we will write Python code as a class and this will be translated to an SQL table thanks to the built-in Django ORM.
Head over to cart/models.py and add the following code:

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=150)
    description = models.TextField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
    is_available = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    def __str__(self) -> str:
        return str(self.name)

Enter fullscreen mode Exit fullscreen mode

There we have defined a few table fields but feel free to add more as you see fit. To make sure that our database has this captured, we need to run the following commands to sync apply the changes:

python makemigrations && python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

At this point our database is ready.
Now, because we will handle product images we need to add a few more settings so that we do not run into errors and frustrations. First up open mysite/settings.py file and at the bottom add the following:

import os

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
Enter fullscreen mode Exit fullscreen mode

We are setting the MEDIA_ROOT, that is the absolute path to the directory where our application will serve the images from
and then we are then setting MEDIA_URL which is the url that will handle the images served from media root.

Next open mysite/urls.py and add the following code:

#...
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
    #...
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Enter fullscreen mode Exit fullscreen mode

At this point we are ready to move to the next chapter.

POPULATING THE DATABASE WITH PRODUCTS

Now we need to populate the database table for products with some data that we can use to test the shopping cart functionality.
We will need to create an endpoint that allows us to create and list products and to achieve this, we will create a serializer class to convert complex data types to native Python equivalents that can then be rendered as JSON.
Next we will create a view that will allow us to write into and read from the database.

Inside the cart folder create a file called serializers.py and add the following code:

from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = "__all__"
Enter fullscreen mode Exit fullscreen mode

Next open up cart/views.py and add the following code:

from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from .serializers import ProductSerializer
from .models import Product

class ProductAPI(APIView):
    """
    Single API to handle product operations
    """
    serializer_class = ProductSerializer

    def get(self, request, format=None):
        qs = Product.objects.all()

        return Response(
            {"data": self.serializer_class(qs, many=True).data}, 
            status=status.HTTP_200_OK
            )

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(
            serializer.data, 
            status=status.HTTP_201_CREATED
            )

Enter fullscreen mode Exit fullscreen mode

We have created a single endpoint that will allow us to perform a GET request to retrieve all products in the database and a POST request to save new products into the database.
Next open mysite/urls.py and add the following code:

#...
from cart.views import ProductAPI

urlpatterns = [
    #...
    path('products', ProductAPI.as_view(), name='products'),

]
#...
Enter fullscreen mode Exit fullscreen mode

We are ready to add new products to the database so spin up the server by:

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:8000/products and add a couple of products.

products api

BUILDING THE SHOPPING CART

At this point we already have a product catalog and now we need to implement a shopping cart that allows customers to add any product they want while browsing the online shop.
The shopping cart should store whatever products it has temporarily while the customers browse the site until they place an order.

For this, we will make use of Django's session framework to persist the cart and keep it in session till an order is placed or the session expires.

Django's session framework supports both anonymous and user sessions and the data stored is unique to each site visitor. When we created the project, Django added a session middleware django.contrib.sessions.middleware.SessionMiddleware to manage all sessions in the project. This middleware avails the current session in the request object as a dictionary thus we can access it by calling request.session and it is JSON serializable.

Enough literature, lets's write some code :). Here is what we are about to do:

  • Create a cart service to handle cart operations
  • Create an API endpoint that calls the cart service
  • Test every cart operation

As mentioned above we need to create a service to put the logic for the cart. Inside the cart folder create a file called service.py and add the following code:


from decimal import Decimal

from django.conf import settings

from .serializers import ProductSerializer
from .models import Product


class Cart:
    def __init__(self, request):
        """
        initialize the cart
        """
        self.session = request.session
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            # save an empty cart in session
            cart = self.session[settings.CART_SESSION_ID] = {}
        self.cart = cart

    def save(self):
        self.session.modified = True

    def add(self, product, quantity=1, overide_quantity=False):
        """
        Add product to the cart or update its quantity
        """

        product_id = str(product["id"])
        if product_id not in self.cart:
            self.cart[product_id] = {
                "quantity": 0,
                "price": str(product["price"])
            }
        if overide_quantity:
            self.cart[product_id]["quantity"] = quantity
        else:
            self.cart[product_id]["quantity"] += quantity
        self.save()

    def remove(self, product):
        """
        Remove a product from the cart
        """
        product_id = str(product["id"])

        if product_id in self.cart:
            del self.cart[product_id]
            self.save()

    def __iter__(self):
        """
        Loop through cart items and fetch the products from the database
        """
        product_ids = self.cart.keys()
        products = Product.objects.filter(id__in=product_ids)
        cart = self.cart.copy()
        for product in products:
            cart[str(product.id)]["product"] = ProductSerializer(product).data
        for item in cart.values():
            item["price"] = Decimal(item["price"]) 
            item["total_price"] = item["price"] * item["quantity"]
            yield item

    def __len__(self):
        """
        Count all items in the cart
        """
        return sum(item["quantity"] for item in self.cart.values())

    def get_total_price(self):
        return sum(Decimal(item["price"]) * item["quantity"] for item in self.cart.values())

    def clear(self):
        # remove cart from session
        del self.session[settings.CART_SESSION_ID]
        self.save()

Enter fullscreen mode Exit fullscreen mode

Now let's go through the code above since a lot is happening therein.
We defined a Cart class that allows us to manage the shopping cart.

The __init__ method is our constructor and requires a request object to initialise the cart. We also store the current session making it accessible to the other methods of the cart class. As seen, we try to get the cart from the current session and if it is missing we create an empty cart by setting an empty dictionary in the session.

The add method is our next one and this is used to add products to the cart or update their quantity. It takes product, quantity, override_quantity as parameteres. The product ID acts as the key to the dictionary while the quantity and price figures become the value of the dictionary.

The save method simply marks the session as modified and thus Django knows that the session changed and needs to be saved

The remove method does exactly that, removes a given product from the cart. It also calls the save method to make sure everything is saved.

We then defined an __iter__ method which allows us to loop over cart's items and access the related product objects. Here we fetch product instances present in the cart. We will make use of this in our view.

The __len__ method returns the number of total items in the cart.

The get_total_price method calculates the total cost of items in the cart.

Last but not least, the clear method removes the cart object from the session as it deletes the entire dictionary before calling the save method.

Finally we add the following setting to the mysite/settings.py file.

CART_SESSION_ID = 'cart
Enter fullscreen mode Exit fullscreen mode

With all that in place, our Cart class is ready to manage shopping carts.

CREATING SHOPPING CART API VIEWS

Well, we have a Cart class to manage the cart, we now need to create API endpoints to list, add, update, remove items and clear the cart.
With this in mind, let's create the views. Open cart/views.py and add the following code:

class CartAPI(APIView):
    """
    Single API to handle cart operations
    """
    def get(self, request, format=None):
        cart = Cart(request)

        return Response(
            {"data": list(cart.__iter__()), 
            "cart_total_price": cart.get_total_price()},
            status=status.HTTP_200_OK
            )

    def post(self, request, **kwargs):
        cart = Cart(request)

        if "remove" in request.data:
            product = request.data["product"]
            cart.remove(product)

        elif "clear" in request.data:
            cart.clear()

        else:
            product = request.data
            cart.add(
                    product=product["product"],
                    quantity=product["quantity"],
                    overide_quantity=product["overide_quantity"] if "overide_quantity" in product else False
                )

        return Response(
            {"message": "cart updated"},
            status=status.HTTP_202_ACCEPTED)

Enter fullscreen mode Exit fullscreen mode

A lot is going on there so let's break it down. We are sub classing Rest framework's APIView and defining two http methods.

In the get method we send a GET request and initialise a cart object then we call its __iter__ method that will give us back a generator object. We then call the built in list to get a list that we then serialize as part of our response. Also we are returning the cart's total by calling get_total_price.

In the post method we send a POST request and then initialise an instance of the cart passing the request and then we check for the presence of a few things in the request payload.

We check whether we have a key named remove in the payload and if it exists then we call the remove() of the cart passing in the product from the payload.

We then check whether we have a key named clear in the payload and if it exists then we call the clear() of the cart to remove it from the current session.
If we don't have those two keys then we call the add() of the cart passing in the product, quantity and whether or not to override quantity.

At the end of the day we send back a response with a message and status but we can do more than that, be creative. We just have one last step before we test our logic.
Open mysite/urls.py and add the following code to it:

#...
from cart.views import CartAPI

urlpatterns = [
    #...
    path('cart', CartAPI.as_view(), name='cart'),
    #...
]
#...
Enter fullscreen mode Exit fullscreen mode

LET'S TEST THE CART

Ensure that the server is still running. Head over to the cart's url that is http://localhost:8000/cart. Let's make a post call with the follwoing payload:

{
  "product": {
            "id": 1,
            "name": "Macbook Pro",
            "description": "Our most powerful laptops, MacBook Pros are supercharged by M1 and M2 chips. Featuring Magic Keyboard, Touch ID, and brilliant Retina display.",
            "price": "1800.00",
            "image": "/products/2023/08/30/sp809-mbp16touch-silver-2019.jpeg",
            "is_available": true,
            "created_at": "2023-08-30T11:58:54.476688Z",
            "modified_at": "2023-08-30T11:58:54.476720Z"
        },
  "quantity": 5
}
Enter fullscreen mode Exit fullscreen mode

Now on get request we should see the following screen to show that the addition worked.

api
To remove a specific product from the cart the payload to send would look like:

{
  "product": {
            "id": 1,
            "name": "Macbook Pro",
            "description": "Our most powerful laptops, MacBook Pros are supercharged by M1 and M2 chips. Featuring Magic Keyboard, Touch ID, and brilliant Retina display.",
            "price": "1800.00",
            "image": "/products/2023/08/30/sp809-mbp16touch-silver-2019.jpeg",
            "is_available": true,
            "created_at": "2023-08-30T11:58:54.476688Z",
            "modified_at": "2023-08-30T11:58:54.476720Z"
        },
  "remove": true
}
Enter fullscreen mode Exit fullscreen mode

And to clear the cart the payload would be simply:

{
  "clear": true
}
Enter fullscreen mode Exit fullscreen mode

Go ahead and test them out and see what you end up :)

THAT"S A WRAP

Yes that's a wrap! If you made it to the end of the tutorial then a big hugops to you. Thank you for taking time to read and follow along and I hope you learnt a thing or two that will help progress your coding journey.
If you have any thoughts, questions etc then leave them down below.

I am also looking for my next role so if your team is hiring or know anyone who is, then I'll be much obliged if I get a referral.

I am available via Twitter or my linkedin or my website and my email is nicksonlangat95@gmail.com.

The code used in this tutorial can be found here.
Bye for now and see you on the next one. Cheers 🥂

Top comments (2)

Collapse
 
miracool profile image
Makanju Oluwafemi

Great content !

Collapse
 
sabbir2609 profile image
sabbir2609

💚💛