My Django-Svelte setup for fullstack development
Tags: django, svelte, webdev
Published: 2021-12-10 10:22:55

# My Django-Svelte workflow for fullstack development
* [Motivation](#motivation)
* [About this post](#about-post)
* [The app](#the-app)
    * [Prerequisites](#the-app/prerequisites)
    * [Setup](#the-app/setup)
    * [Code: Django - settings.py](#the-app/django/settings)
    * [Code: Svelte](#the-app/svelte)
    * [Code: Django - spa view](#the-app/django/spa)
    * [Test it](#the-app/test-it)
* [Adding Authentication](#authentication)
* [Rest API](#rest-api)
* [Building for Production](#production)
* [Conclusions](#conclusions)

# Motivation <a name="motivation"></a>

I recently discovered [Svelte](https://svelte.dev) and became my first choice for personal projects and MVP/POCs.

As a backend developer, I struggled a lot with frameworks like React or Vue, looking for a productive workflow. Svelte is dead simple compared to them, in my opinion. And yet very powerful.

However, coming from [Django](https://www.djangoproject.com), I still struggled on stuff like **security** or **form validation**: if I want a SPA, do I really need to use only Django Rest Framework and implement everything through REST API?
Take [authentication](https://docs.djangoproject.com/en/4.0/topics/auth/default/) for example: Django has built-in views for doing it in a secure way. Do I really need to re-implement it every time client-side using JWT? Looks like a waste of time, for my productivity at least.

I really like Django approach “**batteries included**”, since it ships with best practices and it makes me a lot more focused on the goal I want to achieve.
On the other hand, I still want the flexibility and reactivity provided by a SPA framework.

So, after multiple projects, I'd like to share in this blog post my personal take aways and show how I scaffold my projects, in order to get:
- a **Django website**, using the powerful template system for authentication, registration and password recovery (using Django built in views)
- a **Svelte app** served by Django, with hot reloading during development
- some **Rest API** to provide data to the SPA app using the awesome [Django REST Framework](https://www.django-rest-framework.org)

This way helps me a lot, because it keeps the best of the three worlds without compromising productivity.

# About this post <a name="about-post"></a>
Please consider the following as my personal vademecum for setting up a Django+Svelte project quickly with basic usefules dependencies (such as whitenoise, black...) and authentication (which I haven't found much covered online).

It is a concentrated boilerplate of my favourites tweaks on Django that made me proficient. Feel free to ignore those who don't interests you. Any feedback is very welcome.

I’m using Svelte because is my favourite, but the approach should work for Vue and React, with obvious adjustments.

A medium Django/Python and Svelte knowledge is required.


# The app <a name="the-app"></a>

## Prerequisites <a name="the-app/prerequisites"></a>
My tools for the job are:
* [poetry](https://python-poetry.org)
* [npm](https://www.npmjs.com)

## Setup <a name="the-app/setup"></a>
My setup is very similar to the one in the [DRF quickstart](https://www.django-rest-framework.org/tutorial/quickstart/#project-setup), but I prefer poetry to pip

```bash
mkdir django-svelte-sample-app
cd django-svelte-sample-app
```

### Django setup

For poetry init, go for the defaults and add the basic dependencies for our Django project after
```bash
poetry init
poetry add django whitenoise
poetry add --dev black
```

Set Django up (note the final dot)
```
poetry run django-admin startproject myapp .
```

### Svelte setup
```
npx degit sveltejs/template frontend
cd frontend
npm install
cd ..
```

### Setup

You should have the following structure:
```
$ find .
.
./frontend
./frontend/README.md
./frontend/rollup.config.js
./frontend/public
./frontend/public/index.html
./frontend/public/global.css
./frontend/public/favicon.png
./frontend/.gitignore
./frontend/package-lock.json
./frontend/package.json
./frontend/scripts
./frontend/scripts/setupTypeScript.js
./frontend/src
./frontend/src/App.svelte
./frontend/src/main.js
./pyproject.toml
./myapp
./myapp/asgi.py
./myapp/__init__.py
./myapp/settings.py
./myapp/urls.py
./myapp/wsgi.py
./manage.py
./poetry.lock
./.venv/....
./frontend/node_modules/...
```

## Code: Django - settings.py <a name="the-app/django/settings"></a>
We will configure some settings for Django in order to:
- enable Django Debug using Environment Variables
- configure logging
- read templates from a root folder (instead of subapps templates)
- managing static files with [Whitenoise](http://whitenoise.evans.io/en/stable/)

We will modify the **myapp/settings.py** file

### Introducing DJANGO_DEBUG
This is very useful when working with [Docker](https://www.docker.com) or [Heroku](https://www.heroku.com) and you want to dinamically turn on/off the Django debug mode.

Replace 
```python
DEBUG=True
```
with 
```python
import os
DEBUG = "DJANGO_DEBUG" in os.environ and os.environ["DJANGO_DEBUG"] == "ON"
```

### Configure logging
I usually configure logging as the following:

```python
logging_level = (
    "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"]
)

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "console": {
            "format": "[%(asctime)s][%(levelname)8s][%(name)16.16s]@[%(lineno)5s]$ %(message)s"
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "console",
        },
    },
    "root": {
        "handlers": ["console"],
        "level": "WARNING",
        "propagate": False,
    },
    "loggers": {
        "django.server": {
            "level": "WARNING",
            "handlers": ["console"],
            "propagate": False,
        },
        "myapp": {
            "level": logging_level,
            "handlers": ["console"],
            "propagate": False,
        },
    },
}
```

You can now easily change the log level using the LOGGING_LEVEL env variable. Feel free to change the format as you prefer!

### Django Template folder
I generally prefer to have all my .html templates inside a root level *templates* folder instead of every app having *appname/templates/appname* folder.

Modify the TEMPLATES variable from:
```python
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
```
to
```python
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": ["myapp/templates"], # <-- here
        ...
    },
]
```

and create the folder:
```bash
mkdir -p myapp/templates
```

### Django static files
As mentioned, I really love Whitenoise for serving static files directly from Django. This helps me a lot during development and deploying because I can stay within Django.

We now create a staticfiles folder inside myapp, where files will be collected when we go to production, and the folder that will contain the compiled Svelte files, that will be served as Django static files (we will see it later)

```bash
mkdir -p myapp/static/frontend
```

We need to configure out static configurations accordingly:
```python
STATIC_ROOT = os.path.join(BASE_DIR, "myapp", "staticfiles")
STATIC_URL = "/static/"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

STATICFILES_DIRS = (os.path.join(BASE_DIR, "myapp", "static"),)
```

And do not forget to add Whitenoise to the list of middlewares:
```python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware", # <-- here
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    ...
]
```

### Finish Django configuration
Let's complete Django configuration creating applying the migrations and starting up the server, just to be sure no warnings came up

If you want to just check that everything is ok, you can run:
```bash
DJANGO_DEBUG=ON poetry run python manage.py makemigrations
DJANGO_DEBUG=ON poetry run python manage.py migrate
DJANGO_DEBUG=ON poetry run python manage.py runserver
```

You should see
```
Watching for file changes with StatReloader
[2021-12-09 18:31:03,519][    INFO][django.utils.aut]@[  643]$ Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
December 09, 2021 - 18:31:03
Django version 4.0, using settings 'myapp.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

So far, so good. We just instructed Django to do some useful stuff through standard configuration. Let's add the Svelte part in order to see something.


## Code: Svelte <a name="the-app/svelte"></a>
For now, we will just expose the Svelte default app from Django.

The idea here is: we will serve the files from Django static/frontend dir and we will develop from Django (ie. localhost:8000) instead of localhost:5000 (default rollup dev server).

All we have to do is configure rollup to emit the build files in the static django folder.
Go to **frontend/rollup.config.js** and change the **export default** like this:
```js
export default {
	input: 'src/main.js',
	output: {
		sourcemap: true,
		format: 'iife',
		name: 'app',
		file: '../myapp/static/frontend/bundle.js' // <-- here
	},
    plugins: [
        ...
        !production && livereload('../myapp/static/frontend'), // <-- here
        ...
    ],
    ...
}
```

Move the frontend/public/ files to django
```bash
mv frontend/public/favicon.png myapp/static/frontend/
mv frontend/public/global.css myapp/static/frontend/
```

Now in a terminal starts rollup dev server
```
cd frontend
npm run dev
```

And you will see the bundle files created under the Django folder
```
$ ls myapp/static/frontend
bundle.css     bundle.js      bundle.js.map  favicon.png    global.css     index.html
```

We will leave the rollup dev server up so that we will have the hot reload while working on the Django side, so leave the server up and change terminal.

In order to serve it from Django, we need to render the template using a standard view

## Code: Django - the SPA view <a name="the-app/django/spa"></a>
We now create a dedicated Django app that will just render the Svelte app

```bash
cd myapp
poetry run django-admin startapp spa
cd ..
mkdir -p myapp/templates/spa
mv frontend/public/index.html myapp/templates/spa/
```

Replace the content of **myapp/spa/views.py** with the following:
```python
from django.views.generic import TemplateView

class SpaView(TemplateView):
    template_name = "spa/index.html"
```

Modify the **myapp/templates/spa/index.html** using static resources from the *frontend*
```html
{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset='utf-8'>
	<meta name='viewport' content='width=device-width,initial-scale=1'>

	<title>Svelte app</title>

	<link rel='icon' type='image/png' href="{% static 'frontend/favicon.png' %}"> <!-- <-- here -->
	<link rel='stylesheet' href="{% static 'frontend/global.css' %}"> <!-- <-- here -->
	<link rel='stylesheet' href="{% static 'frontend/bundle.css' %}"> <!-- <-- here -->

	<script defer src="{% static 'frontend/bundle.js' %}"></script> <!-- <-- here -->
</head>

<body>
</body>
</html>
```


Add the view in the **myapps/urls.py**
```python
# myapps/urls.py
from django.contrib import admin
from django.urls import path

from myapp.spa.views import SpaView  # <-- here

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", SpaView.as_view(), name="spa"),  # <-- here
]
```

and finally, in order to add it in the installed apps, we need to:
- modify **myapp/spa/apps.py**
```python
class SpaConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "myapp.spa" # <-- here
```
- add **myapp.spa** in the INSTALLED_APPS in **myapp/settings.py**
```python
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    ...
    "django.contrib.staticfiles",
    "myapp.spa", # <-- here
]
```



## Test it <a name="the-app/test-it"></a>

From the root folder of the project (ie the one with the *frontend* and *myapp* folders inside), run in two different terminal windows:

```shell
# 1st shell - svelte
cd frontend
npm run dev
```

```shell
# 2nd shell - django
DJANGO_DEBUG=ON poetry run python manage.py runserver
```


Go to [localhost:8000](http://localhost:8000/) and you will see the Svelte app served directly from Django.

Try changing **frontend/src/App.svelte** or **frontend/src/main.js**, hit Ctrl+R and you will see it hot reloaded (from Django. Remember to disable cache or force the browser to refresh the page if you don't see any change).

Don't forget that you need the "npm run dev" in background in order to keep sync the bundle.* files under Django static folder.

# Secure our SPA the Django way <a name="authentication"></a>
Now we can leverage the standard Django features, such as authentication.

First, secure our View extending the LoginRequiredMixin. In **myapp/spa/views.py**
```python
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin


class SpaView(LoginRequiredMixin, TemplateView):
    template_name = "spa/index.html"

```

To enable auth, add in your **myapp/settings.py**
```python
from django.urls import reverse_lazy

LOGIN_URL = reverse_lazy("login")
LOGIN_REDIRECT_URL = reverse_lazy("spa")
LOGOUT_REDIRECT_URL = reverse_lazy("spa")
```

and in **myapps/urls.py**
```python
from django.contrib import admin
from django.urls import path, include # <-- here

from myapp.spa.views import SpaView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")), # <-- here
    path("", SpaView.as_view(), name="spa"),
]
```

Create the login form template:
```bash
mkdir -p myapp/templates/registration
touch myapp/templates/registration/login.html
```

Add the following to **myapp/templates/registration/login.html** (taken from the [django documentation](https://docs.djangoproject.com/en/4.0/topics/auth/default/#django.contrib.auth.views.LoginView))
```html
<html>

<head></head>

<body>

    {% if form.errors %}
    <p>Your username and password didn't match. Please try again.</p>
    {% endif %}

    {% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
        please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
    {% endif %}

    <form method="post" action="{% url 'login' %}">
        {% csrf_token %}
        <table>
            <tr>
                <td>{{ form.username.label_tag }}</td>
                <td>{{ form.username }}</td>
            </tr>
            <tr>
                <td>{{ form.password.label_tag }}</td>
                <td>{{ form.password }}</td>
            </tr>
        </table>

        <input type="submit" value="login">
        <input type="hidden" name="next" value="{{ next }}">
    </form>

    {# Assumes you set up the password_reset view in your URLconf #}
    <p><a href="{% url 'password_reset' %}">Lost password?</a></p>
</body>

</html>
```

Create a super user running in a terminal
```bash
poetry run python manage.py createsuperuser
```

Now if you try to hit [localhost:8000](http://localhost:8000/), you will be redirected to [http://localhost:8000/accounts/login/?next=/](http://localhost:8000/accounts/login/?next=/).

Try to login with the provided credentials and you will see the Svelte app again.

Hit [localhost:8000/accounts/logout](http://localhost:8000/accounts/logout) to logout.

# Rest API <a name="rest-api"></a>
Now we will add a very simple REST API using [Django Rest Framework](https://www.django-rest-framework.org/#installation) and consume it from the Svelte App.

Note that our Svelte SPA app will leverage [DRF SessionAuthentication](https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication), since it will invoke the API in the same context of the Django app. This is the most secure way, in my opinion

First, install DRF
```
poetry add djangorestframework
```
In addition, since Django 4.0 [has just been released](https://docs.djangoproject.com/en/4.0/releases/4.0/#zoneinfo-default-timezone-implementation), we need to install pytz, since DRF still uses it
```bash
poetry add pytz
```

and add it in **myapp/settings.py** INSTALLED_APPS
```python
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'rest_framework', # <-- here
    "myapp.spa",
]
```


Create a new Django app for the API
```bash
cd myapp
poetry run django-admin startapp api
cd ..
```

Add the mock api in **myapp/api/views.py**
```python
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from rest_framework.renderers import JSONRenderer


class GreetingApi(APIView):
    authentication_classes = [authentication.SessionAuthentication]
    permission_classes = [permissions.IsAuthenticated]

    renderer_classes = [JSONRenderer]

    def get(self, request, format=None):
        return Response({"message": "Hello world"})
```

Note that:
* we are using the default Django Session Authentication (via login form)
* the API is available only for authenticated users

Now adjust **frontend/src/App.svelte** 
```html
<script>
	import { onMount } from "svelte";

	export let name;

	let apimessage = "Waiting for server...";

	onMount(async () => {
		let resp = await fetch("/api/greet").then((res) => res.json());
		console.log(resp);
		apimessage = JSON.stringify(resp);
	});
</script>

<main>
	<h1>Hello {name}!</h1>
	<p>
		Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn
		how to build Svelte apps.
	</p>

	<h3>Data from server</h3>
	{apimessage}
</main>
```

Add the API to **myapp/urls.py**
```python
from django.contrib import admin
from django.urls import path, include

from myapp.spa.views import SpaView
from myapp.api.views import GreetingApi  # <-- here

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
    path("api/greet", GreetingApi.as_view()),  # <-- here
    path("", SpaView.as_view(), name="spa"),
]
```

Hit [localhost:8000](http://localhost:8000/), login and enjoy your Django based SPA using Server Side Authentication and Rest API

# Building for Production <a name="production"></a>
In order to make our app ready to Production, we need:
1. Gunicorn installed
```bash
poetry add gunicorn
```
2. collect all static files
```bash
poetry run python manage.py collectstatic
```
3. configure the allowed hosts
```python
#myapp/settings.py
# change ALLOWED_HOSTS = [] to
ALLOWED_HOSTS = ["localhost"]
```

Now you are ready to run
```bash
poetry run gunicorn myapp.wsgi
```

Go to [localhost:8000](http://localhost:8000/) and enjoy your gunicorn served app

# Conclusions <a name="conclusions"></a>
In this quite long walkthrough, we saw different things:
1. set up a Django app with my personal useful tweaks
2. set up an integrated Svelte app served by Django
3. configure Django built-in authentication to secure our SPA
4. providing some Rest API for the SPA to consume
5. packing everything up for Production

You can find all the code [here on github](https://github.com/besil/django-svelte-sample-app/tree/setup).

I hope this post can help you make your life as developer easier. You can find Docker integration and other utility scripts for local development [here](https://dev.to/besil/my-django-dev-utilities-4p2e).

Feel free to comment, I'd be glad to receive any feedback your have.