Contents

Introduction

If, for some strange reason, you want to set up a project using TypeScript with pure Svelte v5.x, style the HTML elements with Tailwind v4.x, compile and bundle with Rollup v4.x, and use a Python-based backend like Django or Flask,Β then read this post.Β 

I'm assuming you're already familiar with these technologies.

I’m a big fan of Ubuntu, so all the commands in this post are based on or executed in a Bash terminal.

At the time of writing, I'm using:

  • Ubuntu: 24.04.2 LTS
  • Node: v22.14.0
  • npm: 10.9.2

Initialization

Let's start by creating a folder and initializing the frontend project using npm.

mkdir myproject && cd myproject
mkdir myfrontend && cd myfrontend && npm init -y

-y initializes the new project with default values, which can be changed later inside the package.json file. Nonetheless, we need to add the property: "type": "module" to package.json file, because we are going to use the modern import/export syntax. Also modify the value of main property to src/main.ts.Β  You should have something similar to this:

{
  "name": "myfrontend",
  "version": "1.0.0",
  "main": "src/main.ts",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
  },
  "dependencies": {
  }
}

We need a source folder and entry point files.

mkdir src && touch src/main.ts && touch src/app.css

TypeScript

Since TypeScript encourages writing less error-prone code, we are using it instead of JavaScript. To install TypeScript, type inside the project directory:

npm install typescript --save-dev

--save-dev indicates that this package is for development purposes, therefore it will be saved inside devDependencies

Since TypeScript needs a tsconfig.json file, let's create it with default settings.

npx tsc --init

This will generate the tsconfig.json file with the recommended default settings in the current directory. Important: make sure that target and module have ESNext as their values inside the tsconfig.json file.

Rollup

We are going to install Rollup locally so that we do not pollute the global packages folder.

npm install rollup --save-dev

Create an empty rollup.config.js file inside myproject/myfrontend directory.

touch rollup.config.js

It's time to install the necessary Rollup plugins. We will configure Rollup later.

npm install --save-dev rollup-plugin-svelte
npm install @rollup/plugin-node-resolve --save-dev
npm install @rollup/plugin-replace --save-dev
npm install @rollup/plugin-typescript --save-dev
npm install @rollup/plugin-terser --save-dev

Tailwind

Styling elements with Tailwind is straightforward and easy; therefore, we are installing the CLI version.

npm install tailwindcss @tailwindcss/cli --save

Include this line in src/app.css:

@import "tailwindcss";

Svelte

Time to install the standalone Svelte package:

npm install svelte --save
npm install svelte-preprocess --save-dev 

Create the svelte.config.js file in the root directory (myproject/myfrontend)

touch svelte.config.js

Also create constants.ts file inside the src directory:

touch src/constants.ts

with the following single declaration:

export declare const __PROD__: boolean;

Insert inside the svelte.config.js the following content:

import { sveltePreprocess } from 'svelte-preprocess'

const production = !process.env.ROLLUP_WATCH

export default {
  preprocess: sveltePreprocess({sourceMap: !production}),
};

Create the App.svelte file inside the src directory:

touch src/App.svelte

and insert the subsequent code:

<script lang="ts">

    let count = $state(0);

</script>

<div class="p-6 bg-white rounded-lg shadow-md max-w-md mx-auto mt-10 space-y-4">
    <h1 class="text-3xl font-bold underline">Hello world!</h1>
    <p class="text-lg">This is a Svelte app with Tailwind CSS.</p>
    <p class="text-lg">Count: {count}</p>
    <!-- Use flexbox for button layout -->
    <div class="flex space-x-2">
        <button class="flex-1 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onclick={() => count += 1}>
            Increment
        </button>
        <button class="flex-1 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" onclick={() => count -= 1}>
            Decrement
        </button>
        <button class="flex-1 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded" onclick={() => count = 0}>
            Reset
        </button>
    </div>
</div>

Add the following mounting content inside the src/main.ts file:

import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'

const app = mount(App, {
  target: document.body,
})

export default app

Virtual Environment

At this point, we should create and activate a virtual environment with a specific Python version. For this purpose, I use the Conda package manager and Python 3.13:

conda create --yes --name myproject python=3.13 && conda activate myproject

Django

Our current working directory at this point should be myproject/myfrontend. We need to move back to the myproject directory and create the backend-related folder:

cd .. && mkdir django_back && cd django_back

Let's create the requirements.txt file to hold all the required packages.

touch requirements.txt

and add this to it (change the version to the latest):

Django==5.2

We need to install Django. This can be done with the following command:

pip install -r requirements.txt

Django Project Creation

It's time to create a new Django project:

django-admin startproject mysite .

The command django-admin startproject mysite . initializes a new Django project named mysite in the current directory (.). It generates the following structure:

  • manage.py: A command-line utility for managing the project (e.g., running the server, migrations).
  • mysite/: A directory containing the core project files, including:
    • __init__.py: An empty file indicating that this directory is a Python package.
    • settings.py: The settings/configuration file for the project.
    • urls.py: The URL configuration file for routing requests.
    • asgi.py: The entry point for ASGI-compatible web servers.
    • wsgi.py: The entry point for WSGI-compatible web servers.

The . at the end ensures the project files are created in the current directory instead of a new subdirectory.

Django App Initialization

Django provides a command that can be executed inside the folder containing the manage.py file to create a new application with default folders and files:

python manage.py startapp myapp

The command creates a skeleton of the application. It generates a directory structure with default files needed for the app, including:

  • myapp/migrations/: A directory for database migration files.
  • myapp/admin.py: A file to register models with the Django admin interface.
  • myapp/apps.py: A file to configure the app.
  • myapp/models.py: A file to define database models.
  • myapp/tests.py: A file to write unit tests for the app.
  • myapp/views.py: A file to define views (logic for handling requests and responses).
  • myapp/__init__.py: An empty file indicating that the directory is a Python package.

We need to create a new directory named myapp/templates and two files myapp/templates/index.html and myapp/templates/base.html:

mkdir myapp/templates && touch myapp/templates/index.html && touch myapp/templates/base.html

Insert the following content into myapp/templates/base.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Djanog App{% endblock %}</title>
    
    {% block link_head %}{% endblock %}
</head>
<body>
    <header>
        <h1>Welcome to My Django App</h1>
    </header>
    <main>
        {% block content %}
        <p>This is the default content.</p>
        {% endblock %}
    </main>
    <footer>
        <p>&copy; 2025 My Django App</p>
    </footer>
    {% block script_body %}{% endblock %}
</body>
</html>

And add this content to myapp/templates/index.html:

{% extends "base.html" %}
{% load static %}


{% block link_head %}
<link rel="stylesheet" href="{% static 'frontend/css/app.css' %}">
{% endblock %}

{% block content %}
<p>Hello, World!</p>
<p>This is the index page of my Django app.</p>
{% endblock %}
{% block script_body %}
<script src="{% static 'frontend/js/bundle.js' %}"></script>
{% endblock %}

Add to the myapp/views.py file this:

from django.views.generic import TemplateView
# Create your views here.

class HomePageView(TemplateView):
    template_name = "index.html"

We need to create a URL configuration file:

touch myapp/urls.py

containing the following content:

from django.urls import path
from .views import HomePageView

urlpatterns = [
    path("", HomePageView.as_view(), name="index"),
]

Replace the TEMPLATES variable in mysite/settings.py with the following:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "myapp" / "templates"],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Add the STATICFILES_DIRS variable in mysite/settings.py:

STATICFILES_DIRS = [
    BASE_DIR / "static",
]

Replace the Β mysite/urls.py file with:

from django.contrib import admin
from django.urls import include, path
from myapp.urls import urlpatterns as myapp_urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(myapp_urls)),
]

Create two folders:Β static and frontend. These folders will contain static files, like the bundled files from the frontend.

mkdir -p static/frontend

Flask

If you wish to use Flask, you need to install it.

However, first, we need to create the root folder for the Flask files and the requirements.txt file. I assume you are in the myproject working directory.

mkdir flask_back &&  cd flask_back && touch requirements.txt

Add this to the requirements.txt file (update the version to the latest):

Flask==3.1.0

and install it:

pip install -r requirements.txt

Since we will have bundlers from the frontend, we need to create the templates directory for our HTTP responses. This directory will hold all the HTML files.

mkdir templates

Furthermore, we are generating the index.html and base.html files.

touch templates/index.html && touch templates/base.html

Insert this into base.htmlfile:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Flask App{% endblock %}</title>
    {% block link_head %}{% endblock %}
</head>
<body>
    <header>
        <h1>Welcome to My Flask App</h1>
    </header>
    <main>
        {% block content %}
        <p>This is the default content.</p>
        {% endblock %}
    </main>
    <footer>
        <p>&copy; 2025 My Flask App</p>
    </footer>
    {% block script_body %}{% endblock %}
</body>
</html>

and this into the index.html file:

{% extends "base.html" %}


{% block link_head %}
<link rel="stylesheet" href={{url_for('static', filename='frontend/css/app.css')}}>
{% endblock %}

{% block content %}
<p>Hello, World!</p>
<p>This is the index page of my Flask app.</p>
{% endblock %}
{% block script_body %}
<script src={{url_for('static', filename='frontend/js/bundle.js')}}></script>
{% endblock %}

We need to create the Flask entry point file. Let's call it app.py:

touch app.py

Add this to app.py file:

from flask import Flask
from flask import render_template


app = Flask(__name__)


@app.route("/")
def hello_world():
    return render_template("index.html")

Create a directory named frontend inside the static directory. This directory will contain all the files from the frontend.

mkdir -p static/frontend

Β 

Bridging

Rollup Settings

As the final step, we need to ensure that the result of compilation and bundling of files are redirected to the Python framework's sub-directory. This can be achieved by setting up the Rollup configuration file.

Navigate back to the working directory myproject/myfrontend and update the rollup.config.js file with the following content:

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

import typescript from '@rollup/plugin-typescript';
import replace from '@rollup/plugin-replace';
import terser from '@rollup/plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
import svelte from 'rollup-plugin-svelte';


// the variable `production` is set to true if the environment variable `ROLLUP_WATCH` is not defined
const production = !process.env.ROLLUP_WATCH;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const FLASK_DIR = path.join(path.resolve(__dirname, '../flask_back'), 'static', 'frontend', 'js');
const DJANGO_DIR = path.join(path.resolve(__dirname, '../django_back'),'static', 'frontend', 'js');

console.log('FLASK_DIR', FLASK_DIR);
console.log('DJANGO_DIR', DJANGO_DIR);
// the variable DIR_TO_BACKEND indicates the path for the bundle files to be completed to
const DIR_TO_BACKEND = process.env.BACKEND === 'flask' ? FLASK_DIR : DJANGO_DIR;
console.log('DIR_TO_BACKEND', DIR_TO_BACKEND);


/*
    * This function is used to clean the destination directories. Is defined as a plugin in the rollup config
    * @param {string} dirs - The directories to clean
*/
function cleanDirs(dirs) {
    return {
        name: 'clean-dirs',
        buildStart() {
            const cleanDir = (dirPath) => {
                if (fs.existsSync(dirPath)) {
                    fs.readdirSync(dirPath).forEach((file) => {
                        const curPath = path.join(dirPath, file);
                        if (fs.lstatSync(curPath).isDirectory()) {
                            cleanDir(curPath);
                        } else {
                            fs.unlinkSync(curPath);
                        }
                    });
                }
            };

            // Clean the directories
            dirs.forEach((dir) => {
                if (fs.existsSync(dir)) {
                    cleanDir(dir);
                } else {
                    fs.mkdirSync(dir, {recursive: true});
                }
            });
        }
    }

} 


/*
    * This function is used to create plugins for rollup
    * @return {Array} - The plugins to be used in the rollup config
*/
function createPlugins() {
    return [
        replace({
            preventAssignment: true,
            __IS_PROD: JSON.stringify(production),
        }),
        typescript({
            sourceMap: !production,
            inlineSources: !production,
        }),
        svelte({
            compilerOptions: {
                dev: !production,
            },
            emitCss: true
        }),
        resolve({
            browser: true,
            dedupe: ['svelte'],
            exportConditions: ['svelte', 'browser'],
        }),
        production && terser()
    ]
}


export default [
    {
        input: "src/main.ts",
        output: {
            sourcemap: !production,
            format: 'iife',
            name: 'app',
            dir: DIR_TO_BACKEND,
            entryFileNames: 'bundle.js',
            chunkFileNames: '[name].js',
            assetFileNames: '[name][extname]',
        },
        external: [/\.css$/],
        plugins: [
            cleanDirs([DIR_TO_BACKEND]),
            ...createPlugins(),
        ],
        onwarn(warning, handler) {
            // Ignore circular dependency warnings from svelte internals
            if (warning.code === 'CIRCULAR_DEPENDENCY') {
                // Check if the warning involves paths inside node_modules/svelte
                const involvesSvelteInternal = warning.ids?.some(id => id.includes('node_modules/svelte/'));
                if (involvesSvelteInternal) {
                    return; // Suppress the warning
                }
            }
            // Let Rollup handle other warnings normally
            handler(warning);
        },
        watch: {
            clearScreen: false,
            include: 'src/**',
        },

    }
]

Package.json Settings

To bring everything together, we need to define script entry points inside the package.json file to enable commands like npm run dev or npm run prod.

However, first, we need to install the concurrently npm package to keep both Rollup and Tailwind running simultaneously:

npm install concurrently --save-dev

At this point, the package.json file should look like this (packages version might differ):

{
  "name": "myfrontend",
  "version": "1.0.0",
  "main": "src/main.ts",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^16.0.1",
    "@rollup/plugin-replace": "^6.0.2",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.2",
    "concurrently": "^9.1.2",
    "rollup": "^4.40.0",
    "rollup-plugin-svelte": "^7.2.2",
    "svelte-preprocess": "^6.0.3",
    "typescript": "^5.8.3"
  },
  "dependencies": {
    "@tailwindcss/cli": "^4.1.3",
    "svelte": "^5.26.3",
    "tailwindcss": "^4.1.3"
  }
}

Django

Change the scripts property to:

"scripts": {
    "tailwind:dev-django": "npx tailwindcss -i ./src/app.css -o ../django_back/static/frontend/css/app.css --watch",
    "tailwind:prod-django": "npx tailwindcss -i ./src/app.css -o ../django_back/static/frontend/css/app.css --minify",
    "rollup:dev": "rollup -c -w",
    "rollup:prod": "rollup -c",
    "dev-django": "concurrently \"BACKEND=django npm run rollup:dev\" \"npm run tailwind:dev-django\"",
    "prod-django": "concurrently \"BACKEND=django npm run rollup:prod\" \"npm run tailwind:prod-django\""
  },

Run the development Django command:

npm run dev-django

Run the production Django command:Β 

npm run prod-django

In a new terminal (with virtual env activated), inside the Django root directory, execute:

python manage.py runserver 8090

Flask

Change the scripts property to this for Flask:

"scripts": {
    "tailwind:dev-flask": "npx tailwindcss -i ./src/app.css -o ../flask_back/static/frontend/css/app.css --watch",
    "tailwind:prod-flask": "npx tailwindcss -i ./src/app.css -o ../flask_back/static/frontend/css/app.css --minify",
    "rollup:dev": "rollup -c -w",
    "rollup:prod": "rollup -c",
    "dev-flask": "concurrently \"BACKEND=flask npm run rollup:dev\" \"npm run tailwind:dev-flask\"",
    "prod-flask": "concurrently \"BACKEND=flask npm run rollup:prod\" \"npm run tailwind:prod-flask\""
  },

Run the development Flask command:

npm run dev-flask

Run the production Flask command:

npm run prod-flask

In a new terminal (with virtual env activated), inside the Flask root directory, execute:

flask run --debug

Final Project Structure

myproject/
β”œβ”€β”€ django_back/      # Django backend files
β”‚   β”œβ”€β”€ manage.py
β”‚   β”œβ”€β”€ mysite/       # Django project settings
β”‚   β”œβ”€β”€ myapp/        # Django app
β”‚   β”œβ”€β”€ static/       # Static files for Django (including frontend build)
β”‚   β”œβ”€β”€ templates/    # Django templates
β”‚   └── requirements.txt
β”œβ”€β”€ flask_back/       # Flask backend files
β”‚   β”œβ”€β”€ app.py
β”‚   β”œβ”€β”€ static/       # Static files for Flask (including frontend build)
β”‚   β”œβ”€β”€ templates/    # Flask templates
β”‚   └── requirements.txt
β”œβ”€β”€ myfrontend/       # Frontend source files
β”‚   β”œβ”€β”€ node_modules/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ App.svelte
β”‚   β”‚   β”œβ”€β”€ app.css
β”‚   β”‚   └── main.ts
β”‚   β”œβ”€β”€ package.json
β”‚   β”œβ”€β”€ package-lock.json
β”‚   β”œβ”€β”€ rollup.config.js
β”‚   β”œβ”€β”€ svelte.config.js
β”‚   └── tsconfig.json
└── README.md

GitHub

You can find the complete project in my GitHub repository.

Conclusion

And there you have it! A whirlwind tour of setting up a frontend with TypeScript, Svelte 5, Tailwind 4, and Rollup, all ready to play nice with either a Django or Flask backend.

We went through initializing the project, installing all the necessary bits and pieces for the frontend, setting up TypeScript, configuring Rollup with its plugins, getting Tailwind CLI ready, and installing Svelte. Then, we jumped over to the backend, setting up a Conda environment and creating either a Django or Flask project, complete with templates and static file handling.

Finally, we bridged the gap by configuring Rollup to output the bundled frontend assets directly into the backend's static folders and set up npm scripts to make development and building a bit smoother.

Hopefully, this guide helps you get your project off the ground if you ever find yourself needing this particular combination of technologies. Happy coding!

Credits

Main photo: unsplash

Supporting

Support my work by renting space on my website: The Link