Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Django in Production

You're reading from   Django in Production Expert tips, strategies, and essential frameworks for writing scalable and maintainable code in Django

Arrow left icon
Product type Paperback
Published in Apr 2024
Publisher Packt
ISBN-13 9781804610480
Length 348 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Arghya Saha Arghya Saha
Author Profile Icon Arghya Saha
Arghya Saha
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. Part 1 – Using Django and DRF to Build Modern Web Application
2. Chapter 1: Setting Up Django with DRF FREE CHAPTER 3. Chapter 2: Exploring Django ORM, Models, and Migrations 4. Chapter 3: Serializing Data with DRF 5. Chapter 4: Exploring Django Admin and Management Commands 6. Chapter 5: Mastering Django Authentication and Authorization 7. Part 2 – Using the Advanced Concepts of Django
8. Chapter 6: Caching, Logging, and Throttling 9. Chapter 7: Using Pagination, Django Signals, and Custom Middleware 10. Chapter 8: Using Celery with Django 11. Chapter 9: Writing Tests in Django 12. Chapter 10: Exploring Conventions in Django 13. Part 3 – Dockerizing and Setting Up a CI Pipeline for Django Application
14. Chapter 11: Dockerizing Django Applications 15. Chapter 12: Working with Git and CI Pipelines Using Django 16. Part 4 – Deploying and Monitoring Django Applications in Production
17. Chapter 13: Deploying Django in AWS 18. Chapter 14: Monitoring Django Application 19. Index 20. Other Books You May Enjoy

Creating RESTful API endpoints with DRF

The most popular and widely used API is the REST API. Throughout this book, we shall be working with the REST API. REST has been around for more than two decades, and every company has its interpretation and implementation. In the following section, we shall try to put all the best practices used in the industry into practice.

Opinionated note

The RESTful API is not a protocol; instead, it is a standard convention that developers follow. There is no right or wrong while designing RESTful APIs. Since there is no enforced standard, the details we will provide are purely opinionated and come from my past experiences. You are free to pick the points you like and leave out the things that you feel are not relevant to your implementations.

Best practices for defining RESTful APIs

Let’s look at a few generic good practices that developers use in the industry while defining RESTful endpoints:

  • Using nouns instead of verbs in endpoint paths using appropriate HTTP request methods. Here are some examples (please note that the URL example used here is just an outline of how we should define our REST URLs and that we are not defining the exact code):
    # To get all blogs
    Avoid GET /get-all-blogs, rather use GET /blogs
    # To delete a particular blog
    Avoid POST /delete-blog rather use DELETE /blogs/<blogId>
    # To create a new blog with POST request
    Avoid POST /create-new-blog rather use POST /blogs
    # To update an existing blog with a PUT request
    Avoid PUT /update-blog rather use PUT /blogs/<blogId>
  • Using the appropriate HTTP method is preferred to perform CRUD operations. There are multiple HTTP methods present, but we shall only cover the top five commonly used methods:
    • GET: To retrieve an entity, be it a list or detail
    • POST: To create any new entity
    • PUT: To Update an entity
    • PATCH: To partially update an entity
    • DELETE: To delete an entity
  • It is preferred to create plural nouns in the endpoint. When you have to get a single entry, then use id after the endpoint to retrieve the information. For example, to get a list of blogs, use GET /blogs, and to get the details of one blog, use GET /blogs/<blog id>.
  • Using a logical nested structure for an endpoint is important to clean the API interface and maintain a good information architecture. For example, to get all the comments for a particular blog, the API should be GET /blogs/<blog id>/comments.
  • Versioning the API is important since it helps support legacy systems without breaking the contract in newer systems. Examples of this are /v1/blogs/ and /v2/blogs. We will learn more about this later, in the Using API versioning section.
  • Servers should send appropriate HTTP response status codes as per the action, along with the message body (if applicable). Here are a few of the most widely used HTTP status codes:
    • 2xx: Used for any success. For example, 200 is for any request responding with the data successfully, 201 is for creating a new entry, and so on.
    • 3xx: Used for any redirection.
    • 4xx: For any error. For example, use 400 for bad requests and 404 for requested data not found.
    • 5xx: When the server crashes due to any unexpected request or the server is unavailable.
  • The server must accept and respond with a JSON response. The API will not support other data types, such as plain text, XML, and others.

Best practices to create a REST API with DRF

DRF is a framework that helps us create the REST endpoint faster. It’s the responsibility of the developer to write scalable and maintainable code while following the best practices. Let’s look at some best practices that we can implement using DRF.

Using API versioning

Creating versions of an API is probably the most important thing to follow when working with clients whose updates are not under your control. An example of this is working on a mobile app. Once an end user installs a given mobile app, with a given API integrated, we have to support the given API until the end user updates the mobile app version with the newer API.

While creating an endpoint, a developer should consider all the future requirements possible, along with all the corner cases. However, just like it is not possible to predict the future, a developer cannot always foresee how the current API design might have to be redesigned. A redesign would mean breaking the contract between the client and the server. This is when the importance of API versioning comes into the picture. A well-versioned API will implement a new contract without breaking any of the existing clients.

There are multiple ways to implement API versioning:

  • Accept header versioning: Since the version is passed through the Accept header, whenever there is a new version, the client doesn’t need to update any endpoint whenever a new version is created:
    GET /bookings/ HTTP/1.1
    Host: example.com
    Accept: application/json; version=1.0
  • URL path versioning: The API version is passed through the URL path pattern. This is one of the most widely used API versioning methods due to it providing better visibility:
    GET /v1/bookings/ HTTP/1.1
    Host: example.com
    Accept: application/json
  • Query parameter versioning: The query parameter in the URL contains the version. After URL path-based versioning, this is the second most common versioning method due to its cleaner interface and better discoverability:
    GET /something/?version=1.0 HTTP/1.1
    Host: example.com
    Accept: application/json
  • Host name versioning: This involves using the subdomain to pass the API version in the hostname. This kind of versioning is used when someone migrates the whole service to a newer version rather than single endpoints:
    GET /bookings/ HTTP/1.1
    Host: v1.example.com
    Accept: application/json

DRF supports all four methods of API versioning out of the box and also gives the option to create our custom API version logic if needed. We shall explore URLPathVersioning primarily since it is one of the easiest and most popular ways of implementing versioning using DRF.

In URL path versioning, the API version is passed in the URL path, which makes it easy to identify on both the client and server side. To integrate URLPathVersioning in DRF, add the following in the Django config/settings.py file:

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning'
}

Now, we must add the version to the URL path. It is important to name the URL parameter <version> since DRF is expecting it to be <version> by default. Here, <version> is the URL’s pattern, which means that any URL that matches this pattern shall be linked to the views.

Important note

To learn more about urlpatterns, go to https://docs.djangoproject.com/en/stable/topics/http/urls/.

It is advisable to add <version> at the beginning of the URL, so let’s do that in the main config/urls.py file:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('<version>/demo-app-version/', include('demo_app.urls'))
]

Once we have configured the URL with the <version>, we can try to create a new view and retrieve the version in our view. Add the following code to your demo_app/urls.py file:

from django.urls import path
from demo_app import views
urlpatterns = [
    path('hello-world/', views.hello_world),
    path('demo-version/', views.demo_version),
]

We shall retrieve the API version in the view and return the version in response:

@api_view(['GET'])
def demo_version(request, *args, **kwargs):
    version = request.version
    return Response(data={
        'msg': f'You have hit {version} of demo-api'
    })

Now, when we open http://127.0.0.1:8000/v1/demo-app-version/demo-version/, we should be able to see the following screen:

Figure 1.7: Output showing which version we have hit for the given API

Figure 1.7: Output showing which version we have hit for the given API

If we change the URL to http://127.0.0.1:8000/v9/demo-app/demo-version/, then we’ll see that it returns v9. v9 might not have been released yet, so this might create confusion for the end user hitting the endpoint. To solve this problem, we shall see how we can customize the version class of DRF so that we can add constraints that can help us design better applications.

Using a custom version class with DRF

Let’s see how we can extend the URLPathVersioning class provided by DRF to address the problem we just raised. First, create a file called demo_app/custom_versions.py. This file will have a custom version class for each view, along with a default class for all the views that don’t have multiple versions yet:

from rest_framework.versioning import URLPathVersioning
class DefaultDemoAppVersion(URLPathVersioning):
    allowed_versions = ['v1']
    version_param = 'version'
class DemoViewVersion(DefaultDemoAppVersion):
    allowed_versions = ['v1', 'v2', 'v3']
class AnotherViewVersion(DefaultDemoAppVersion):
    allowed_versions = ['v1', 'v2']

Let’s see what the preceding code does:

  • The DefaultDemoAppVersion class can be used for all the views that are created in demo_app. It has an allowed_versions attribute that lists all the allowed versions that can be used in the URL path whenever we use this class-based view. version_param is the URL path parameter name that we have used to define the version; it can be anything, depending on how you name the parameter, but in our case, we are using <version>, which is used in the config/urls.py file. This class will be used for all the views that are created in the demo app by default until a new version is added, after which we will create an individual class, as shown next.
  • The DemoViewVersion class will contain the list of all the allowed_versions attributes for DemoView that are allowed in the URL path.
  • The AnotherViewVersion class will contain all the versions that are allowed for a different class.

Add the following code to the demo_app/views.py file to integrate the custom version class (note that the custom versioning_class can be only linked to a class-based view, so we are using APIView here):

from rest_framework.response import Response
from rest_framework.views import APIView
from demo_app import custom_versions
class DemoView(APIView):
    versioning_class = custom_versions.DemoViewVersion
    def get(self, request, *args, **kwargs):
        version = request.version
        return Response(data={'msg': f' You have hit {version}'})
class AnotherView(APIView):
    versioning_class = custom_versions.AnotherViewVersion
    def get(self, request, *args, **kwargs):
        version = request.version
        if version == 'v1':
            # perform v1 related tasks
            return Response(data={'msg': 'v1 logic'})
        elif version == 'v2':
            # perform v2 related tasks
            return Response(data={'msg': 'v2 logic'})

Let’s explore the code and understand what is happening under the hood when we use the custom version class:

  • The DemoView class is a class-based APIView where we are passing the allowed versions for the view by the versioning_class attribute. This allows the request object to have a version attribute that is parsed from the URL path. Since we have specified the DemoViewVersion class, this view will only allow the v1, v2, and v3 versions in the URL path. Any other version in the path will result in a 404 response.
  • The AnotherView class is a class-based view where we are passing AnotherViewVersion as the versioning_class attribute. In this view, we are bifurcating the request by checking different versions and responding differently whenever we have a v1 or v2 request.

Now, to link the view logic to the demo_app/urls.py file, add the following code:

urlpatterns = [
    path('hello-world/', views.hello_world),
    path('demo-version/', views.demo_version),
    path('custom-version/', views.DemoView.as_view()),
    path('another-custom-version/', views.AnotherView.as_view())
]

If we go to http://127.0.0.1:8000/v4/demo-app-version/custom-version/ in our browser, we shall see the following error as shown in Figure 1.8, since we have only allowed three versions in our custom versioning_class:

Figure 1.8: 404 error message stating “Invalid version in URL path”

Figure 1.8: 404 error message stating “Invalid version in URL path”

This serves our purpose of only allowing certain versions of the API; any other API version shall result in an error response.

Important note

Custom versioning can only be attached to class-based views. If you don’t pass any custom versioning_class, then Django will pick DEFAULT_VERSIONING_CLASS from the default settings.

Avoid Router

Frameworks such as Ruby on Rails provide the functionality to automatically map requests to a given pattern of URLs, depending on the functionality. DRF borrowed this concept and incorporated it into the framework as the Routers feature. Though this is a wonderful concept to learn and experiment with, developers should avoid it in production since this goes against the principle of Django: “Explicit is better than implicit.”

Django mentions that it should not show too much of the magic. I have personally seen legacy systems where developers added Router and after a couple of months, when a different developer wanted to fix a bug in the view, they were unable to find the corresponding view directly before having the “aha!” moment of identifying the Router concept.

Opinionated note

Avoiding the use of Router is something I have learned the hard way and have seen multiple developers avoid in production. But this is also an opinion that was developed through a bad experience; you can always try to implement it in a better way in your project.

If you want to learn more about Router, you can do so here: https://www.django-rest-framework.org/api-guide/routers/.

With that, we’ve learned how to create RESTful APIs and work with versioning. Now, let’s learn how to work with views using DRF. We mainly write business logic inside views.

You have been reading a chapter from
Django in Production
Published in: Apr 2024
Publisher: Packt
ISBN-13: 9781804610480
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime