Integrating a React Frontend into a Django Application

This article explains how to integrate a React frontend with a Django application using Django REST Framework, Vite, and django-cors-headers. It covers project setup, API creation, React data fetching, CORS and CSRF, authentication options, deployment approaches, common problems, and recommended frontend-backend boundaries.

Integrating a React Frontend into a Django Application

Django and React solve different parts of a web application.

Django is commonly responsible for:

  • database models
  • business logic
  • authentication
  • permissions
  • server-side validation
  • API endpoints
  • administrative tools

React is commonly responsible for:

  • interactive user interfaces
  • browser-side state
  • reusable UI components
  • client-side navigation
  • API requests
  • updating pages without full reloads

A common integration uses Django as an API backend and React as a separate frontend application.

The basic flow is:

text

1
2
3
4
5
6
7
React frontend
      ↓ HTTP request
Django API
      ↓
Database
      ↓ JSON response
React frontend

React requests data from Django. Django validates the request, communicates with the database, and returns JSON. React then displays the returned data.

This article demonstrates a basic integration using:

  • Django
  • Django REST Framework
  • React
  • Vite
  • django-cors-headers

Django REST Framework provides serializers, API views, viewsets, authentication support, and URL routers for building web APIs. Vite provides a development server and React project template for modern frontend applications.

Integration Approaches

There are two common ways to combine Django and React.

Separate Frontend and Backend

React and Django run as separate applications.

text

1
2
3
4
5
6
7
project/
├── backend/
│   ├── manage.py
│   └── config/
└── frontend/
    ├── package.json
    └── src/

During development:

text

1
2
Django: http://localhost:8000
React:  http://localhost:5173

React communicates with Django through API requests.

This approach provides:

  • independent frontend and backend development
  • Vite’s development server and hot reloading
  • a clear API boundary
  • flexible deployment options
  • easier replacement of either frontend or backend

It also requires handling:

  • CORS
  • API authentication
  • separate development processes
  • environment-specific API URLs

This is the approach used in the main example.

React Built into Django

React can also be built into static JavaScript files that Django serves.

text

1
2
3
4
5
React source
     ↓ npm run build
Compiled JavaScript and CSS
     ↓
Django static files

This approach can simplify deployment because Django and React are delivered from the same origin.

However, frontend development still normally uses Vite’s development server. The compiled production assets are copied or generated into a location managed by Django’s static-file system.

Django’s staticfiles application can discover additional static directories through STATICFILES_DIRS, while collectstatic gathers production assets into STATIC_ROOT.

Example Application

The example will create a simple task application.

Django will provide an API with endpoints for:

  • listing tasks
  • creating tasks
  • updating tasks
  • deleting tasks

React will:

  • request tasks from Django
  • display the task list
  • submit new tasks
  • mark tasks as complete
  • delete tasks

Project Structure

The completed project will resemble:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
django-react-project/
├── backend/
│   ├── manage.py
│   ├── config/
│   │   ├── settings.py
│   │   └── urls.py
│   └── tasks/
│       ├── admin.py
│       ├── models.py
│       ├── serializers.py
│       ├── urls.py
│       └── views.py
└── frontend/
    ├── package.json
    ├── vite.config.js
    └── src/
        ├── App.jsx
        ├── api.js
        └── main.jsx

Creating the Django Backend

Create the main project directory:

bash

1
2
mkdir django-react-project
cd django-react-project

Create and activate a Python virtual environment:

bash

1
python -m venv .venv

On Linux or macOS:

bash

1
source .venv/bin/activate

On Windows PowerShell:

powershell

1
.venv\Scripts\Activate.ps1

Install Django, Django REST Framework, and the CORS package:

bash

1
python -m pip install django djangorestframework django-cors-headers

Create the Django project:

bash

1
2
django-admin startproject config backend
cd backend

Create an application:

bash

1
python manage.py startapp tasks

Configuring the Django Applications

Open backend/config/settings.py.

Add the task app, REST Framework, and CORS support:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "corsheaders",
    "rest_framework",

    "tasks",
]

Add the CORS middleware near the beginning of the middleware list:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

The CORS middleware should appear before middleware that may generate responses, such as CommonMiddleware, so it can add the required headers.

Allow the React development server:

python

1
2
3
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
]

The origin must include the scheme and port:

text

1
http://localhost:5173

Do not write only:

text

1
localhost:5173

For local development, the React server and Django server use different origins because they run on different ports.

Avoid enabling every origin in production unless the API is intentionally public:

python

1
CORS_ALLOW_ALL_ORIGINS = True

An explicit allowlist is safer for most applications.

Creating the Task Model

Open tasks/models.py:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django.db import models


class Task(models.Model):
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

Create and apply the migration:

bash

1
2
python manage.py makemigrations
python manage.py migrate

Register the model in tasks/admin.py:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.contrib import admin

from .models import Task


@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "completed",
        "created_at",
    ]
    list_filter = ["completed"]
    search_fields = ["title"]

Create an administrator account if needed:

bash

1
python manage.py createsuperuser

Creating a Serializer

Django model instances cannot be sent directly to React as JSON.

A serializer converts model instances into Python data that can be rendered as JSON. It also validates incoming request data before creating or updating model instances.

Create tasks/serializers.py:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from rest_framework import serializers

from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = [
            "id",
            "title",
            "completed",
            "created_at",
        ]
        read_only_fields = [
            "id",
            "created_at",
        ]

ModelSerializer provides a shortcut for creating serializers based on Django models.

Creating the API ViewSet

Open tasks/views.py:

python

1
2
3
4
5
6
7
8
9
from rest_framework import viewsets

from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

ModelViewSet provides standard API actions for:

  • listing objects
  • retrieving one object
  • creating objects
  • updating objects
  • partially updating objects
  • deleting objects

The resulting HTTP operations are:

HTTP method Endpoint Action
GET /api/tasks/ List tasks
POST /api/tasks/ Create a task
GET /api/tasks/1/ Retrieve task 1
PUT /api/tasks/1/ Replace task 1
PATCH /api/tasks/1/ Partially update task 1
DELETE /api/tasks/1/ Delete task 1

Viewsets group related API behavior, while routers generate their associated URL patterns.

Creating the API URLs

Create tasks/urls.py:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from rest_framework.routers import DefaultRouter

from .views import TaskViewSet


router = DefaultRouter()
router.register(
    "tasks",
    TaskViewSet,
    basename="task",
)

urlpatterns = router.urls

Open the project URL configuration in config/urls.py:

python

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import include, path


urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("tasks.urls")),
]

Run the Django development server:

bash

1
python manage.py runserver

Open:

text

1
http://localhost:8000/api/tasks/

Django REST Framework should display the browsable API.

Testing the API

Create a task with a command-line request:

bash

1
2
3
4
5
curl \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"title": "Connect React to Django"}' \
    http://localhost:8000/api/tasks/

Retrieve all tasks:

bash

1
curl http://localhost:8000/api/tasks/

A response may look like:

json

1
2
3
4
5
6
7
8
[
    {
        "id": 1,
        "title": "Connect React to Django",
        "completed": false,
        "created_at": "2026-08-02T12:00:00Z"
    }
]

Creating the React Frontend

Return to the main project directory:

bash

1
cd ..

Create a React project with Vite:

bash

1
npm create vite@latest frontend -- --template react

Move into the frontend directory:

bash

1
cd frontend

Install the dependencies:

bash

1
npm install

Start the React development server:

bash

1
npm run dev

Vite supports a React starter template and provides a development server with React Fast Refresh.

The frontend normally becomes available at:

text

1
http://localhost:5173

At this point, two servers should be running:

text

1
2
Django: http://localhost:8000
React:  http://localhost:5173

Creating an API Module

Create frontend/src/api.js:

javascript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const API_BASE_URL =
    import.meta.env.VITE_API_BASE_URL ??
    "http://localhost:8000/api";


async function request(path, options = {}) {
    const response = await fetch(
        `${API_BASE_URL}${path}`,
        {
            headers: {
                "Content-Type": "application/json",
                ...options.headers,
            },
            ...options,
        },
    );

    if (!response.ok) {
        let message = `Request failed: ${response.status}`;

        try {
            const data = await response.json();
            message = JSON.stringify(data);
        } catch {
            // The response did not contain JSON.
        }

        throw new Error(message);
    }

    if (response.status === 204) {
        return null;
    }

    return response.json();
}


export function getTasks() {
    return request("/tasks/");
}


export function createTask(title) {
    return request("/tasks/", {
        method: "POST",
        body: JSON.stringify({
            title,
            completed: false,
        }),
    });
}


export function updateTask(task) {
    return request(`/tasks/${task.id}/`, {
        method: "PATCH",
        body: JSON.stringify({
            completed: task.completed,
        }),
    });
}


export function deleteTask(taskId) {
    return request(`/tasks/${taskId}/`, {
        method: "DELETE",
    });
}

Keeping API requests in a separate module prevents networking code from being repeated throughout the React components.

Creating the React Component

Replace frontend/src/App.jsx:

jsx

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import { useEffect, useState } from "react";

import {
    createTask,
    deleteTask,
    getTasks,
    updateTask,
} from "./api";


function App() {
    const [tasks, setTasks] = useState([]);
    const [title, setTitle] = useState("");
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");

    useEffect(() => {
        async function loadTasks() {
            try {
                const data = await getTasks();
                setTasks(data);
            } catch (requestError) {
                setError(requestError.message);
            } finally {
                setLoading(false);
            }
        }

        loadTasks();
    }, []);

    async function handleSubmit(event) {
        event.preventDefault();

        const trimmedTitle = title.trim();

        if (!trimmedTitle) {
            return;
        }

        try {
            setError("");

            const newTask = await createTask(
                trimmedTitle,
            );

            setTasks((currentTasks) => [
                newTask,
                ...currentTasks,
            ]);

            setTitle("");
        } catch (requestError) {
            setError(requestError.message);
        }
    }

    async function handleToggle(task) {
        const changedTask = {
            ...task,
            completed: !task.completed,
        };

        try {
            setError("");

            const savedTask = await updateTask(
                changedTask,
            );

            setTasks((currentTasks) =>
                currentTasks.map((currentTask) =>
                    currentTask.id === savedTask.id
                        ? savedTask
                        : currentTask,
                ),
            );
        } catch (requestError) {
            setError(requestError.message);
        }
    }

    async function handleDelete(taskId) {
        try {
            setError("");
            await deleteTask(taskId);

            setTasks((currentTasks) =>
                currentTasks.filter(
                    (task) => task.id !== taskId,
                ),
            );
        } catch (requestError) {
            setError(requestError.message);
        }
    }

    return (
        <main>
            <h1>Tasks</h1>

            <form onSubmit={handleSubmit}>
                <label htmlFor="task-title">
                    New task
                </label>

                <input
                    id="task-title"
                    value={title}
                    onChange={(event) =>
                        setTitle(event.target.value)
                    }
                />

                <button type="submit">
                    Add task
                </button>
            </form>

            {error && (
                <p role="alert">
                    {error}
                </p>
            )}

            {loading ? (
                <p>Loading tasks...</p>
            ) : (
                <ul>
                    {tasks.map((task) => (
                        <li key={task.id}>
                            <label>
                                <input
                                    type="checkbox"
                                    checked={task.completed}
                                    onChange={() =>
                                        handleToggle(task)
                                    }
                                />

                                <span>
                                    {task.title}
                                </span>
                            </label>

                            <button
                                type="button"
                                onClick={() =>
                                    handleDelete(task.id)
                                }
                            >
                                Delete
                            </button>
                        </li>
                    ))}
                </ul>
            )}
        </main>
    );
}


export default App;

The component uses:

  • useState() to store tasks, form input, errors, and loading state
  • useEffect() to load tasks after the component is mounted
  • fetch() through the API module
  • state updates after successful API requests

Configuring the API URL

Create frontend/.env.development:

text

1
VITE_API_BASE_URL=http://localhost:8000/api

Access the value in JavaScript with:

javascript

1
import.meta.env.VITE_API_BASE_URL

Environment variable names exposed to Vite client code must begin with:

text

1
VITE_

Do not place secret keys, database credentials, private API tokens, or Django’s SECRET_KEY in frontend environment variables.

Values bundled into React code can be inspected by users in the browser.

Using a Vite Development Proxy

CORS can be avoided during local development by proxying API requests through Vite.

Open frontend/vite.config.js:

javascript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";


export default defineConfig({
    plugins: [react()],
    server: {
        proxy: {
            "/api": {
                target: "http://localhost:8000",
                changeOrigin: true,
            },
        },
    },
});

The API module can then use a relative URL:

javascript

1
const API_BASE_URL = "/api";

A request to:

text

1
http://localhost:5173/api/tasks/

is forwarded by Vite to:

text

1
http://localhost:8000/api/tasks/

This proxy is only a development convenience. Production routing still needs to be configured through the deployment environment.

CORS and CSRF Are Different

CORS and CSRF solve different problems.

CORS controls whether browser JavaScript from one origin may read responses from another origin.

CSRF protection prevents another site from making unwanted authenticated requests on behalf of a user.

Enabling CORS does not disable the need for CSRF protection.

This distinction becomes especially important when React uses Django session authentication.

Authentication Options

A React frontend commonly uses one of the following approaches.

Session Authentication

Django stores the user’s authenticated state in a server-side session, and the browser sends a session cookie.

Advantages include:

  • integration with Django’s existing authentication
  • integration with the Django admin
  • server-controlled sessions
  • familiar login and logout behavior

For cross-origin requests, JavaScript must include credentials:

javascript

1
2
3
fetch("http://localhost:8000/api/tasks/", {
    credentials: "include",
});

Django must allow credentials:

python

1
CORS_ALLOW_CREDENTIALS = True

The frontend origin may also need to be trusted for CSRF checks:

python

1
2
3
CSRF_TRUSTED_ORIGINS = [
    "http://localhost:5173",
]

Unsafe requests such as POST, PUT, PATCH, and DELETE require a valid CSRF token when using session authentication.

Token Authentication

The client sends an authentication token in a request header.

A typical header looks like:

text

1
Authorization: Token abc123

Token authentication can be convenient for API clients, but token storage and expiration need careful design.

JSON Web Tokens

JWT-based authentication is commonly added through a third-party package.

It can support short-lived access tokens and refresh tokens, but it also introduces decisions about:

  • token storage
  • token rotation
  • revocation
  • expiration
  • refresh behavior
  • protection against token theft

JWT is not automatically better than Django sessions. Choose the authentication model that matches the application’s deployment and security requirements.

For a browser frontend served from the same site as Django, session authentication is often a practical option.

Adding Session Authentication and CSRF

Suppose React and Django use Django’s session system.

Create a view that ensures the CSRF cookie is set:

python

1
2
3
4
5
6
7
8
9
from django.http import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie


@ensure_csrf_cookie
def csrf_cookie(request):
    return JsonResponse({
        "detail": "CSRF cookie set.",
    })

Add the URL:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.urls import path

from .views import csrf_cookie


urlpatterns = [
    path(
        "csrf/",
        csrf_cookie,
        name="csrf-cookie",
    ),
]

React can call it before submitting protected requests:

javascript

1
2
3
4
5
6
await fetch(
    "http://localhost:8000/api/csrf/",
    {
        credentials: "include",
    },
);

A helper can read the CSRF cookie:

javascript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function getCookie(name) {
    const cookies = document.cookie.split(";");

    for (const cookie of cookies) {
        const trimmedCookie = cookie.trim();

        if (
            trimmedCookie.startsWith(
                `${name}=`,
            )
        ) {
            return decodeURIComponent(
                trimmedCookie.slice(
                    name.length + 1,
                ),
            );
        }
    }

    return null;
}

Include it in unsafe requests:

javascript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const csrfToken = getCookie("csrftoken");

await fetch(
    "http://localhost:8000/api/tasks/",
    {
        method: "POST",
        credentials: "include",
        headers: {
            "Content-Type": "application/json",
            "X-CSRFToken": csrfToken,
        },
        body: JSON.stringify({
            title: "Protected task",
        }),
    },
);

Django recommends sending the token through the X-CSRFToken header for AJAX requests.

Do not solve CSRF errors by applying csrf_exempt to every API view. That removes an important security control.

Protecting the API

The initial example allows unrestricted access.

A real application should define authentication and permissions.

For example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet

from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Task.objects.filter(
            owner=self.request.user,
        )

The model would need an owner:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django.conf import settings
from django.db import models


class Task(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="tasks",
    )
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

Set the owner when creating a task:

python

1
2
3
4
def perform_create(self, serializer):
    serializer.save(
        owner=self.request.user,
    )

The serializer should not accept arbitrary owners from the frontend:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class TaskSerializer(
    serializers.ModelSerializer,
):
    class Meta:
        model = Task
        fields = [
            "id",
            "title",
            "completed",
            "created_at",
        ]
        read_only_fields = [
            "id",
            "created_at",
        ]

Filtering by the current user prevents one user from retrieving another user’s tasks.

Authentication verifies who the user is. Permissions and queryset restrictions determine which data the user may access.

Returning Validation Errors

Suppose the task title cannot be shorter than three characters.

Add serializer validation:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from rest_framework import serializers

from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = [
            "id",
            "title",
            "completed",
            "created_at",
        ]
        read_only_fields = [
            "id",
            "created_at",
        ]

    def validate_title(self, value):
        cleaned_value = value.strip()

        if len(cleaned_value) < 3:
            raise serializers.ValidationError(
                "The title must contain at least "
                "three characters."
            )

        return cleaned_value

Django REST Framework may return:

json

1
2
3
4
5
{
    "title": [
        "The title must contain at least three characters."
    ]
}

React should display these errors instead of assuming that every failed request has the same structure.

A more useful error parser might be:

javascript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function formatApiError(data) {
    if (typeof data === "string") {
        return data;
    }

    return Object.entries(data)
        .map(([field, messages]) => {
            const text = Array.isArray(messages)
                ? messages.join(" ")
                : String(messages);

            return `${field}: ${text}`;
        })
        .join(" ");
}

Production Option 1: Deploy Separately

The frontend and backend can be deployed separately:

text

1
2
3
4
5
React:
https://app.example.com

Django API:
https://api.example.com

The React production environment might contain:

text

1
VITE_API_BASE_URL=https://api.example.com/api

Django would allow the frontend origin:

python

1
2
3
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
]

For session-based authentication:

python

1
2
3
4
5
CSRF_TRUSTED_ORIGINS = [
    "https://app.example.com",
]

CORS_ALLOW_CREDENTIALS = True

Cookie settings may also need review:

python

1
2
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Production authentication involving different sites or subdomains requires careful cookie, SameSite, HTTPS, CORS, and CSRF configuration.

Production Option 2: Serve the React Build with Django

React can be compiled and served alongside Django.

Build the React project:

bash

1
2
cd frontend
npm run build

Vite normally creates:

text

1
frontend/dist/

The generated directory contains files such as:

text

1
2
3
4
5
dist/
├── index.html
└── assets/
    ├── index-abc123.js
    └── index-def456.css

One integration strategy is to configure Vite to place generated files in Django-controlled template and static directories.

For example:

javascript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "node:path";


export default defineConfig({
    plugins: [react()],
    build: {
        outDir: resolve(
            __dirname,
            "../backend/frontend_build",
        ),
        emptyOutDir: true,
    },
});

Django can then be configured to find the generated assets:

python

1
2
3
STATICFILES_DIRS = [
    BASE_DIR / "frontend_build" / "assets",
]

A Django template can serve the React entry page, but Vite’s generated asset filenames are hashed. A robust integration normally uses one of these strategies:

  • copy and transform index.html
  • read Vite’s build manifest
  • use a Django-Vite integration package
  • configure a reverse proxy to serve the React build
  • deploy the frontend separately

Manually hard-coding generated asset filenames is fragile because the names can change after each build.

Serving a Single-Page Application

A React single-page application may use client-side routes:

text

1
2
3
4
/
/tasks
/tasks/42
/settings

When a user directly opens:

text

1
/tasks/42

the server must still return React’s index.html. React Router can then interpret the route in the browser.

This usually requires a fallback rule.

A reverse proxy such as Nginx may:

  1. send /api/ requests to Django
  2. serve static frontend assets directly
  3. return index.html for unmatched frontend routes

A conceptual configuration is:

text

1
2
3
4
/api/*      → Django
/admin/*    → Django
/static/*   → static files
everything else → React index.html

Do not send API or admin routes to the React fallback.

Static and Uploaded Media Files

React build files are static files.

User uploads are media files.

These should be treated separately.

Typical Django settings include:

python

1
2
3
4
5
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"

For production static assets, Django’s documented workflow is to run collectstatic and configure a web server or static-file service to serve the collected directory.

React may display uploaded media using URLs returned by the Django API:

json

1
2
3
4
5
{
    "id": 1,
    "title": "Example",
    "image": "https://api.example.com/media/tasks/example.jpg"
}

Development Commands

The Django backend and React frontend usually run in separate terminals.

Terminal one:

bash

1
2
cd backend
python manage.py runserver

Terminal two:

bash

1
2
cd frontend
npm run dev

The development workflow becomes:

text

1
2
3
4
5
6
7
React component change
    ↓
Vite refreshes the frontend

Django model or API change
    ↓
Django reloads the backend

Common Problems

CORS Errors

Example browser message:

text

1
Blocked by CORS policy

Check:

  • django-cors-headers is installed
  • "corsheaders" is in INSTALLED_APPS
  • CorsMiddleware is placed correctly
  • the exact frontend origin is in CORS_ALLOWED_ORIGINS
  • the origin includes the correct scheme and port
  • the Django server has been restarted

Do not confuse:

text

1
http://localhost:5173

with:

text

1
http://127.0.0.1:5173

Browsers treat them as different origins.

A 404 API Response

Verify:

  • the project includes the app URLs
  • the router registered the viewset
  • the frontend uses the correct /api/ prefix
  • trailing slashes match Django’s URL configuration
  • the object ID exists

React Receives HTML Instead of JSON

A response beginning with:

html

1
<!doctype html>

usually means the request reached:

  • a frontend fallback page
  • a Django error page
  • a login redirect
  • the wrong server
  • the wrong URL

Inspect the browser network panel and verify the response URL, status, and content type.

403 CSRF Verification Failed

When using session authentication, verify:

  • the CSRF cookie was set
  • credentials: "include" is present
  • the X-CSRFToken header is included
  • the React origin is trusted
  • the request uses HTTPS in production
  • the cookie settings match the deployment

Do not disable CSRF protection as the default fix.

Request Data Is Rejected

Inspect the JSON response from Django REST Framework.

A 400 Bad Request often contains useful serializer errors:

json

1
2
3
4
5
{
    "title": [
        "This field may not be blank."
    ]
}

React should expose these messages to the user or developer.

Changes Do Not Appear

Check that:

  • both development servers are running
  • React is calling the intended backend
  • the API base URL is correct
  • the browser is not using stale data
  • state is updated after successful requests
  • the Django database contains the expected records

Common Beginner Mistakes

Putting Database Logic in React

React should not connect directly to the database.

Use:

text

1
React → Django API → Database

Django should enforce validation, permissions, and business rules.

Trusting Frontend Validation

React validation improves the user experience, but it is not a security boundary.

A user can submit requests without using the React interface.

Always validate important data in Django.

Allowing Every CORS Origin

Avoid using:

python

1
CORS_ALLOW_ALL_ORIGINS = True

as a permanent production solution.

Allow only the origins that need access.

Hard-Coding Development URLs Everywhere

Avoid repeating:

javascript

1
"http://localhost:8000/api"

throughout components.

Use one API module and an environment variable.

Ignoring Request Failures

Do not assume every request succeeds:

javascript

1
2
const response = await fetch(url);
const data = await response.json();

Check:

javascript

1
2
3
4
5
if (!response.ok) {
    throw new Error(
        `Request failed: ${response.status}`,
    );
}

Treating CORS as Authentication

CORS does not decide whether a user is allowed to access an API.

It controls browser cross-origin access.

The API still needs:

  • authentication
  • permissions
  • object ownership checks
  • validation

Exposing Secrets in React

Do not include secrets in:

  • React source files
  • VITE_ environment variables
  • compiled JavaScript
  • browser storage

Anything sent to the browser should be considered visible to the user.

Returning All Database Objects

Avoid:

python

1
queryset = Task.objects.all()

for private user-owned data.

Filter by the current user:

python

1
2
3
4
def get_queryset(self):
    return Task.objects.filter(
        owner=self.request.user,
    )

Disabling CSRF Without Understanding It

Avoid adding csrf_exempt merely to make a request work.

Determine whether the application uses:

  • session authentication and CSRF
  • token authentication
  • another deliberate authentication method

A useful separation is:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
React responsibilities:
    UI rendering
    form state
    client-side navigation
    loading states
    displaying validation errors
    making API requests

Django responsibilities:
    database access
    server-side validation
    authentication
    permissions
    business logic
    API responses
    administrative tools

Some validation may exist in both places.

For example, React can immediately warn that a title is empty, while Django must still reject an empty title if someone bypasses the React interface.

Basic Integration Checklist

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Django backend:
    [ ] Install Django REST Framework
    [ ] Create models
    [ ] Create serializers
    [ ] Create API views or viewsets
    [ ] Add API URLs
    [ ] Configure authentication
    [ ] Configure permissions
    [ ] Configure CORS when required
    [ ] Keep CSRF enabled for sessions

React frontend:
    [ ] Create the Vite project
    [ ] Configure the API base URL
    [ ] Create an API request module
    [ ] Load data in components
    [ ] Handle loading states
    [ ] Handle API errors
    [ ] Submit JSON requests
    [ ] Update state after responses
    [ ] Avoid exposing secrets

Production:
    [ ] Use HTTPS
    [ ] Restrict allowed origins
    [ ] Configure secure cookies
    [ ] Choose separate or combined deployment
    [ ] Serve static files correctly
    [ ] Route API and frontend paths correctly
    [ ] Test authentication and CSRF

Mini Reference

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Django
    Backend framework

Django REST Framework
    API serializers, views, authentication,
    permissions, and routers

React
    Component-based user interface

Vite
    React development server and build tool

Serializer
    Converts model data to and from API data

ViewSet
    Groups common API actions

Router
    Generates API URL patterns

CORS
    Controls browser access between origins

CSRF
    Protects authenticated state-changing requests

Basic backend request:

text

1
GET http://localhost:8000/api/tasks/

Basic React request:

javascript

1
2
3
4
5
const response = await fetch(
    "http://localhost:8000/api/tasks/",
);

const tasks = await response.json();

Basic API viewset:

python

1
2
3
class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

Basic CORS configuration:

python

1
2
3
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
]

A common Django and React architecture uses Django as an API backend and React as a browser frontend.

The main integration steps are:

  1. Create the Django models.
  2. Expose the data through Django REST Framework.
  3. Create the React application with Vite.
  4. Request the API from React.
  5. Configure CORS when the applications use different origins.
  6. add authentication, permissions, and CSRF handling.
  7. Choose a production deployment strategy.

The most important principle is that React should not replace Django’s server-side responsibilities.

React controls the interface. Django remains responsible for data integrity, authentication, permissions, and business rules.

For a small application, start with a simple JSON API and a few React components. Add routers, authentication methods, deployment tooling, and more advanced state management only when the project has a clear need for them.

Join the Newsletter

Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.

Get updates when new guides, learning paths, cheat sheets, and field notes are published.

No spam. Unsubscribe anytime.



There is no third-party involved so don't worry - we won't share your details with anyone.