How To Use Django as Your SaaS Framework

The Django Framework has been a popular choice for web development with Python for many years. And for good reason: it leverages Python’s ease of modularity to offer a very flexible solution for a variety of applications.

Django’s core features include a model-view-controller architecture, an ORM, a templating system, caching, internationalization, and many other things you’d expect in a modern web framework. “Applications” distributed with the system add features for authentication, an administrative interface which automatically provides CRUD operations for data models, multi-site support, and mitigation from common security threats.

Possibly Django’s greatest strength is that just about all of its features can be extended or overridden. This means you’ll rarely feel restricted when implementing a feature, making it an ideal choice for complex and ever-growing SaaS applications. Here are some tips for making the best use of Django in your next SaaS app.

Keep Your “Apps” Organized

Every system built on top of Django is written as one or more “applications”, as the framework calls them. Each application is a bundle of models, views, templates, and any other custom code needed to run some aspect of the website. The built-in administrative system is one such application. There are plenty of others freely available online. Each is enabled in the settings file and their URL routes can be registered as part of your main configuration.

When building a custom site it may seem natural to write everything into one such application. You’ll quickly find your code growing as you add features. Just as it’s a best practice to separate and organize your code by its responsibility, applications should be broken down in a similar way in Django. This allows each to stay focused on their primary task and also makes them much easier to unit test and debug.

For example, let’s say your site lets users manage subscriptions, has a section for authenticated users to view premium content, and public pages for marketing purposes. I would build each as a separate Django application. Since Django applications are Python modules, they can include functionality from each other as needed. The premium content app can leverage the model defined in the subscription app to check if a user should be allowed access.

Use Django REST Framework for APIs

The Django REST framework (DRF) is a highly flexible and customizable extension to Django for exposing APIs. With very little code it can expose CRUD operations directly against your data model. You can also write your own endpoints that leverage its other features, such as authentication and automatic documentation.

I often write APIs that do not exactly match my data model and require customization. For me the biggest benefits of using Django REST framework over rolling my own solutions are the authentication, caching, and throttling options. The automatic documentation and extensions for things like Swagger are an added bonus.

The small learning curve for DRF makes it well worth the investment for the value it adds.

Throttle APIs Based on User Subscriptions

A common business model of many SaaS applications today includes user subscriptions to APIs. Access to these APIs can be restricted to specific endpoints or rate limited based on subscription levels.

Django REST framework supports both. When adding APIs to project I decided to throttle requests at different levels depending on subscription tier. This is not natively built into the framework. Fortunately just about every aspect of the system can be overridden.

I chose to extend the DRF 

UserRateThrottle

 class:

from rest_framework.throttling import UserRateThrottle

class SubscriptionRateThrottle(UserRateThrottle):
    # Custom scope required by DRF
    scope = "subscription"

    def __init__(self):
        super().__init__()

    def allow_request(self, request, view):
        """
        Override rest_framework.throttling.SimpleRateThrottle.allow_request
        """
        
        if request.user.is_authenticated:
            user_daily_limit = get_user_limit(request.user)
            if user_limits:
                # Override the default from settings.py
                self.duration = 86400
                self.num_requests = user_daily_limit
            else:
                # No limit == unlimited plan
                return True

        # Original logic from the parent method...

        if self.rate is None:
            return True

        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        self.history = self.cache.get(self.key, [])
        self.now = self.timer()

        # Drop any requests from the history which have now passed the
        # throttle duration
        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        if len(self.history) >= self.num_requests:
            return self.throttle_failure()
        return self.throttle_success()

In this example a 

get_user_limit

 function will need to be written to retrieve a daily rate limit from the user’s subscription. This class then needs to be added to the 

DEFAULT_THROTTLE_CLASSES

 array within 

REST_FRAMEWORK

 in 

settings.py

. With just a few dozen lines of code we now have custom API rate throttling!

Cache, Cache, Cache

The key to a fast website is sending responses to the browser as fast as possible. Application level caching should be a component of this strategy. Django includes a caching interface which can be backed by just about anything. I recommend using Redis via django-redis for a few reasons.

Redis has already proven itself as a fast, efficient, and convenient key-value store. As your SaaS system scales it will eventually need multiple servers / containers. By using a shared cache such as Redis across all instances you’ll conserve resources. At launch you might even choose to warm the cache with certain information before the application starts to process requests.

Redis has multiple options for persistence. This means if you need to restart or move your system you do not need to lose all of your cached information.

Django’s cache system offers built-in view level caching. Just about anything else, including data model instances, can be manually cached as well. Carefully leverage this wherever possible for huge performance gains.

Settings for Each Environment

Django loads its configuration from a 

settings.py

 file provided by your application. Secure information such as database passwords and keys can be stored here and should never be committed to your project’s code repository. It’s also very likely developer environments and test systems will need certain settings to differ from production deployments.

There are two ways to handle this. Both are equally convenient and flexible.

One solution is to use environment variables. 

settings.py

 being another Python file can simply inspect the 

environ

 mapping from the built-in 

os

 module. With this method the settings file can still be committed to code and deployed as usual. This plays very nicely with docker deployments which often use environment variables for custom settings.

Another solution is to create a 

settings_base.py

 or similar file for common settings and a custom 

settings.py

 file which imports the base and overrides variables as needed. This may be more or less convenient depending on the details of your deployments to various environments.

Settings to consider for SaaS applications include database parameters, user sessions, static file locations such as a CDN, and the cache configuration.

Conclusion

Python plus Django provides a robust and flexible solution for SaaS applications. They’ve proven their value and have a great community of support. I highly recommend trying it out for your next major project.

Leave a Comment