Building a shopping cart using Django Rest Framework
Tags: django, webdev, programming, python
Published: 2023-08-30 14:29:44

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
```
```
mkdir drf_shopping_cart && cd drf_shopping_cart
```
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
```
```
source env/bin/activate
```
```
pip install django djangorestframework pillow
```
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 .
```
```
python manage.py startapp cart
```
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:
```python
INSTALLED_APPS = [
    ....
    'cart',
    'rest_framework',
]
```

## 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:
```python
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)

```
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
```
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:

```python
import os

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
```
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:
```python
#...
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)
    
```
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:
```python
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = "__all__"
```
Next open up `cart/views.py` and add the following code:

```python
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
            )

```
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:
```python
#...
from cart.views import ProductAPI

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

]
#...
```
We are ready to add new products to the database so spin up the server by:
```
python manage.py runserver
```
Visit http://localhost:8000/products and add a couple of products.

![products api](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxaml3q81a1vyvov1sjm.png)

## 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:

```python

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()

```
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.
```python
CART_SESSION_ID = 'cart
```
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:
```python
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)

```
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:
```python
#...
from cart.views import CartAPI

urlpatterns = [
    #...
    path('cart', CartAPI.as_view(), name='cart'),
    #...
]
#...
```

## 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:
```js
{
  "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
}
```
Now on get request we should see the following screen to show that the addition worked.

![api](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/49envttgywnjeab2gnq0.png)
To remove a specific product from the cart the payload to send would look like:
```js
{
  "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
}
```
And to clear the cart the payload would be simply:
```js
{
  "clear": true
}
```
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](https://twitter.com/NicksonLangat20) or my [linkedin](https://www.linkedin.com/in/langat-nickson-26081119a/) or my [website](https://techwithnick.com/) and my email is nicksonlangat95@gmail.com. 

The code used in this tutorial can be [found here.](https://github.com/nicksonlangat/drf_shopping_cart.git)
Bye for now and see you on the next one. Cheers 🥂