Thursday, April 29, 2021

Stack Abuse: Creating a REST API in Python with Django

Introduction

Django is a powerful Python Web Framework used to build secure, scalable web applications rapidly with fewer efforts. It became popular because of its low barrier to entry, and strong community that uses and develops the framework.

In this guide, we are going to build a RESTful API using Django without any external libraries. We will cover the basics of Django and implement a JSON-based API to perform CRUD operations for a shopping cart application.

What is a REST API?

REST (Representational State Transfer) is a standard architecture for building and communicating with web services. It typically mandates resources on the web are represented in a text format (like JSON, HTML, or XML) and can be accessed or modified by a predetermined set of operations. Given that we typically build REST APIs to leverage with HTTP instead of other protocols, these operations correspond to HTTP methods like GET, POST, or PUT.

An API (Application Programming Interface), as the name suggests, is an interface that defines the interaction between different software components. Web APIs define what requests can be made to a component (for example, an endpoint to get a list of shopping cart items), how to make them (for example, a GET request), and their expected responses.

We combine these two concepts to build a REST(ful) API, an API that conforms to the constraints of the REST architectural style. Let's go ahead and make one, using Python and Django.

Setting up Django and Our Application

As mentioned earlier, Django is a Web Framework that promotes the rapid development of secure and scalable web services.

Note: We'll be using Django version 3.1, as that's the latest version as of writing.

Before installing Django, for good measure and in the name of isolating dependencies - let's make a virtual environment:

$ python3 -m venv env

On some code editors, you will find it already activated. If not, you can go to the scripts directory inside the environment and run activate.

On Windows:

$ env\scripts\activate

On Mac or Linux:

$ . env/bin/activate

Now, let's go ahead and install Django via pip:

$ pip install django

Once installed, we can create our project. While you can o it manually, it's much more convenient to start off with a skeleton project through Django itself.

The django-admin tool allows us to spin off a blank, skeleton project that we can start working on immediately. It comes bundled with Django itself, so no further installation is necessary.

Let's start the project by invoking the tool, as well as the startproject command, followed by the project's name:

$ django-admin startproject shopping_cart

This creates a simple skeleton project in the working directory. Each Django project can contain multiple apps - though, we'll be making one. Let's call on the manage.py file, created via the startproject command to spin up an application:

$ cd shopping_cart
$ python manage.py startapp api_app

Once created, our project structure will look something along the lines of:

> env
> shopping_cart
  > api_app
    > migrations
    __init__.py
    admin.py
    apps.py
    models.py
    tests.py
    views.py
  > shopping_cart
    __init__.py
    asgi.py
    settings.py
    urls.py
    wsgi.py
  manage.py

The top-level shopping_cart is the Django root directory and shares the name with the project name. The root directory is the bridge between the framework and the project itself and contains several setup classes such as manage.py which is used to spin up applications.

The api_app is the application we're spinning up, and there can be many here. Each has a few default scripts that we'll be modifying to accomodate for the CRUD functionality.

The low-level shopping-cart is the project directory, which contains settings-related files such as settings.py that will house all of our application's properties.

Django is an Model-View-Controller (MVC). It's a design pattern that separates an application into three components: the model which defines the data being stored and interacted with, the view which describes how the data is presented to the user, and the controller which acts as an intermediary between the model and the view. However, Django's interpretation of this pattern is slightly different from the standard interpretation. For example, in a standard MVC framework, the logic that processes HTTP requests to manage shopping cart items would live in the controller.

In Django, that logic resides in the file containing views. You can read more about their interpretation here. Understanding the core MVC concept as well as Django's interpretation makes the structure of this application easier to understand.

Each Django project comes pre-installed with a few Django applications (modules). These are used for authentication, authorization, sessions, etc. To let Django know that we'd also like to include our own application, api_app, we'll have to list it in the INSTALLED_APPS list.

Let's list it by going to the settings.py file and modifying the list to include our own app:

...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api_app',
]

Once listed, we're done with the project setup. Though, we're not done with the foundations on which our app will be built. Before developing the CRUD functionality, we'll need a model to work with as our basic data structure.

Note: Starting a Django project will, by default, also prepare an SQLite database for that project. You don't need to set it up at all - just defining models and calling the relevant CRUD functions will kick off a process under the hood that does everything for you.

Defining a Model

Let's start off with a simple, basic model - the CartItem, that represents an item listed on a fictional eCommerce website. To define models that Django can pick up - we modify the api_app/models.py file:

from django.db import models

class CartItem(models.Model):
    product_name = models.CharField(max_length=200)
    product_price = models.FloatField()
    product_quantity = models.PositiveIntegerField()

Here, we're leveraging the built-in db module, which has a models package within it. The Model class represents, well, a model. It has various fields such as CharField, IntegerField, etc. that are used to define the schema of the model in the database. These are mapped, under the hood, by Django's ORM when you want to save an instance of a model into the database.

Various fields exist, and they're designed to work well with relational databases, which we'll be doing here as well. For non-relational databases, though, it doesn't work very well due to an inherent difference in how data is stored.

If you'd like to work with a non-relational database, such as MongoDB - check out our Guide to Using The Django MongoDB Engine.

To make changes to model schemas, like what we just did, we have to call on Django Migrations. Migrations are pretty effortless to do, though, you'll have to run them each time you want to persist a change in the schema.

To do that, we'll call on the manage.py file, and pass in makemigrations and migrate arguments:

$ python manage.py makemigrations  # Pack model changes into a file
$ python manage.py migrate  # Apply those changes to the database

The migrate operation should result in something like this:

Operations to perform:
  Apply all migrations: admin, api_app, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying api_app.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

If your migrations did not run successfully, please review the previous steps to ensure that you're set up correctly before continuing.

The Django Admin Site

When creating applications, Django automatically creates an admin-site, intended to be used by the developer to test things out, and give them access to forms generated for registered models. It's only meant to be used as a handy dashboard during development - not as the actual administration dashboard, which you'd create separately if you want to have one.

The admin module of django.contrib is the package that allows us to customize the admin-site.

To leverage the automatic form-creation and model management of Django, we'll have to register our model in the admin.site.

Let's go to api_app/admin.py and register our model:

from django.contrib import admin
from .models import CartItem

admin.site.register(CartItem)

Now, we'll want to create a user that's able to access this dashboard and use it. Let's create a superadmin account and confirm that all these changes were made successfully:

$ python manage.py createsuperuser

To create an account, you'll have to provide a username, email, and password. You can leave the email blank. Password is not reflected when you type. Just type and hit enter:

Username (leave blank to use 'xxx'): naazneen
Email address:
Password:
Password (again):
Superuser created successfully.

Finally, let's run our application to see if things are working as intended:

$ python manage.py runserver

The application is started on our localhost (127.0.0.1) on port 8000 by default. Let's naviate a browser to http://127.0.0.1:8000/admin:

Admin page after successful Django setup

Now that our application and database models are set up, let's focus on developing the REST API.

Creating a REST API in Django

The Django application is all set - the settings are defined, our application is prepared, the model is in place and we've created an administrator user to verify that the model is registered to the admin dashboard.

Now, let's implement the CRUD functionality for our model.

Creating Entities - The POST Request Handler

POST requests are used to send data to the server. Typically, they contain data in their body that's supposed to be stored. When filling out forms, uploading images or submitting a message - POST requests are sent with that data, which is then accordingly handled and saved.

Let's create a Django view to accept data from the client, populate a model instance with it, and add it to the database. Essentially, we'll be able to add an item to our shopping cart with our API. Views in Django can be written purely as functions or as methods of a class. We are going to use Class-Based Views.

To add a view, we'll modify the api_app_views.py file, and add a post() method that receives a POST request. It'll write the incoming request body into a dictionary and create a CartItem object, persisting it in the database:

from django.views import View
from django.http import JsonResponse
import json
from .models import CartItem

class ShoppingCart(View):
    def post(self, request):

        data = json.loads(request.body.decode("utf-8"))
        p_name = data.get('product_name')
        p_price = data.get('product_price')
        p_quantity = data.get('product_quantity')

        product_data = {
            'product_name': p_name,
            'product_price': p_price,
            'product_quantity': p_quantity,
        }

        cart_item = CartItem.objects.create(**product_data)

        data = {
            "message": f"New item added to Cart with id: {cart_item.id}"
        }
        return JsonResponse(data, status=201)

Using the json module, we've decoded and parsed the incoming request's body into an object we can work with, and then extracted that data into variables p_name, p_price and p_quantity.

Finally, we've created a product_data dictionary to hold our fields and their values, and persisted a CartItem to our database, via the create() method of the Model class, filling it with our product_data.

Note the use of the JsonResponse class at the end. We use this class to convert our Python dictionary to a valid JSON object that is sent over HTTP back to the client. We set the status code to 201 to signify resource creation on the server-end.

If we run our application and tried to hit this endpoint, Django would reject the request with a security error. By default, Django adds a layer of protection for Cross-site request forgery (CSRF) attacks. In practice, This token is stored in our browser's cookies and is sent with every request made to the server. As this API will be used without a browser or cookies, the requests will never have a CSRF token. Therefore, we have to tell Django that this POST method does not need a CSRF token.

We can achieve this by adding a decorator to the dispatch method of our class which will set the csrf_exempt to True:

...
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCart(View):

    def post(self, request):
        data = json.loads(request.body.decode("utf-8"))
        ...

Now, we have the models that store our data, and a view that can create a new cart item, via an HTTP request. The only thing left to do is tell Django how to treat URLs and their respective handlers. For each accessed URL, we'll have an adequate view mapping that handles it.

It's considered good practice to write respective URLs in each app, and then include them into the project's urls.py file, rather than having them all on the top-level from the get-go.

Let's begin by amending the project's urls.py, in the shopping_cart directory:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('api_app.urls')),
]

Be sure to import the include library from django.urls, it's not imported by default.

The first argument is the string path, and the second is where we're getting the URLs from. With our path being '', or empty, it means that our API's URLs will be the root path of the web app.

We now need to add the endpoints for our API app's urls.py. In the api_app folder, we'll create a new file called urls.py:

from django.urls import path
from .views import ShoppingCart

urlpatterns = [
    path('cart-items/', ShoppingCart.as_view()),
]

Similar to the project's own urls.py, the first argument is the subpath where our views would be accessible, and the second argument is the views themselves.

Finally, we can run the application. Like before, we'll be utilizing the manage.py file, and pass in the runserver argument:

$ python manage.py runserver

Let's open a terminal and send a POST request to our endpoint. You can use any tool here - from more advanced tools like Postman, to simple CLI-based tools like curl:

$ curl -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/car
t-items/ -d "{\"product_name\":\"name\",\"product_price\":\"41\",\"product_quantity\":\"1\"}"

If all works well, you'll be greeted with a message:

{
    "message": "New item added to Cart with id: 1"
}

If you visit http://127.0.0.1:8000/admin/api_app/cartitem/, the cart item we've added will also be listed.

Retrieving Entities - The GET Request Handler

Let's make a handler for GET requests, which are typically sent by clients when they'd like to receive some information. Since we have a CartItem saved to the database, it makes sense that someone would want to retrieve information about it.

Assuming the possibility of more than one item, we'll iterate over all the CartItem entries and add their attributes into a dictionary - which is easily converted into a JSON response for the client.

Let's modify the ShoppingCart view:

...
@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCart(View):

    def post(self, request):
        ...

    def get(self, request):
        items_count = CartItem.objects.count()
        items = CartItem.objects.all()

        items_data = []
        for item in items:
            items_data.append({
                'product_name': item.product_name,
                'product_price': item.product_price,
                'product_quantity': item.product_quantity,
            })

        data = {
            'items': items_data,
            'count': items_count,
        }

        return JsonResponse(data)

The count() method counts the number of occurrences in the database, while the all() method retrieves them into a list of entities. Here, we extract their data and return them as a JSON response.

Let's send a GET request to our the endpoint:

$ curl -X GET http://127.0.0.1:8000/cart-items/

This results in a JSON response to the client:

{
    "items": [
        {
            "product_name": "name",
            "product_price": 41.0,
            "product_quantity": 1
        },
    ],
    "count": 1
}

Updating Entities - The PATCH Requesst Handler

We can persist and retrieve data via our API, though, it's equally as important to be able to update already persisted entities. The PATCH and PUT requests come into play here.

A PUT request entirely replaces the given resource. Whereas, a PATCH request modifies a part of the given resource.

In the case of PUT, if the given resource context does not exist, it will create one. To perform a PATCH request, the resource must already exist. For this application, we only want to update a resource if one exists already, so we'll be using a PATCH request.

The post() and get() methods are both located in the same ShoppingCart view class. This is because they affect more than one shopping cart item.

A PATCH request only affects one cart item. So we'll create a new class that will contain this view, as well as the future delete() method. Let's add a PATCH request handler to api_app/views.py:

...
@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCartUpdate(View):

    def patch(self, request, item_id):
        data = json.loads(request.body.decode("utf-8"))
        item = CartItem.objects.get(id=item_id)
        item.product_quantity = data['product_quantity']
        item.save()

        data = {
            'message': f'Item {item_id} has been updated'
        }

        return JsonResponse(data)

We'll be retrieving the item with a given ID, and modifying it before saving it again.

Since we don't want the customer to be able to change the product price or name, we're only making the quantity of the item in the shopping cart variable in size. Then, we're calling the save() method to update the already existing entity in the database.

Now, we need to register an endpoint for this as well, just like we did for the cart-items/ endpoint:

api_app/urls.py:

from django.urls import path
from .views import ShoppingCart, ShoppingCartUpdate


urlpatterns = [
    path('cart-items/', ShoppingCart.as_view()),
    path('update-item/<int:item_id>', ShoppingCartUpdate.as_view()),
]

This time around, we're not only relying on the HTTP verb. Last time, sending a POST request to /cart-items resulted in the post() method being called, while sending a GET request resulted in the get() method being run.

Here, we're adding a URL-variable - /<int:item_id>. This is a dynamic component in the URL, which is mapped to the item_id variable from the view. Based on the provided value, the appropriate item is retrieved from the database.

Let's send a PATCH request to http:127.0.0.1:8000/update-item/1 with the appropriate data:

$ curl -X PATCH http://127.0.0.1:8000/update-item/1 -d "{\"product_quantity\":\"3\"}"

This results in a JSON response:

{
    "message": "Item 1 has been updated"
}

Let's also verify this via the Admin Panel at: http://127.0.0.1:8000/admin/api_app/cartitem/1/change/.

Deleting Entities - The DELETE Request Handler

Finally, the last piece of the CRUD functionality - removing entities.

To remove the item from the cart, we'll use the same ShoppingCartUpdate class as it only affects one item. We will add the delete() method to it which takes the ID of the item we'd like to remove.

Similar to how we save() the item when updating it with new values, we can use delete() to remove it. Let's add the delete() method to the api_app/views.py:

...
@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCartUpdate(View):

    def patch(self, request, item_id):
        ...

    def delete(self, request, item_id):
        item = CartItem.objects.get(id=item_id)
        item.delete()

        data = {
            'message': f'Item {item_id} has been deleted'
        }

        return JsonResponse(data)

And now, let's send the DELETE request, and provide the ID of the item we'd like to remove:

curl -X "DELETE" http://127.0.0.1:8000/update-item/1

And we will get the following response:

{
    "message": "Item 1 has been deleted"
}

Visiting http://127.0.0.1:8000/admin/api_app/cartitem/ verifies that the item is no longer there.

Conclusion

In this short guide, we've gone over how to create a REST API in Python with Django. We've gone over some of the fundamentals of Django, started a new project and an app within it, defined the requisite models and implemented CRUD functionality.

The complete code for this application can be found here.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...