A Minimalistic Modern Django Boilerplate

A Minimalistic Modern Django Boilerplate

November 29 2021

Starting a new Django project is exciting. If we follow the documentations it will go something like this. First, we install Django on a local environment, then we start the project via a django-admin startproject command. Then we hack away. Easy!

Before starting though, we have a bunch of warnings about migrations to apply. We better not migrate yet, because the docs recommend changing the default user model before starting. Also, we are not really sure where would our HTML files go. Finally, we have a database but it says in the docs not to use it in production. So, this also has to be changed before hacking away.

Okay, fine. We will make all these changes before hacking away. But, the directions on installing a production database are different for each operating system. So we spend a whole day figuring out how does it work. Also, I got a new M1 mac laptop recently, does it even work there yet?

After a while, hacking away doesn't seem so fun anymore. There are so many things to tweak and adjust. This is clearly a problem. This boilerplate attempts to fix this.

In this article, we will go over the process of building a minimalistic, modern Django boilerplate. If you just want to use it and don't need to see how we built it, then simply go to the how to use this boilerplate section.

We could use this boilerplate in several ways. First, next time we want to hack away, we will simply clone this boilerplate and start working immediately. Also, I plan to use it as a starter code for future blog articles. Lastly, it could also be extended for full use in production or to quickly experiment with new Django ideas. 

We are doing this because Django is more than 15 years old. While its default settings are reasonable, many developers choose to override them with more modern patterns.

Also, there are fully featured boilerplates and amazing CLI tools out there. They are by nature, opinionated. This is not the goal here. This boilerplate has a minimal amount of code with minimal opinions.

The idea is once you build and understand a basic example, you could swap parts of it with your own preferences, customize more complicated options, or simply build on top of this one. This is simply meant to replace the default startproject Django code.

Here is what we will cover:

Here is what you will need before starting:

  • Python installed on your system with an understanding of basic syntax
  • Comfortable with a modern IDE such as VS Code
  • Familiarity with basic Git commands & Python package managers
  • Familiarity with the command line
  • Docker installed on your system. You don't need to know how it works yet. You can install here.

So let's get started!


Django default directory structure

Starting a Django project with the default command (django-admin startproject project_name)  and then starting 3 applications will give us this directory structure. 

project_name/
    manage.py
    project_name/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py
    app_1/
    	__init__.py
    	admin.py
    	apps.py
    	migrations/
        	__init__.py
    	models.py
    	tests.py
    	views.py
     app_2/
     app_3/

The default structure works and is reasonable. I adjust it for the following reasons though:

  1. Two directories with the "project_name" are confusing, especially for beginners!
  2. The inner project_name directory is really meant to be a configuration application, not the whole project.
  3. There is no HTML templates folder. The default behavior expects a different template folder for each application.
  4. Having a template folder for each application doesn't add to the modularity of apps, since no two projects share the same design. It complicates the separation of front and backend though.
  5. Our default user model has older UX patterns, and we should definitely change it before starting.

The Boilerplate Directory Structure

For our boilerplate we would need three things. A configuration application that we can use in any project, no directories with the same name, and a separate templates directory.

Here are the steps to get there:

1. Make a folder with your project name and go to it. In our case, the project is named "boilerplate".

mkdir boilerplate
cd boilerplate

2. Install Django in a local environment and activate it. I am using pipenv.

pipenv install Django
pipenv shell

3. Start your Django project

django-admin startproject config .

config is our "project name" in the default structure terminology, but we are treating it as its own separate app that will contain all our configuration files. The "." tells Django not to start a new folder for the project. Rather, to build the project in the folder we are in

4. Confirm that everything works

python manage.py runserver

5. Change where Django looks for templates via the settings file.

#in config/settings.py
#keep everything else the same, only change the "DIRS" line
TEMPLATES = [
  {
 ...
   "DIRS":[str(BASE_DIR.joinpath("templates"))], #previously "DIRS":[]
 ...
    },
]

6. Close the server ( CTRL + C ),  exit the local environment, and make a templates folder

exit
mkdir templates

Following these steps, our boilerplate directory should eventually look like this. Note - we didn't start working on any apps yet, so your directory wouldn't have any.

boilerplate/
    manage.py
    config/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py
    app_1/
    	__init__.py
    	admin.py
    	apps.py
    	migrations/
        	__init__.py
    	models.py
    	tests.py
    	views.py
     app_2/
     app_3/
     templates/

That's it for the first part of our boilerplate. We managed to start a Django new project, change its file structure for naming clarity, and change where our frontend will live.


Docker & PostgreSQL

Docker is a Linux container. It allows our boilerplate applications and database to run the same way on any operating system.

Using Docker makes sense for a boilerplate. Since, the goal is to have something that will run the same way regardless if you have a Mac, Windows, or Linux.

For example, it allows us to use the PostgreSQL database quickly. So we don't have to worry about specific installing instructions for each operating system.

By default, Django uses sqlite3.  This is because it is included in Python, so we don't need to install anything else to support it. But, the Django documentation recommends this.

When starting your first real project, however, you may want to use a more scalable database like PostgreSQL, to avoid database-switching headaches down the road.

And this is exactly what we will do now!

How Docker actually works is outside the scope of this article and we really don't need to understand all the details to use it. I do recommend this course though if you want a deep dive into Docker.

7. Make a Dockerfile

touch Dockerfile

8. Add the following code to the Dockerfile and save it (adapted from here).


FROM python:3
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /code
COPY Pipfile Pipfile.lock /code/
RUN pip install pipenv && pipenv install --system
COPY . /code/

9. Make a docker-compose file

touch docker-compose.yml

10. We will then add the following code to it (adapted from here)


version: "3.9"

services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - 8000:8000
    depends_on:
      - db
  db:
    image: postgres:11
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - "POSTGRES_HOST_AUTH_METHOD=trust"
volumes:
  postgres_data:

11. Build our docker image to make sure everything works.

docker compose up -d --build

12. Now that our application is running on Docker with no issues, we will download any python package we need on it instead of the local environment. So, let's install the psycopg2 package which is a PostgreSQL database adapter for Python.

docker compose exec web pipenv install psycopg2-binary

13. Lastly, we will change the setting file to tell Django to use PostgreSQL database instead of the default sqlite



# in config/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql', #changed from sqlite
        'NAME': 'postgres', #development settings, will change in production
        'USER': 'postgres', #development settings, will change in production
        'PASSWORD': 'postgres', #development settings, will change in production
        'HOST': 'db',
        'PORT': 5432
    }
}

14. We would then rebuild the docker image with pyscopg2 installed

docker compose down
docker compose up -d --build

15. Our boilerplate is now ready for development using Docker and PostgreSQL. You can go ahead and delete db.sqlite3 default file.

Our next part has to do with user accounts. So do not run the database migrations just yet!


User Authentication

Django comes with a user authentication system. It handles user accounts, groups, permissions and cookie-based user sessions out of the box with very little extra code. Awesome, right? Yes and no.

The default Django user authentication system follows older patterns in web development where the UX was still evolving.  And to maintain backward compatibility, not much has changed over the last 15 years. The default user model has several limitations, which means almost all new projects replace it or customize parts of it.

Also, changing the default user system after starting is extremely difficult. So, to overcome these limitations, we will replace the default user model with our own. To do that, we will install an open-source python package called django-allauth. Django-allauth takes the default user authentication system even a step further by implementing  social logins for us and giving us very flexible settings.

16. Install django-allauth

docker compose exec web pipenv install django-allauth

17. Make a Django accounts application. This application will manage our users.

docker compose exec web python manage.py startapp accounts

18. Change the default user model to a custom user, using the AbstractUser Django class.

#in boilerplate/accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    pass

19. In your settings file, add the following

#in boilerplate/config/settings.py
# all auth settings

SITE_ID = 1     #all-auth supports multiple sites

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend", #this how admins will log in to the admin site
    "allauth.account.auth_backends.AuthenticationBackend", #this how users log in
]
LOGIN_REDIRECT_URL = "home"   #change to desired url name
LOGOUT_REDIRECT_URL = "home" #change to desired url name
ACCOUNT_SESSION_REMEMBER = True #remember user via sessions
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False #preferred UX
ACCOUNT_USERNAME_REQUIRED = False #preferred UX
ACCOUNT_AUTHENTICATION_METHOD = "email" 
ACCOUNT_EMAIL_REQUIRED = True #required for email authentication
ACCOUNT_UNIQUE_EMAIL = True #required for email authentication
ACCOUNT_EMAIL_VERIFICATION = "optional" #can use as a welcome email as well
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 5
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 86400
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
AUTH_USER_MODEL = "accounts.CustomUser"
#email will go to console for now, need to change in production 
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 

20. Add the django sites app, since we added a SITE_ID. Also, we will need to add three different all-auth apps that we could potentially use. As well as the accounts app that we just made.

#in boilerplate/config/settings.py
INSTALLED_APPS = [
     …, #keep the same
    "django.contrib.sites",
    "allauth",
    "allauth.account",
    "accounts",
    "allauth.socialaccount"

For social logins, we will need to add ‘allauth.socialaccount' app as well as the app for the provider itself. For example, to start the process for google social login, you will need to add this application to the list (we won't do this for this project though).

allauth.socialaccount.providers.google

21. Add all-auth URLs to your application's URLs.

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

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("allauth.urls")),
]

22. Rebuild the docker image with all-auth installed (every time you install a package, you will need to rebuild the docker image)

docker compose down
docker compose up -d --build

23. Run migrations for the new CustomUser model.

docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate

Now if you go to “http://127.0.0.1:8000/accounts/signup/" - you will see that you have all your signup functionality already built-in for you. You don't actually need to write any additional code for anything authentication related (signing up, logging in and out, password management, etc.). All of it is included for you.

Most people override the default HTML templates to fit the style of the rest of their site. The default django-allauth ones are minimal and plain. We won't do this in this guide though, since we just want a minimal working template. Also, at this point, the URL http://127.0.0.1:8000 is not configured. So, going there will get a Not Found error. We will fix this in a second.

24. The last thing we will do is to register the Customuser in the Django admin for easy future use.

#in boilerplate/accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import CustomUser

admin.site.register(CustomUser)

25. Finally, as noted earlier - the URL path "/" is not configured, so going there will error out which is not a great experience. To fix this, we all add a few lines to our config/urls.py file. 

 

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

from django.http import HttpResponse  #new

#new
def home(request):
    return HttpResponse("Hello, world. This is a django boilerplate!")


urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("allauth.urls")),
    path("", home, name="home"), #new
]

That's it. Our minimal, modern Django boilerplate is now ready for use. You can explore the code here: https://github.com/Jonathan-Adly/django-boilerplate 

How to use this boilerplate

  1. Git clone https://github.com/Jonathan-Adly/django-boilerplate
  2. docker compose up -d --build
  3. docker compose exec web python manage.py migrate
  4. Hack away

Don't forget to remove the "home" function in config/urls.py and use your own (step 25). Now, to start a new project - a personal blog for example, run 

docker compose exec web python manage.py startapp blog

Replace blog with whatever the name of the project you are starting. Also, to run any of the Django commands or pipenv install new packages you have to start with "docker compose exec web" - where exec stands for execute and web is the name of the service you defined in the docker-compose.yml file.


Conclusion:

In this tutorial we built a minimal, modern Django boilerplate. We can use this boilerplate for quick project setup for tutorials, experimenting with new development ideas, and even extend it for full production usage.

Get notified about new HTMX/Django tutorials
You will only get an email whenever a post or a course is published!

Copyright © HTMX-Django 2021.

Built by Jonathan Adly. Contact for opportunities.