iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔌

A New Choice for Web Development: Building with Django, Inertia, Vite, and React

に公開

Introduction

Are you tired of microservices?

  • It makes no sense to set up Django or Laravel for the backend while also setting up a separate Next.js (Node.js) instance for the frontend.
  • I just want to build a modern web service, so why do I have to expose an API?
  • [Django React App Construction] [Search]
    • I just want to use Django for the backend and React for the frontend. Do I really have to use such a complex architecture just for that...?

With the modular monolith architecture I will introduce this time, you can build a web service quickly like this:

views.py
import django
import inertia

@intertia('ShowVersion')
def index(request):
  return {
    'version': django.get_version()
  }
ShowVersion.tsx
type Props = {
    version: string;
}

export default Page({ version }: Props){
    return (<div>
        Django: {verison}
    </div>);
}

Values from the backend are passed to the frontend without exposing a REST API[1].

If you felt a spark of interest in this approach, this article is for you!
In this article, I will introduce a method to build a web service with a modular monolith architecture using Django + Inertia.js + Vite + React.

Why?

In recent years, it has become common to adopt a production architecture where React and an arbitrary backend environment are launched as microservices, with communication via RESTful APIs or GraphQL. While these architectures are loosely coupled and allow for easier replacement of individual components, a notable drawback is the "increase in development man-hours."

For example, let's say you launch React and Django as independent entities. Since the frontend is hosted via static hosting and requires interaction between the two, you implement a RESTful API in Django. At this point, you might worry about:

  • I've opened up an API, but I don't want end-users tampering with it... but issuing CSRF tokens is difficult.
  • I want to change the base HTML dynamically... should I switch to Next.js? Wait, then I need to set up Node.js too.
  • ...Thinking about it calmly, it feels like over-engineering to open up an API when I'm not providing functionality to end-users.

While thinking about these things, don't you ever feel like: "What am I doing... I just want to build a simple web app." In recent years, loosely coupled microservices have been highly regarded for large-scale projects, but for individuals building small-scale applications, might this not be the optimal solution? Is there any possibility that you will replace either part of the project you are currently developing in the future?

In contrast, the modular monolith I am introducing here requires only this:

views.py
import django
import inertia

@intertia('ShowVersion')
def index(request):
  return {
    'version': django.get_version()
  }
ShowVersion.tsx
type Props = {
    version: string;
}

export default Page({ version }: Props){
    return (<div>
        Django: {verison}
    </div>);
}

What do you think? Simply by separating the frontend and backend, you don't have to worry about the issues mentioned above, and best of all, you don't need to open an API for integration[1:1]!

Cases where this seems useful

It is particularly suitable for cases where the likelihood of future replacement is low and where development man-hours are critical (tight deadlines/schedules), such as:

  • Personal projects
  • Hackathons
  • Assignments/projects at educational institutions

Personally, I feel this is a great solution for building systems at educational institutions. The reason I am adopting this architecture is because Django was specified for my school assignment.

Regarding modular monoliths, the following article goes into detail, so please refer to it as well!
https://r-kaga.com/blog/what-is-modular-monolith

Conclusion

Everything done below is contained in the repository below.
If setting it up is a hassle, please use this:

https://github.com/BonyChops/djangoReactApp?new

Building the project[2][3]

Currently, Inertia.js supports various backends and frontends, but the setup is a bit of a challenge 😅 It would be nice if there were an interactive setup like Vite has for this too...

The author's environment

Please use this for reference.

Python 3.9.6
pip 23.2.1
Django 4.2.13

Setting up Django

Create a directory and cd into it.

mkdir djangoReactApp
cd djangoReactApp
If using venv
python3 -m venv myenv
source ./.venv/bin/activate

Install Django and set it up.

pip install Django
django-admin startproject djangoReactApp .

Let's launch it.

python manage.py runserver 8080

If you have opened it on port 8080 as shown above, the link is http://localhost:8080.

If it starts like this, you're good to go. Go back to the terminal and stop it with ^(Ctrl) + C.

Setting up React + Vite

 npm create vite@latest frontend
 Select a framework: React
 Select a variant: TypeScript + SWC // SWC is a fast compiler from Vercel, optional

Scaffolding project in /Users/bonychops/PycharmProjects/djangoReactApp/frontend...

Done. Now run:

  cd frontend
  npm install
  npm run dev

Run the following commands as instructed.

cd frontend
npm install
npm run dev

By default, it should launch at http://localhost:5173/.

It has started! 🎉
Once confirmed, type ^ + C to return to the root directory.

cd ..

Setting up the adapter and integration

Django side

Next, build the bridge between Django and React.

pip install inertia-django django-vite

Edit djangoReactApp/djangoReactApp/settings.py as follows.

djangoReactApp/djangoReactApp/settings.py
 # ...
 INSTALLED_APPS = [
     # ...
     'django.contrib.staticfiles', # Add below the Django apps
+    "django_vite",
+    "inertia",
     # ... Describe other installed apps here
 ]

 MIDDLEWARE = [
     # ...
     'django.middleware.clickjacking.XFrameOptionsMiddleware', # Add below the Django Middleware
+    "inertia.middleware.InertiaMiddleware",
      # ... Describe other installed middleware here
 ]

Next, create djangoReactApp/templates/base.html.

djangoReactApp/templates/base.html
{% load django_vite %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
        <!-- vite hmr -->
        {% vite_hmr_client %}
        {% vite_react_refresh %}
        {% vite_asset 'src/main.tsx' %}
        <title>djangoReactApp</title>
    </head>
    <body>      
        <!-- inertia -->
        {% block inertia %}{% endblock %}
    </body>
</html>

Specify the templates/base.html you just created in settings.py.

djangoReactApp/djangoReactApp/settings.py
 TEMPLATES = [
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [],
+        'DIRS': [BASE_DIR / 'templates'],
         # ...
     },
 ]

Next, add the following to the end of the file.

djangoReactApp/djangoReactApp/settings.py
 USE_I18N = True

 USE_TZ = True

+# Static files (CSS, JavaScript, images)
+# https://docs.djangoproject.com/en/4.2/howto/static-files/
+
+STATIC_URL = 'static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+INERTIA_LAYOUT = 'base.html'
+
+# Required for Django form submissions
+CSRF_HEADER_NAME = 'HTTP_X_XSRF_TOKEN'
+CSRF_COOKIE_NAME = 'XSRF-TOKEN'
+
+# Where ViteJS assets are built
+DJANGO_VITE_ASSETS_PATH = BASE_DIR / 'frontend' / 'dist'
+
+# Whether to use HMR
+DJANGO_VITE_DEV_MODE = DEBUG
+
+# Vite server port settings
+DJANGO_VITE_DEV_SERVER_PORT = 3000
+ 
+# Static file folder name (after running python manage.py collectstatic)
+STATIC_ROOT = BASE_DIR / 'static'
+
+# Include DJANGO_VITE_ASSETS_PATH in STATICFILES_DIRS to ensure it is copied internally when running collectstatic
+STATICFILES_DIRS = [DJANGO_VITE_ASSETS_PATH]

Create views.py.

djangoReactApp/djangoReactApp/views.py
from inertia import inertia


@inertia('Index/index')  # Select the React component to render
def index(request):
    return {}  # Empty Props

Finally, add the created view to urls.py.

djangoReactApp/djangoReactApp/urls.py
 from django.contrib import admin
 from django.urls import path
+from djangoReactApp import views
 
 urlpatterns = [
+    path('', views.index, name='home'),
     path('admin/', admin.site.urls),
 ]

React side

Install @types/node as it is needed for vite.config.ts.

cd frontend
npm install -D @types/node

Edit vite.config.ts.

djangoReactApp/frontend/vite.config.ts
 import {defineConfig} from 'vite'
 import react from '@vitejs/plugin-react-swc'
+import { resolve }  from 'path'
 
 // https://vitejs.dev/config/
-export default defineConfig({
+export default defineConfig((env) => ({
     plugins: [react()],
+    base: 'static/',
+    server: {
+        host: '127.0.0.1',
+        port: 3000,
+        open: false,
+        watch: {
+            usePolling: true,
+            disableGlobbing: false,
+        },
+        origin: env.mode === "development" ?  'http://127.0.0.1:3000' : "",
+    },
+    resolve: {
+        alias: {
+            '@': resolve(__dirname, './src')
+        }
+    },
+    build: {
+        outDir: resolve('./dist'),
+        manifest: "manifest.json",
+        assetsDir: "assets",
+        target: 'es2015',
+        rollupOptions: {
+            input: {
+                main: resolve('./src/main.tsx'),
+            },
+            output: {
+                entryFileNames: `assets/[name]/bundle.js`,
+            },
+        },
+    },
-})
+}))

Inertia uses <div id="app"> as the React root, so we adjust it.

djangoReactApp/frontend/src/App.css
-#root {
+#app {
   max-width: 1280px;
   margin: 0 auto;
   padding: 2rem;
   text-align: center;
 }
npm i -D @inertiajs/react

Replace the entire content of main.tsx as follows:

djangoReactApp/frontend/src/main.tsx
import 'vite/modulepreload-polyfill';
import {createRoot} from 'react-dom/client';
import {createInertiaApp} from '@inertiajs/react';
import './index.css'
import {StrictMode} from "react";


document.addEventListener('DOMContentLoaded', () => {
    createInertiaApp({
        resolve: (name) => {
            const pages = import.meta.glob('./pages/**/*.tsx', {eager: true});
            return pages[`./pages/${name}.tsx`];
        },
        setup({el, App, props}) {
            createRoot(el).render(
                <StrictMode>
                    <App {...props} />
                </StrictMode>
            );
        }
    }).then(() => {
    });
});
mkdir -p src/pages/Index
mv src/App.tsx src/pages/Index/index.tsx
djangoReactApp/frontend/src/pages/index/Index.tsx
 import { useState } from 'react'
-import reactLogo from './assets/react.svg'
+import reactLogo from '@/assets/react.svg'
 import viteLogo from '/vite.svg'
-import './App.css'
+import '@/App.css'

Startup

Finally, let's start the app and verify it. Run the following command sets in separate terminals.

Terminal 1
python manage.py runserver 8080
Terminal 2
cd frontend
npm run dev

With this state, access http://localhost:8080.

If it displays without issues, the minimal setup is complete. 🎉 Good job!

Production Environment

To use it in a production environment, follow the steps below.

djangoReactApp/djangoReactApp/settings.py
-DEBUG = True
+DEBUG = False

-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = [
+    "localhost" # You need to configure the host to be public in production; here we use localhost
+]

Please note that you must run the following every time there is a change on the Frontend side:

cd frontend
npm run build
cd ../
python manage.py collectstatic

Start it up:

python manage.py runserver 8080 # By default it doesn't serve static files, add --insecure if necessary

Appendix

Below are some column-style elements. Feel free to check them out if interested.

djangoReactApp/djangoReactApp/views.py
+import django
 from inertia import inertia


 @inertia('Index/index')  # Selecting the React component to render
 def index(request):
-    return {}
+    return {
+        "services": [
+            {
+                "version": django.get_version(),
+                "name": "Django",
+                "url": "https://www.djangoproject.com/",
+                "iconUrl": "https://github.com/django.png"
+            }
+        ]
+    }
djangoReactApp/frontend/src/pages/Index/index.tsx
 import {useState} from 'react'
 import reactLogo from '@/assets/react.svg'
 import viteLogo from '/vite.svg'
 import '@/App.css'
 import {Link} from "@inertiajs/react";
 
+type Props = {
+    services: {
+        version: string;
+        name: string;
+        url: string;
+        iconUrl: string;
+    }[]
+}
 
-function App() {
+function App(props: Props) {
+    const {services} = props;
     const [count, setCount] = useState(0)
 
     return (
         <>
             <div>
                 <a href="https://vitejs.dev" target="_blank">
                     <img src={viteLogo} className="logo" alt="Vite logo"/>
                 </a>
                 <a href="https://react.dev" target="_blank">
                     <img src={reactLogo} className="logo react" alt="React logo"/>
                 </a>
+                {services.map(v => (
+                    <a href={v.url} target="_blank">
+                        <img src={v.iconUrl} className="logo" alt={`${v.name} logo`}/>
+                    </a>
+                ))}
             </div>
 
-            <h1>Vite + React</h1>
+            <h1>{["Vite", "React", ...services.map(v => v.name)].join(" + ")}</h1>

You can see that even the version is displayed.

Code splitting

Inertia naturally supports code splitting. In the current configuration, it fetches all components during the first request before rendering. To change this to only fetch the currently viewed page, simply modify the following:

djangoReactApp/frontend/src/main.tsx
 document.addEventListener('DOMContentLoaded', () => {
     createInertiaApp({
         resolve: (name) => {
-            const pages = import.meta.glob('./pages/**/*.tsx', {eager: true});
+            const pages = import.meta.glob('./pages/**/*.tsx', {eager: false});
-            return pages[`./pages/${name}.tsx`];
+            return pages[`./pages/${name}.tsx`]();
         },

However, Inertia does not strictly recommend code splitting. The logic seems to be: "Is one component of the web app you're building really that heavy? Fetching everything at once might be more efficient so that you don't have to send requests for every single page."

For reference, here is the bundle size when code splitting is enabled versus disabled:

Code splitting: Disabled Code splitting: Enabled

The above is an example of when 2 pages (components) were created. How is it? Despite the main/bundle.js difference being only 200B, you can see that with code splitting enabled, the size actually increases slightly when considering the split files. It's probably better to consider this with the knowledge that even with code splitting enabled, the original bundle.js is about 200kB.

Well, I still enable code splitting, though. [4]

脚注
  1. Strictly speaking, Inertia.js handles it for you in this case. Even so, it performs optimizations without requiring user effort, such as embedding into HTML during the initial fetch and using JSON responses via API for page transitions. For details, see The protocol - Inertia.js. ↩︎ ↩︎

  2. Build Web Fullstack Apps with DIRT: Django, Inertia, React & Tailwind CSS aka D.I.R.T Stack - DEV Community ↩︎ ↩︎

  3. Django documentation | Django documentation | Django ↩︎

  4. It just doesn't feel right to fetch components for pages I'm not looking at... ↩︎

Discussion