A cookiecutter for Django Rest Framework projects.
Tags: django, python, webdev
Published: 2022-12-12 06:10:47

## TLDR;
I made a cookiecutter for DRF projects, you can check it out here:
https://github.com/Ohuru-Tech/drf-cookiecutter

## Now, if you do want to read the rest of it:
DRF (Django Rest Framework) is just awesome !! I have been working a lot in DRF and I find myself writing same code multiple times for different projects, it is a tedious process and goes strongly against the DRY (don't repeat yourselves) principle... (I know that applies to a single application, bear with me..). After writing the exact same configuration code for setting up a new project thousands of times, I finally came across this gem:

https://cookiecutter.readthedocs.io/en/stable/

This helped me come up with a template (sort of) which will serve as the starting point for a new Django Rest Framework project. I am writing this post to talk about it's features and urge anyone interested to try and help make it better :) 

So, let's get to it:

## Project outline

### What's included:
* Settings via environment variables with [django-environ](https://django-environ.readthedocs.io/).
* [Celery](http://www.celeryproject.org/) configuration.
* [Django REST Framework](https://www.django-rest-framework.org/) configuration + Swagger with [drf-yasg](https://drf-yasg.readthedocs.io/).
* Typical user CRUD APIs.
* Code formatting with [Black](https://black.readthedocs.io/).
* Imports sorting with [isort](https://isort.readthedocs.io/).
* [Pre-commit](https://pre-commit.com/) hook for running test & linters on each commit.
* Custom user model
* Custom documentation with [drf-yasg](https://drf-yasg.readthedocs.io/).
* Authentication setup using [dj-rest-auth](https://dj-rest-auth.readthedocs.io/en/latest/)

If you want to just see it in action, you can skip the details below and just try it out:

Create python virtual environment.

```bash
python3 -m venv venv
source venv/bin/activate
```

Install cookiecutter and packaging:

```bash
pip install cookiecutter packaging
```

Step into directory you want to create project and generate project with cookiecutter:

```bash
cd /path/to/directory
cookiecutter https://github.com/Ohuru-Tech/drf-cookiecutter
```

Answer the questions in wizard.

#### Steps after project setup

Install the dependencies:

```bash
pip install -r requirements_Dev.txt
```

Copy `.env` file from example file and set your settings:

```bash
cp .env.template .env
```

Run migrations:

```bash
python manage.py migrate
```

Start building your awesome project!


This section will mostly talk about specific project details:

### App Structure

All the apps are located in the [`{{ cookiecutter.project_slug }}/apps`](https://github.com/Ohuru-Tech/drf-cookiecutter/tree/main/%7B%7B%20cookiecutter.project_slug%20%7D%7D/%7B%7B%20cookiecutter.project_slug%20%7D%7D/apps) folder. The project uses a custom `startapp` command, refer [`{{ cookiecutter.project_slug }}/management/commands/startapp.py`](https://github.com/Ohuru-Tech/drf-cookiecutter/blob/main/%7B%7B%20cookiecutter.project_slug%20%7D%7D/%7B%7B%20cookiecutter.project_slug%20%7D%7D/apps/common/management/commands/startapp.py) for more information.

The command initializes a new app with the following:
- A models directory as opposed to a generic `models.py` file which is created by Django's default `startapp` command, this allows you to have models separated by their use-case and helps to keep the project clean.
- A `views` directory which corresponds to the above reasoning for having a models directory.
- A `seriazliers` directory which corresponds to the above reasoning for having a models directory.

The other files and folders are similar to how Django does (`admin.py`, `apps.py`).

This command ensures that the apps are added to the `apps` folder and are initialized in accordance with the project-setup.
To add a new app run:
```bash
$ python manage.py startapp <app_name>
```
Then include the app to the `INSTALLED_APPS` list in `config/base.py` like so:
```python
INSTALLED_APPS = [
    ...
    '{{ cookiecutter.project_slug }}.apps.<app_name>.apps.<app_name_capitalized>Config',
    ...
]
```
eg: if your `app_name` is `foo` then this would be:
```python
INSTALLED_APPS = [
    ...
    '{{ cookiecutter.project_slug }}.apps.foo.apps.FooConfig',
    ...
]
```

It is always nice to include the complete config while adding the app in the `INSTALLED_APPS` list. This maintains consistency when using signals.

### Settings

The project uses [`django-configurations`](https://django-configurations.readthedocs.io/en/stable/) for class based settings, there are two configurations available, `Dev` and `Prod`, the common settings exist in `config/base.py`, `Dev` in `config/dev.py` and  `Prod` in `config/prod.py`.

You can use the `DJANGO_CONFIGURATION` environment variable to switch between the configurations, the `Dev` settings are used by default.

#### Environment Variables
Most of the configurations in the project are based on environment variables, you have the following options to use them:
- Set them up in a `.env` file in the root of the project, i.e. the place where you have this `README.md` file, you can refer to the `.env.template` for the available options and easily create your configuration file using that.
- Set them up via the environment variables themselves.
- Set them up in the docker / kubernetes runtime via the `.env` file, most of the things still remain the same except that you will need to get rid of the comments in `.env.template` while creating the `.env` file.

You can still run the project without creating any of those files because there are defaults available to almost all of the required settings but, you will need to set up the db instances, rabbit mq (in case you want to use celery) and make sure the settings match the ones used by default in the configurations. It is recommended to always have a `.env` file managing these settings.

NOTE:- The values for `{{ cookiecutter.project_slug | upper() }}_PASSWORD_RESET_URL` and `{{ cookiecutter.project_slug | upper() }}_EMAIL_VERIFICATION_URL` needs to be set in order for these flows to work properly, they make sure that the redirection happens to the frontend URLs for password reset and email verification.

The email verification could either be a page that gets and processes the link and sends the corresponding request to BE for email verification or have a page that waits for an explicit confirmation with the user.

### Documentation
The project uses `drf-yasg` to auto-create documentation for the API endpoints, but the generated documentation is not very good, hence, this project uses a custom schema to manage the documentation.

The details for each endpoint are managed using a class, for example, the `accounts` app supports profile update, so the `ProfileUpdate` class is used to manage the documentation for that endpoint, it looks something like this:
    
```python
class ProfileUpdate:
    desc = f"""
Profile Update is an API endpoint to update the profile associated
with a particular user.

By using this endpoint, you can make a patch call to update the profile
associated with any user. The {fields_to_md(ProfileCreateUpdateSerializer.fields_names)}
are the fields that can currently be updated
    """  # noqa

    responses = {
        "200": openapi.Response(
            description="OK",
            examples=profile_update_200_example,
            schema=UserInfoSerializer,
        ),
        "401": openapi.Response(
            description="Unauthorized", examples=profile_update_401_example
        ),
        "403": openapi.Response(
            description="Permission Denied",
            examples=profile_update_403_example,
        ),
    }

    code_examples = [{"lang": "Bash", "source": profile_update_curl}]

    swagger_setup = {
        "operation_id": "Profile Update",
        "operation_description": desc,
        "request_body": ProfileCreateUpdateSerializer,
        "responses": responses,
        "code_examples": code_examples,
    }
```

The examples come from `config/examples` and look something like this:
```python
profile_update_200_example = {
    "application/json": {
        "id": 1,
        "name": "Test developer",
        "profile_pic": "/some_pic.jpg"
    },
}

profile_update_401_example = {
    "application/json": {
        "error": "NotAuthenticated",
        "detail": "Incorrect authentication credentials.",  # noqa
    }
}

profile_update_403_example = {
    "application/json": {
        "error": "PermissionDenied",
        "detail": "You do not have permission to perform this action."
    }
}
```

After setting up the class, you can use the `swagger_auto_schema` decorator to plug the specification into the view.

```python
@method_decorator(
    swagger_auto_schema(**ProfileUpdate.swagger_setup), "partial_update"
)
class ProfileViewSet(UpdateModelMixin, PsqMixin, GenericViewSet):
    ...
```

In the above example, the `ProfileUpdate` class we declared earlier is used to plug those settings onto the `partial_update` action.

### The views

The project is set up to use [`GenericViewSet`](https://www.django-rest-framework.org/api-guide/viewsets/#genericviewset), it further uses different [`mixins`](https://www.django-rest-framework.org/api-guide/generic-views/#mixins) to provide the `create`, `retrieve`, `update` and `destroy` actions.
You can set up custom actions for anything else that you might need to do.

The views also use the [`PsqMixin`](https://github.com/drf-psq/drf-psq#1-psqmixin-class) to configure different serizlizers and permissions for the actions. The serializers come from the `serializers` directory and the permissions are from the `permissions` directory (or file, as per your convinience).

### Routers
The `GenericViewSet` automatically sets up the URLs based on the mixins you use with it, it also adds the custom actions as urls. Thus, we have `urls/versioned_urls.py` which uses a [`SimpleRouter`](https://www.django-rest-framework.org/api-guide/routers/#simplerouter) to add these viewsets and plug them at different endpoints.

#### Versioning
The version comes from the environment variables, you can set it up in the `.env` file. To add and support different versions, you can have differnt `urls` list in the `urls/versioned_urls.py` file, then update the environment to default to the latest version. You can manually include the other versions in `urls/__init__.py` as and when needed.

### Database
The project supports PostgreSQL, PostGIS, MySQL, MySQL (GIS), Oracle, Oracle (GIS), Redshift, CockroachDB, and SQLite, you can refer [this](https://github.com/jazzband/dj-database-url#url-schema) table in order to find out the details on forming the url corresponding to your database.

## Deployment

I have included a [Dockerfile](https://github.com/Ohuru-Tech/drf-cookiecutter/blob/main/%7B%7B%20cookiecutter.project_slug%20%7D%7D/Docker/Dockerfile) with a custom bash script [entrypoint](https://github.com/Ohuru-Tech/drf-cookiecutter/blob/main/%7B%7B%20cookiecutter.project_slug%20%7D%7D/Docker/entrypoint.sh) that can help you get both the django backend and celery worker + beat scheduler up and running in a jiffy...

I plan to leverage `kubernetes` and make a helm chart in future to easily set up a production grade server with `nginx` but I can use some help around this :P...

Anyway, that's almost as much as I wanted to say about this, please check the project out, do leave a star on the repository if you like it and feel free to make PRs to improve it.