Electron + Django ( Part 1 ), desktop app integrate JavaScript & Python
Tags: electron, django, python, typescript
Published: 2022-12-29 15:04:00

## Introduction & Concept

Building a desktop app from Electron and Django is actually building a web application, which uses Electron as the front-end by a local browser and Django as the backend.

![chart.drawio.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650934109667/hxCd3iARO.png)

Then, why do we need to build the desktop application instead of web application?🤔

Because:
- desktop app can run offline
- desktop app can access client PC low-level API (e.g. file system 📁)

## POC (Proof-Of-Concept)

We will build an simple deskop application using Electron, TypeScript, Webpack and Django.

![output.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1650790030236/vh5PIC4ez.gif)

As shown above,
- User first inputs a text.
- The input is passed to Django web server and return output, which combined the input, date and time and some other text.
- Finally, the output will be shown in the window

## 0. Prerequisites
Assume that you have installed:
- Node js, v14.19.1
- python, 3.9.10
- virtualenv, 20.13.1

## 1. Setup Electron

We will use Electron Forge [TypeScript + Webpack](https://www.electronforge.io/templates/typescript-+-webpack-template) template to create our desktop application.

- Run the following command in command window:
```
npx create-electron-app edtwExample --template=typescript-webpack
```

We named tha example `edtw`, whick stands for **E**lectron, **D**jango, **T**ypScript, **W**ebpack

- After running command, you should see the output of the command window:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650779106506/vfSGWK12X.png)

File Structure:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650779151501/yGd4aLIHq.png)

- Run `npm run start` insides `edtwExample` folder and the following window should be popped up
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650779726857/fVTlTDeD-.png)

## 2. Setup Django
- Create a folder called `python` in `edtwExample` folder
```cmd
mkdir python
cd python
```
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650779958944/aRKDvcBHK.png)

- Create a virtual environment and activate it
```
virtualenv edtwExampleEnv
edtwExampleEnv\Scripts\activate
```
- Install Django and Django REST framework (with the version)
```
pip install django==4.0.3 djangorestframework==3.13.1
```
- Initiate Django project
```
django-admin startproject edtwExample
```
Here is the result file structure:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650780579519/zeAeNkB_n.png)

- Run Django app by the following command
```
python manage.py runserver
```

- Open 127.0.0.1:8000 in browser and you should see the following:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650780907890/OMJGl9eeF.png)

## 3. Start Django app when the electron start (using spawn)
In order to do so, we create a `startDjangoServer` method in `index.ts` that use `spawn` to run django `runserver` command
``` typescript
import { spawn } from 'child_process';

const startDjangoServer = () =>
{
	const djangoBackend = spawn(`python\\edtwExampleEnv\\Scripts\\python.exe`,
		['python\\edtwExample\\manage.py', 'runserver', '--noreload']);
	djangoBackend.stdout.on('data', data =>
	{
		console.log(`stdout:\n${data}`);
	});
	djangoBackend.stderr.on('data', data =>
	{
		console.log(`stderr: ${data}`);
	});
	djangoBackend.on('error', (error) =>
	{
		console.log(`error: ${error.message}`);
	});
	djangoBackend.on('close', (code) =>
	{
		console.log(`child process exited with code ${code}`);
	});
	djangoBackend.on('message', (message) =>
	{
		console.log(`message:\n${message}`);
	});
	return djangoBackend;
}
```

The following script calls cmd to run a new process with the command `python\edtwExampleEnv\Scripts\python.exe` with the arguments `['python\\edtwExample\\manage.py', 'runserver', '--noreload']`.


```typescript
const djangoBackend = spawn(`python\\edtwExampleEnv\\Scripts\\python.exe`,
		['python\\edtwExample\\manage.py', 'runserver', '--noreload']);
```

The following script log the output of the django process
```typescript
djangoBackend.stdout.on('data', data =>
{
	log.info(`stdout:\n${data}`);
});
djangoBackend.stderr.on('data', data =>
{
	log.error(`stderr: ${data}`);
});
djangoBackend.on('error', (error) =>
{
	log.error(`error: ${error.message}`);
});
djangoBackend.on('close', (code) =>
{
	log.info(`child process exited with code ${code}`);
});
djangoBackend.on('message', (message) =>
{
	log.info(`stdout:\n${message}`);
});
```

We call the `startDjangoServer`  method in the `createWindow` method.
```typescript
const createWindow = (): void => {

	startDjangoServer();

  // Create the browser window.
  const mainWindow = new BrowserWindow({
	height: 600,
	width: 800,
  });

  // and load the index.html of the app.
  mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);

  // Open the DevTools.
  mainWindow.webContents.openDevTools();
};
```
- Run `npm run start` and open the task manager, you should be able to see python process
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650784674016/Gozdptk5b.png)

- If you close the app window, the python process will stop

### Note
The argument `--noreload` in `['python\\edtwExample\\manage.py', 'runserver', '--noreload']` MUST BE INCLUDED to prevent django application started twice.

If `--noreload` is omitted, you will have 4 python instance running in the background.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650784773303/-b19FvdVG.png)

Even you close application window, there are 2 python instance left and you can still access django site.
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650784871466/cqiPUa_En.png)

## 4. Construct API method in Django
- Add `rest_framework` in `INSTALLED_APPS` in `settings.py`

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650790375112/rn-NIpOBh.png)

- Running the below command in command window to create an app named `edtwExampleAPI`

```cmd
python manage.py startapp edtwExampleAPI
```
You should see the below file structure:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1651015949319/aS6CEcac-.png)

- Add `path('', include('edtwExampleAPI.urls')),` in `edtwExample\urls.py`

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650790493565/ivBt6iEFf.png)

- Create `urls.py` under the folder `edtwExampleAPI` and paste the following content there

``` python
from django.urls import include, path
from rest_framework import routers
from . import views

router = routers.DefaultRouter()
router.register( 'edtwExampleAPI', views.EdtwViewSet, basename='edtwExampleAPI' )

## Wire up our API using automatic URL routing.
## Additionally, we include login URLs for the browsable API.
urlpatterns = [
    path('', include(router.urls)),
]
```

- In `views.py`, copy and paste the following content

```python
from datetime import datetime
from rest_framework import viewsets
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response

class EdtwViewSet(viewsets.ViewSet):
	## Create your views here.
	@action(methods=['GET'],  detail=False, name='Get Value from input' )
	def get_val_from( self, request ):

		input = request.GET[ 'input' ]

		return Response( status=status.HTTP_200_OK,
				data=f"[{ datetime.now() }] input= { input }, value from Django" )
```
- Restart Django web server and go to `http://127.0.0.1:8000/edtwExampleAPI/get_val_from/?input=This+is+an+input+text`.

The following will be shown:
![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650790811169/kBi5v-tlM.png)

## 5. Call Django API from Electron
- Copy and paste the following code to `index.html`

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>
  </head>
  <body>
    <h1>💖 Hello World!</h1>
    <p>Welcome to your Electron application.</p>
	<input id="input_text" type="text"></body>
	<button id="btn_get_val_from_django" >Get Value From Django</button>
	<h2>Output</h2>
	<p id="p_output"></p>
  </body>
</html>
```

- Copy and paste the following code to `renderer.ts`

```typescript
import axios from 'axios';
import './index.css';

const btnGetValFromDjango = document.getElementById('btn_get_val_from_django');
btnGetValFromDjango.onclick = async () => {

	const res = await axios.get("http://127.0.0.1:8000/edtwExampleAPI/get_val_from/", { params: {
		input: ( document.getElementById('input_text') as HTMLInputElement ).value
	} });

	const result = res.data;
	document.getElementById('p_output').innerHTML = result;
};

console.log('👋 This message is being logged by "renderer.js", included via webpack');
```

- Our logic is finished. In here, there will be 2 errors coming up. Do not worry, I will let you know how to solve it. 😊

If you test the app, it will show the following error.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650791529353/JRhC06qVD.png)

The above error was due to the content security policy. We will fix it by adding `devContentSecurityPolicy` in `package.json` and restart the application. (See [this](https://stackoverflow.com/a/70137354) for more info.)

```json
"@electron-forge/plugin-webpack",
{
	"mainConfig": "./webpack.main.config.js",
	"devContentSecurityPolicy": "connect-src 'self' http://127.0.0.1:8000 'unsafe-eval'",
	"renderer": {
		"config": "./webpack.renderer.config.js",
		"entryPoints": [
			{
				"html": "./src/index.html",
				"js": "./src/renderer.ts",
				"name": "main_window"
			}
		]
	}
}
```
- After that, if you try the application again, there will be another error.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1650792189600/12mxmx-Tc.png)

This is due to the common CORS policy. We choose the fix introduced in [here](https://pratikpc.medium.com/bypassing-cors-with-electron-ab7eaf331605#6a83).

The concept is to replace the header before browser check the origin.

Add the following method in `index.ts`
```typescript
const UpsertKeyValue = (obj : any, keyToChange : string, value : string[]) => {
	const keyToChangeLower = keyToChange.toLowerCase();
	for (const key of Object.keys(obj)) {
		if (key.toLowerCase() === keyToChangeLower) {
		// Reassign old key
		obj[key] = value;
		// Done
		return;
		}
	}
	// Insert at end instead
	obj[keyToChange] = value;
}
```

Change `createWindow` method as follow in `index.ts`

```typescript
const createWindow = (): void => {

	startDjangoServer();

	// Create the browser window.
	const mainWindow = new BrowserWindow({
		height: 600,
		width: 800,
	});

	mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
		(details, callback) => {
			const { requestHeaders } = details;
			UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);
			callback({ requestHeaders });
		},
	);

	mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
		const { responseHeaders } = details;
		UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);
		UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);
		callback({
			responseHeaders,
		});
	});

	// and load the index.html of the app.
	mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);

	// Open the DevTools.
	mainWindow.webContents.openDevTools();
};
```

- Restart the application and Done! 🎉👏

## Source code

You may check for the [source code](
https://github.com/ivanyu199012/4.-Electron_n_django_setup) for more information.
<br/>

## Reason for building a Desktop app using Electron and Django

Initially, I would like to build an desktop app and focus on using Python to build backend or business logic so I first search for Python's Desktop frameworks. However, they are either
- Not very user friendly
- Lack of nice-looking UI framework
- Not free of charge

Due to the above 3 reasons, I expected I may need to spend a lot of time to develop if I choose them. (Here is a good [ref](https://www.fyears.org/2015/06/electron-as-gui-of-python-apps.html).)

As I am a web developer, I asked myself, can I use as much as what I already know (e.g. JavaScript & Python) to build a Desktop app?

That is why Electron get into my sight.

- Electron + Django is a good approach if
	- You already have a web app using Django as the backend and JavaScript for the frontend and you would like to convert it to Desktop app
	- You want to be more excellent on Django
	  and develop the frontend using your favorite frontend library (e.g. React, Angular or vue e.t.c)

- It may not be an nice approach if
	- You build a desktop app from nothing. (Electron + Django takes relative more time to set up and you need to maintain 2 programming languages).

		For this case, I suggest to use Electron itself combined with TypeScript as there is already existing template to handle this. Also, Electron itself can access PC low-level API to fulfill your need