How to setup the modern frontend toolchain in Django

Published

So you want to deploy a traditional Django application, but you also want Typescript, Tailwind, and all the good that modern frontend offers?


Show me the code! Here's a link to the GitHub repository of html-webpack-plugin-django that also contains all the code from this post in the example folder.

Introduction

In this article I will explain how I've set up the latest Django project I've deployed at RIPE NCC to work with the modern frontend toolchain. If you want to check it out, it's a small app called Hosted DNS.

While other web frameworks are developing close integrations with Webpack, for example Laravel Mix or Rails Webpacker, I think Django is lagging behind.

On the other hand, I don't think such abstractions are even necessary as Webpack can be easily configured to play nicely with Django.

In this post, I explain how you can set up Django to benefit from modern frontend tooling, without compromising the conventional structure of your project.

I'm going to build a Django app with a Webpack bundle served from the usual static folder managed with django.contrib.staticfiles.

First things first: why do you even need this? Nothing wrong with jQuery, but maybe you want to write your scripts with Typescript; maybe you want to use Tailwind instead of Bootstrap and you need to build your stylesheet; maybe you need to import Web Components distributed through npm; maybe you want to use Material Design UI components which require Sass; or, as in my case, all of the above.

Your Django app

Let's start with a basic Django app. If you're not feeling at home at this stage, you may want to take a look at Django's Get Started page.

.
├── mysite/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── requirements.txt
├── myapp
│   ├── __init__.py
│   ├── apps.py
│   ├── migrations/
│   ├── models.py
│   ├── static/
│   ├── templates/
│   ├── urls.py
│   └── views.py
└── templates/
    └── base.html

At this point, you could throw all your scripts and stylesheets in ./myapp/static and in base.html manually add <script> and <link> tags and be done with it.

However, if you want to easily use Typescript, Tailwind, Sass and more, read on to the next section to learn how to set it up.

The frontend toolchain

First things first, you'll want to install node.js on your machine.

I greatly prefer yarn to npm, but that's another story. In any case, you can install yarn with npm install --global yarn.

Then, in the root of your project, run this command:

yarn init -2
# if you prefer the `node_modules` approach, run this instead
yarn init

You should now have these files in your project's root:

  • .yarn/: a directory that will store all the frontend dependencies you will add
  • .yarnrc.yml: a file to configure yarn's internal settings
  • package.json: a file to list your frontend dependencies and build scripts
  • yarn.lock: a lockfile to store which version of each dependency was installed

Installing Webpack and the necessary dependencies

At this point, you'll want to install these dependencies:

yarn add webpack webpack-cli \
         html-webpack-plugin html-webpack-plugin-django
  • webpack: webpack is an open-source JavaScript module bundler. It can also transform front-end assets like HTML, CSS, and images if the corresponding loaders are included
  • html-webpack-plugin: this plugin will generate an HTML5 file for you that includes all your webpack bundles using script tags
  • html-webpack-plugin-django: a plugin I wrote to transform the output of html-webpack-plugin to use Django's {% static %} tags

Configuring Webpack

At this stage, running webpack build will throw a lot of errors because it's not configured yet.

Let's start by creating an empty webpack.config.js file. This is simply a JavaScript file that should export an object containing the configuration for our build.

// webpack.config.js
module.exports = () => {
  return {};
};

Entry point

Your bundle needs an entry point: the point or points where to start the application bundling process. If an array is passed then all items will be processed. Learn more about this in the official Webpack documentation.

Let's stick to convention and name this file index.js. We'll place it in ./myapp/static/index.js because that's where a Django developer would expect to find this, and we're trying to integrate Webpack into Django in the least surprising way possible.

At this point, we can configure Webpack so it knows that's our entry point.

// webpack.config.js
module.exports = () => {
  return {
    entry: {
      index: "./myapp/static/index.js",
    },
  };
};

Output

The output is the location where Webpack will save the final bundle it produces. We'll configure Webpack to save the bundle in ./myapp/static/dist so we don't have to tweak Django to serve these static files.

// webpack.config.js
const path = require("path");

module.exports = () => {
  return {
    entry: {
      index: "./myapp/static/index.js",
    },
    output: {
      path: path.resolve("myapp/static/dist"),
      filename: "[name].[contenthash].js",
      // corresponds to the STATIC_URL setting in Django
      publicPath: "/static/",
    },
  };
};

There is one thing I'd like to point out in this configuration entry:

  • output.filename: this setting tells Webpack to process our bundle's entry point and output it with a filename made of these two parameters
    • [name]: corresponds to the original filename
    • [contenthash]: a hash based on the content of the bundle

For example, in the case of the entry point index.js, the output could be something like index.744a73e75a45758334f9.js.

Plugins

Now the most important part: configuring html-webpack-plugin. The following configuration will output all your webpack bundles using script or link tags to a webpack.html file in the templates directory.

You can then easily include this file in your base.html. Alternatively, you can write your own template, base_template.html, and let html-webpack-plugin take over and output a full base.html.

e.g.

<!-- base.html -->
<!DOCTYPE html>
<html lang="en-US">
  <head>
    {% include "webpack.html" %}
  </head>
  ...
</html>
// webpack.config.js
const path = require("path");

const templateContent = ({ htmlWebpackPlugin }) =>
 `{% load static %}
 ${htmlWebpackPlugin.tags.headTags}
 ${htmlWebpackPlugin.tags.bodyTags}`;

module.exports = () => {
  return {
    entry: {
      index: "./myapp/static/index.js",
    },
    output: {
      path: path.resolve("myapp/static/dist"),
      filename: "[name].[contenthash].js",
      // corresponds to the STATIC_URL setting in Django
      publicPath: "/static/",
    },
    plugins: [
      new HtmlWebpackPlugin({
        appMountId: "index",
        filename: path.resolve(__dirname, "templates", "webpack.html"),
        inject: false,
        templateContent: templateContent,
        scriptLoading: "defer",
      }),
      new HtmlWebpackPluginDjango({ bundlePath: "dist" }),
    ],
  };
};

Building the bundle

All is configured now, you can build your bundle. In your package.json add the following script, then run yarn build.

// package.json
{
  "scripts": {
    "build": "webpack --mode production"
  },
  "dependencies": {
    "webpack": "*",
    "webpack-cli": "*",
    "html-webpack-plugin": "*",
    "html-webpack-plugin-django": "*"
  }
}

Now you'll see that ./myapp/static/dist contains a few files and that they are all imported in templates/webpack.html, which now contains something like this:

{% load static %}
<script defer src="{% static 'dist/js/runtime.XXX.js' %}"></script>
<script defer src="{% static 'dist/js/index.XXX.js' %}"></script>

Conclusion

If you kept reading until here, thank you first and foremost.

You should now have a conventional Django app with a Webpack integration that doesn't sacrifice anything: by using the {% static %} tag, your project doesn't require any adjustments to run locally or to be deployed.

From here the sky is the limit: you can now easily start using Typescript with ts-loader or process your CSS with PostCSS using postcss-loader for example. The basic set up implemented above is just a starting point.

Here's once again a link to a GitHub repository that contains all the code from this post.

© 2022 Tommaso Amici, All Rights Reserved