Skip to main content

Django: Serving Single-Page Applications

Django: Serving Single-Page Applications

Serving Single-Page Applications (SPAs) with Django involves combining Django’s robust backend with modern JavaScript frameworks like React, Vue.js, or Angular to deliver dynamic, client-side rendered web applications. Django handles APIs, authentication, and static file serving, while the SPA manages the frontend. This tutorial explores Django serving SPAs, covering setup, routing, and practical applications for scalable full-stack development.


01. Why Serve SPAs with Django?

SPAs provide fast, interactive user experiences by rendering content on the client side, ideal for applications like dashboards, social platforms, or e-commerce sites. Django complements SPAs by offering secure APIs, database management, and authentication, creating a powerful full-stack solution. This approach separates backend logic from frontend rendering, enhancing maintainability and scalability.

Example: Basic SPA Integration

# myapp/views.py
from django.shortcuts import render

def spa_view(request):
    return render(request, 'index.html')

Output:

Serves SPA's index.html at http://127.0.0.1:8000/

Explanation:

  • render - Serves the SPA’s main HTML file.
  • Frontend framework (e.g., React) handles client-side routing.

02. Core Concepts for Serving SPAs

Django serves SPAs by providing APIs (via Django REST Framework) and routing all frontend requests to the SPA’s entry point (e.g., index.html). The frontend framework manages client-side routing. Below is a summary of key components and their roles:

Component Description Use Case
Django REST Framework (DRF) Builds APIs for SPA data Dynamic data fetching
Static Files Serves SPA assets (JS, CSS) Deliver frontend bundle
URL Routing Redirects to index.html Handle SPA routes
CORS Enables API access Frontend-backend communication


2.1 Setting Up Django Backend

Example: Django with DRF and CORS

# Install required packages
pip install django djangorestframework django-cors-headers
# myproject/settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'myapp',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # Add at the top
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ALLOWED_ORIGINS = [
    'http://localhost:3000',  # React/Vue.js frontend
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'static'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'frontend/build'],  # SPA build directory
        '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',
            ],
        },
    },
]
# myapp/models.py
from django.db import models

class Item(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name
# myapp/serializers.py
from rest_framework import serializers
from .models import Item

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ['id', 'name', 'created_at']
# myapp/views.py
from rest_framework import viewsets
from .models import Item
from .serializers import ItemSerializer
from django.shortcuts import render

class ItemViewSet(viewsets.ModelViewSet):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

def spa_view(request, path=''):
    return render(request, 'index.html')
# myapp/urls.py
from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import ItemViewSet, spa_view

router = DefaultRouter()
router.register(r'items', ItemViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    re_path(r'^.*$', spa_view, name='spa_view'),  # Catch-all for SPA routes
]
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
]
# Apply migrations
python manage.py makemigrations
python manage.py migrate

Output:

Django API at http://127.0.0.1:8000/api/items/, SPA served via index.html

Explanation:

  • django-cors-headers - Enables frontend API access.
  • re_path(r'^.*$') - Routes all non-API requests to index.html.
  • TEMPLATES['DIRS'] - Points to SPA’s build directory.

2.2 Setting Up React SPA

Example: React SPA Setup

# Create React app
npx create-react-app frontend
cd frontend
npm install axios react-router-dom

// frontend/src/App.js
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import axios from 'axios';
import './App.css';

function Home() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    axios.get('http://127.0.0.1:8000/api/items/')
      .then(response => setItems(response.data))
      .catch(error => console.error('Error:', error));
  }, []);

  return (
    <div>
      <h1>Items</h1>
      <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

function About() {
  return <h1>About Page</h1>;
}

function App() {
  return (
    <Router>
      <div className="App">
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
        </Switch>
      </div>
    </Router>
  );
}

export default App;
  

/* frontend/src/App.css */
.App {
  text-align: center;
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}
ul {
  list-style: none;
  padding: 0;
}
li {
  padding: 10px;
  border-bottom: 1px solid #ddd;
}
  
# Build React app
npm run build
# Move build files to Django
mv build/* ../myproject/frontend/build/

Output:

React SPA served by Django at http://127.0.0.1:8000/, with client-side routing

Explanation:

  • react-router-dom - Handles client-side routing.
  • Build output is served by Django as static files.

2.3 Setting Up Vue.js SPA

Example: Vue.js SPA Setup

# Install Vue CLI and create app
npm install -g @vue/cli
vue create frontend
cd frontend
npm install axios vue-router

// frontend/src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

Vue.use(VueRouter);

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: About },
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;
  

// frontend/src/views/Home.vue
<template>
  <div>
    <h1>Items</h1>
    <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'Home',
  data() {
    return {
      items: [],
    };
  },
  mounted() {
    axios.get('http://127.0.0.1:8000/api/items/')
      .then(response => {
        this.items = response.data;
      })
      .catch(error => console.error('Error:', error));
  },
};
</script>

<style scoped>
ul {
  list-style: none;
  padding: 0;
}
li {
  padding: 10px;
  border-bottom: 1px solid #ddd;
}
</style>
  

// frontend/src/views/About.vue
<template>
  <h1>About Page</h1>
</template>

<script>
export default {
  name: 'About',
};
</script>
  

// frontend/src/App.vue
<template>
  <div class="app">
    <router-view />
  </div>
</template>

<style>
.app {
  text-align: center;
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}
</style>
  
# Build Vue.js app
npm run build
# Move build files to Django
mv dist/* ../myproject/frontend/build/

Output:

Vue.js SPA served by Django at http://127.0.0.1:8000/, with client-side routing

Explanation:

  • vue-router - Manages client-side navigation.
  • mode: 'history' - Ensures clean URLs without hash (#).

2.4 Serving Static Files in Production

Example: Nginx Configuration for SPA

# nginx.conf
server {
    listen 80;
    server_name example.com;

    location / {
        root /path/to/myproject/frontend/build;
        try_files $uri $uri/ /index.html;  # Serve index.html for SPA routes
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /admin/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /static/ {
        alias /path/to/myproject/static/;
    }
}
# myproject/settings.py
STATICFILES_DIRS = [
    BASE_DIR / 'frontend/build/static',
]
# Collect static files
python manage.py collectstatic

Output:

Nginx serves SPA and proxies API requests to Django.

Explanation:

  • try_files - Redirects all SPA routes to index.html.
  • collectstatic - Gathers SPA’s static assets for production.

2.5 Adding Authentication

Example: JWT Authentication with React

# Install JWT package
pip install djangorestframework-simplejwt
# myproject/settings.py
INSTALLED_APPS = [
    ...
    'rest_framework_simplejwt',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}
# myproject/urls.py
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

// frontend/src/App.js
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import axios from 'axios';

function Home() {
  const [items, setItems] = useState([]);
  const token = localStorage.getItem('token');

  useEffect(() => {
    if (token) {
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
      axios.get('http://127.0.0.1:8000/api/items/')
        .then(response => setItems(response.data))
        .catch(error => console.error('Error:', error));
    }
  }, [token]);

  return (
    <div>
      <h1>Items</h1>
      <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

function Login() {
  const [credentials, setCredentials] = useState({ username: '', password: '' });

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post('http://127.0.0.1:8000/api/token/', credentials);
      localStorage.setItem('token', response.data.access);
      window.location.href = '/';
    } catch (error) {
      console.error('Login error:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={credentials.username}
        onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
        placeholder="Username"
        required
      />
      <input
        type="password"
        value={credentials.password}
        onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
        placeholder="Password"
        required
      />
      <button type="submit">Login</button>
    </form>
  );
}

function App() {
  return (
    <Router>
      <div className="App">
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/login" component={Login} />
        </Switch>
      </div>
    </Router>
  );
}

export default App;
  

Output:

Authenticated users access protected API endpoints in SPA.

Explanation:

  • djangorestframework-simplejwt - Secures API with JWT.
  • React stores JWT in localStorage and sends it in API requests.

2.6 Incorrect SPA Routing

Example: Missing Catch-All Route

# myapp/urls.py (Incorrect)
from django.urls import path
from .views import spa_view

urlpatterns = [
    path('', spa_view, name='spa_view'),  # Only serves root URL
]

Output:

404 error for SPA routes like /about

Explanation:

  • Without a catch-all route, Django returns 404 for client-side routes.
  • Solution: Use re_path(r'^.*$') to redirect to index.html.

03. Effective Usage

3.1 Recommended Practices

  • Use a catch-all route to handle SPA client-side routing.

Example: Optimized SPA Serving

# myapp/views.py
from django.shortcuts import render

def spa_view(request, path=''):
    return render(request, 'index.html')
# myapp/urls.py
from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import ItemViewSet, spa_view

router = DefaultRouter()
router.register(r'items', ItemViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    path('admin/', admin.site.urls),
    re_path(r'^(?!api/).*$', spa_view, name='spa_view'),  # Exclude API routes
]

Output:

All non-API routes serve SPA’s index.html efficiently.
  • Use environment variables for API URLs.
  • Optimize static file serving with a web server like Nginx in production.

3.2 Practices to Avoid

  • Avoid serving SPA static files with Django in production.

Example: Inefficient Static File Serving

# myproject/settings.py (Incorrect)
DEBUG = True  # In production
STATICFILES_DIRS = [
    BASE_DIR / 'frontend/build/static',
]

Output:

Slow and insecure static file serving in production.
  • Solution: Use Nginx or a CDN to serve static files.

04. Common Use Cases

4.1 Building a Dashboard SPA

Create a dashboard with Django APIs and React frontend.

Example: Dashboard SPA

# myapp/models.py
from django.db import models

class Metric(models.Model):
    name = models.CharField(max_length=100)
    value = models.IntegerField()
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name
# myapp/serializers.py
from rest_framework import serializers
from .models import Metric

class MetricSerializer(serializers.ModelSerializer):
    class Meta:
        model = Metric
        fields = ['id', 'name', 'value', 'updated_at']
# myapp/views.py
from rest_framework import viewsets
from .models import Metric
from .serializers import MetricSerializer
from django.shortcuts import render

class MetricViewSet(viewsets.ModelViewSet):
    queryset = Metric.objects.all()
    serializer_class = MetricSerializer

def spa_view(request, path=''):
    return render(request, 'index.html')
# myapp/urls.py
from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import MetricViewSet, spa_view

router = DefaultRouter()
router.register(r'metrics', MetricViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    re_path(r'^.*$', spa_view, name='spa_view'),
]

// frontend/src/App.js
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import axios from 'axios';
import './App.css';

function Dashboard() {
  const [metrics, setMetrics] = useState([]);

  useEffect(() => {
    axios.get('http://127.0.0.1:8000/api/metrics/')
      .then(response => setMetrics(response.data))
      .catch(error => console.error('Error:', error));
  }, []);

  return (
    <div>
      <h1>Dashboard</h1>
      <table class="article-table-container">
        <thead>
          <tr>
            <th>Metric</th>
            <th>Value</th>
            <th>Updated</th>
          </tr>
        </thead>
        <tbody>
          {metrics.map(metric => (
            <tr key={metric.id}>
              <td>{metric.name}</td>
              <td>{metric.value}</td>
              <td>{new Date(metric.updated_at).toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function App() {
  return (
    <Router>
      <div className="App">
        <Switch>
          <Route exact path="/" component={Dashboard} />
        </Switch>
      </div>
    </Router>
  );
}

export default App;
  

/* frontend/src/App.css */
.App {
  text-align: center;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}
table {
  width: 100%;
  border-collapse: collapse;
}
th, td {
  border: 1px solid #ddd;
  padding: 8px;
}
th {
  background-color: #f4f4f4;
}
  

Output:

Dashboard SPA displays metrics from Django API.

Explanation:

  • Django provides metric data via API.
  • React renders a table for real-time dashboard updates.

4.2 Building an E-commerce SPA

Create an e-commerce SPA with product listings and cart functionality.

Example: E-commerce SPA

# myapp/models.py
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField()

    def __str__(self):
        return self.name
# myapp/serializers.py
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'stock']
# myapp/views.py
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
from django.shortcuts import render

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

def spa_view(request, path=''):
    return render(request, 'index.html')
# myapp/urls.py
from django.urls import path, include, re_path
from rest_framework.routers import DefaultRouter
from .views import ProductViewSet, spa_view

router = DefaultRouter()
router.register(r'products', ProductViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    re_path(r'^.*$', spa_view, name='spa_view'),
]

// frontend/src/App.js
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import axios from 'axios';
import './App.css';

function Store() {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);

  useEffect(() => {
    axios.get('http://127.0.0.1:8000/api/products/')
      .then(response => setProducts(response.data))
      .catch(error => console.error('Error:', error));
  }, []);

  const addToCart = (product) => {
    setCart([...cart, product]);
  };

  return (
    <div>
      <h1>Store</h1>
      <h2>Cart ({cart.length} items)</h2>
      <div className="products">
        {products.map(product => (
          <div key={product.id} className="product">
            <h3>{product.name}</h3>
            <p>${product.price}</p>
            <button onClick={() => addToCart(product)} disabled={product.stock === 0}>
              {product.stock > 0 ? 'Add to Cart' : 'Out of Stock'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

function App() {
  return (
    <Router>
      <div className="App">
        <Switch>
          <Route exact path="/" component={Store} />
        </Switch>
      </div>
    </Router>
  );
}

export default App;
  

/* frontend/src/App.css */
.App {
  text-align: center;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}
.products {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}
.product {
  border: 1px solid #ddd;
  padding: 10px;
  width: 200px;
}
button {
  padding: 8px 16px;
}
  

Output:

E-commerce SPA displays products and manages cart.

Explanation:

  • Django API serves product data.
  • React handles dynamic cart functionality.

Conclusion

Serving SPAs with Django combines Django’s backend strengths with the dynamic capabilities of modern JavaScript frameworks. By configuring APIs, static file serving, and proper routing, developers can build scalable, interactive applications. Key takeaways:

  • Use Django REST Framework for API-driven backends.
  • Implement a catch-all route to support SPA client-side routing.
  • Secure APIs with authentication (e.g., JWT).
  • Avoid serving static files with Django in production; use Nginx or a CDN.

With Django and SPAs, you can create modern, user-friendly applications that deliver seamless experiences!

Comments