Connect React to a Django Backend

Build a full-stack Task Manager with a Django REST API and React frontend.

Introduction

30 Second Summary

Most apps you use every day have a visible interface and a hidden engine behind it. The screen you see (the frontend) talks to a server (the backend) through structured messages called API requests. Understanding how these two halves connect is one of the most valuable skills in modern software development.

In this project, you will build a Task Manager app with a Django backend serving data through a REST API, and a React frontend that displays, creates, and deletes tasks by talking to that API.

What You'll Build

A full-stack Task Manager application with a Django REST Framework backend and a React frontend (built with Vite) that communicates with it over HTTP.

By the end of this project, you'll have:

  • A Django REST API that serves task data from a SQLite database as JSON.
  • A React app that fetches, displays, creates, and deletes tasks using Axios.
  • A working CORS configuration that enables cross-origin communication between two separate dev servers.
  • Secret Mission: Add inline editing so users can update a task's title with a PATCH request.

Are there any prerequisites?

This project assumes basic familiarity with Python and JavaScript syntax. You do not need prior experience with Django or React.

Before We Start

Before We Start...

Set Up the Backend (Django + DRF)

Most modern web applications split into two parts: a backend that stores data and handles logic, and a frontend that users interact with.

In this step, you'll set up the backend half using Django and Django REST Framework. You'll also install django-cors-headers so your frontend can talk to this backend later.

In this step, get ready to:
  • Create a Python virtual environment and install dependencies.
  • Create a Django project and app with DRF and CORS configured.
  • Verify the development server starts successfully.
Create a virtual environment and install packages

A virtual environment keeps your project's Python packages isolated from everything else on your machine. This prevents version conflicts between different projects.

  • Open the VS Code terminal by pressing Ctrl+` (Control + backtick) on both macOS and Windows.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your backend folder by running this command:
mkdir backend
  • Confirm the folder was created by running:
ls

You should see backend listed in the output.

  • Move into the new folder by running:
cd backend
  • Create a virtual environment by running this command:
python3 -m venv venv

What does this command do?

python3 -m venv venv tells Python to create a virtual environment in a folder called venv. This folder contains a self-contained Python installation for your project.

  • Activate the virtual environment by running the command for your operating system:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

Linux

source venv/bin/activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activation command for your operating system. On macOS/Linux it starts with source. On Windows the path uses backslashes.

On Windows, if you see a permissions error, you may need to allow script execution in PowerShell before retrying the activation command.

help me activate my Python virtual environment

  • Install Django, Django REST Framework, and django-cors-headers by running:
pip install django==5.2.14 djangorestframework==3.17.1 django-cors-headers==4.9.0

What are these packages?

django==5.2.14 is the web framework that handles routing, database access, and server logic.

djangorestframework==3.17.1 adds tools for building REST APIs on top of Django. django-cors-headers==4.9.0 lets your frontend (running on a different port) communicate with the backend.

You should see a success message for each package once the installation finishes.

Seeing an installation error?

Make sure (venv) appears in your prompt before running pip install. Installing without an active virtual environment can cause permission errors.

If you see a network timeout, check your internet connection and try the command again.

help me fix a pip install error

  • Now record your dependencies by creating a requirements.txt file. In VS Code, right-click inside the Explorer sidebar and select New File.
  • Name the file requirements.txt and paste the following content:
django==5.2.14
djangorestframework==3.17.1
django-cors-headers==4.9.0

What is requirements.txt?

This file lists the exact package versions your project needs. Anyone who clones your project can run pip install -r requirements.txt to install the same packages you used.

  • Save requirements.txt.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

django==5.2.14
djangorestframework==3.17.1
django-cors-headers==4.9.0
Create a Django project and configure settings

With your packages installed, you'll now scaffold the Django project structure and configure it to use DRF and CORS.

  • Create a new Django project called taskproject by running this command:
django-admin startproject taskproject .

What does the dot at the end mean?

The . tells Django to create the project in the current directory instead of making a nested folder. This keeps your file structure cleaner.

  • Create a Django app called tasks by running:
python manage.py startapp tasks

What's the difference between a project and an app?

A Django project is the overall configuration container. An app is a modular piece of functionality inside that project.

Your taskproject is the project, and tasks is the app that will handle all task-related logic.

  • Confirm the files were created by running:
ls

You should see manage.py, a taskproject/ folder, a tasks/ folder, a venv/ folder, and requirements.txt in the output.

Now you need to tell Django about the new packages and app you installed. All of this configuration lives in settings.py.

  • In the VS Code Explorer sidebar, expand the taskproject folder and open settings.py.
  • Find the INSTALLED_APPS list (around line 33) and replace it with:
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "corsheaders",
    "tasks",
]

What did you add?

You added three entries at the bottom of the list. rest_framework enables Django REST Framework's API tools. corsheaders enables cross-origin request handling. tasks registers the app you just created.

  • Now find the MIDDLEWARE list (just below INSTALLED_APPS) and replace it with:
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

Why does CorsMiddleware go at the top?

CORS middleware must run before any other middleware that might generate a response. Placing it at the top ensures cross-origin requests are handled before Django processes the request further.

  • Scroll to the very bottom of settings.py and add this new setting:
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]

What is CORS_ALLOWED_ORIGINS?

This tells Django which origins (domains + ports) are allowed to make requests to your API. Port 5173 is where your React frontend will run later.

Without this setting, the browser would block your frontend from contacting the backend.

  • Save settings.py.

You won't see a visible change yet. The next substep verifies everything works by starting the server.

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before moving on.

ⓧ I'd like to double check the full code

Here are the three sections you changed in settings.py. Make sure they match exactly:

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

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]
Verify the Django development server starts

Time to confirm everything is wired up correctly by starting the Django development server.

  • Start the development server by running:
python manage.py runserver

You'll see some warnings about unapplied migrations. That's normal for now. Look for a line that says the server is running at http://127.0.0.1:8000/.

  • Open your browser and go to http://localhost:8000.

You should see Django's default congratulations page with a rocket icon. This confirms your Django project is set up correctly.

Don't see the rocket page?

Make sure your terminal still shows (venv) in the prompt. If it disappeared, reactivate the virtual environment and run the server again.

If you see an error about a port already in use, another process is using port 8000. Close other terminal sessions or applications that might be running a server.

help me get the Django development server running

Keep the server running

Leave this terminal running for the rest of the project. When you need to run more commands later, open a new terminal tab with Ctrl+Shift+` instead of stopping the server.

Your Django backend is scaffolded and ready to serve data. Next up, you'll build the actual REST API that your React frontend will talk to.

Build the REST API

Your Django project is configured and ready to serve requests. Now you need to give it something to serve.

In this step, you'll build a complete REST API for managing tasks. This means defining what a task looks like in the database, creating a way to convert it to JSON, and exposing URL endpoints that a frontend can call.

In this step, get ready to:
  • Define a Task model and apply it to the database.
  • Create a serializer and ViewSet to expose the model as an API.
  • Wire up URL routing and test the API in the browser.
Define the Task model and run migrations

A model in Django defines the structure of a database table. Each field on the model becomes a column in the table.

  • In VS Code, open backend/tasks/models.py from the **Explorer** sidebar.
  • Replace everything in the file with the following code:
from django.db import models


class Task(models.Model):
    # The task's title, limited to 200 characters
    title = models.CharField(max_length=200)
    # Whether the task is complete (defaults to not complete)
    completed = models.BooleanField(default=False)
    # Automatically records when the task was created
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

What does this code do?

The Task class defines a database table with three columns. title stores the task name, completed tracks whether it's done, and created_at automatically stamps when it was created.

The __str__ method tells Django how to display a task in the admin and shell. It returns the title instead of something like "Task object (1)".

  • Save models.py.
  • Generate the migration file for your new model by running this command in your terminal:
python manage.py makemigrations

You should see output mentioning Migrations for 'tasks' and a Create model Task operation.

  • Apply the migration to your SQLite database by running:
python manage.py migrate

You should see lines like Applying tasks.0001_initial... OK confirming the table was created.

Seeing an error about no changes detected?

Make sure you saved models.py before running makemigrations. Django reads the saved file, not what's in your editor buffer.

Also check that "tasks" appears in your INSTALLED_APPS list in settings.py. Django only detects models from registered apps.

Help me fix my makemigrations error.

✔️ Awesome, I've got everything!

Great. Make sure models.py is saved and your migration applied successfully.

ⓧ I'd like to double check the full code

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)

    def __str__(self):
        return self.title
Create a serializer and ViewSet

Your model stores data in the database, but the API needs to send that data as JSON over HTTP. A serializer handles that conversion.

  • Create a new file called serializers.py inside the backend/tasks/ folder.
  • In VS Code, right-click the tasks folder in the **Explorer** sidebar and select **New File**.
  • Name the file serializers.py and add the following code:
from rest_framework import serializers
from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        # Which model to serialize
        model = Task
        # Which fields to include in the JSON output
        fields = ["id", "title", "completed", "created_at"]

What is a serializer?

A serializer translates a Django model instance into JSON (and vice versa). ModelSerializer automatically generates the conversion logic based on the model fields you list.

The fields list controls exactly which columns appear in the API response. Django auto-generates the id field, so you include it here even though you didn't define it in the model.

  • Save serializers.py.

You won't see a visible change yet. The next file uses this serializer to build the API view.

✔️ Awesome, I've got everything!

Great. Make sure you saved serializers.py before moving on.

ⓧ I'd like to double check the full code

from rest_framework import serializers
from .models import Task


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

Now you need a view that handles incoming HTTP requests and uses the serializer to respond with JSON.

  • Open backend/tasks/views.py in the **Explorer** sidebar.
  • Replace everything in the file with the following code:
from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    # Return all tasks, newest first
    queryset = Task.objects.all().order_by("-created_at")
    # Use TaskSerializer to convert Task objects to/from JSON
    serializer_class = TaskSerializer

What is a ModelViewSet?

A ModelViewSet from Django REST Framework provides all the standard CRUD operations (list, create, retrieve, update, delete) in a single class. You only need to specify the queryset and serializer.

The order_by("-created_at") ensures the most recently created tasks appear first. The minus sign means descending order.

  • Save views.py.

You won't see a change yet. The next substep connects these views to URL routes.

✔️ Awesome, I've got everything!

Great. Make sure you saved views.py before moving on.

ⓧ I'd like to double check the full code

from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all().order_by("-created_at")
    serializer_class = TaskSerializer
Wire up the URL router and test the API

Your ViewSet defines what the API does, but Django doesn't know which URLs should trigger it. A router automatically generates the URL patterns for your ViewSet.

  • Create a new file called urls.py inside the backend/tasks/ folder.
  • Right-click the tasks folder in the **Explorer** sidebar and select **New File**.
  • Name the file urls.py and add the following code:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

# Create a router and register the TaskViewSet
router = DefaultRouter()
router.register(r"tasks", TaskViewSet)

urlpatterns = [
    # Include all router-generated URLs
    path("", include(router.urls)),
]

What does DefaultRouter do?

A DefaultRouter inspects your ViewSet and automatically creates URL patterns for all CRUD operations. Registering TaskViewSet under "tasks" generates endpoints like /tasks/ (list and create) and /tasks/1/ (retrieve, update, delete).

  • Save tasks/urls.py.

Now connect the app-level URLs to the project's main URL configuration.

  • Open backend/taskproject/urls.py in the **Explorer** sidebar.
  • Replace everything in the file with the following code:
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    # All API routes live under /api/
    path("api/", include("tasks.urls")),
]

Why the api/ prefix?

Prefixing all API routes with api/ keeps them separate from other URL paths (like the admin panel). This means your task endpoints live at /api/tasks/ rather than just /tasks/.

  • Save taskproject/urls.py.
  • Start the Django development server by running:
python manage.py runserver
  • Open your browser and navigate to http://localhost:8000/api/tasks/.

You should see DRF's browsable API page. It displays a formatted HTML interface with the page title "Task List" at the top, an empty list [] in the response body, and a form at the bottom where you can POST new tasks.

What is the browsable API?

Django REST Framework automatically renders a human-friendly HTML page when you visit an API endpoint in a browser. You can use the built-in forms to create, update, and delete data without writing any frontend code.

This is a great tool for testing your API as you build it. Append ?format=json to any URL to see the raw JSON response instead.

Seeing a Page not found error?

Make sure you included the trailing slash in the URL: http://localhost:8000/api/tasks/ (not http://localhost:8000/api/tasks). Django's URL routing requires the trailing slash by default.

Also check that taskproject/urls.py includes tasks.urls under the "api/" prefix. If you accidentally used a different prefix, the URL will be different.

Help me fix my browsable API not loading.

✔️ Awesome, I've got everything!

Great. Make sure all files are saved.

ⓧ I'd like to double check the full code

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

router = DefaultRouter()
router.register(r"tasks", TaskViewSet)

urlpatterns = [
    path("", include(router.urls)),
]
from django.contrib import admin
from django.urls import path, include

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

Your REST API is live and serving data. Next up, you'll build the React frontend that communicates with these endpoints.

Set Up the Frontend (React + Vite)

Your Django API is ready to serve task data. Now you need something to display it.

In this step, you will scaffold a React frontend using Vite, a fast build tool that gives you instant hot-reloading as you develop. You will also install Axios, a library for making HTTP requests to your API.

In this step, get ready to:
  • Scaffold a new React project with Vite.
  • Install Axios for HTTP requests.
  • Clean up the default template and verify the dev server runs.
Scaffold a new React project with Vite

Your frontend will live in a separate frontend/ directory alongside your backend/ directory. This separation mirrors how modern full-stack teams structure their codebases.

  • Open a new terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Navigate to your project's root directory (the parent folder that contains backend/) by running:
cd ~/Desktop/task-manager
  • Scaffold the React frontend by running this command:
npm create vite@latest frontend -- --template react

What does this command do?

This tells npm to run the create-vite scaffolding tool. frontend is the folder name it creates. The -- --template react part tells Vite to use the React JavaScript template without prompting you interactively.

  • Move into the new frontend/ directory by running:
cd frontend
  • Install the project's default dependencies by running:
npm install
  • Confirm the files exist by running:
ls src
  • You should see files like App.jsx, App.css, and main.jsx in the output.

Seeing an error about npm not being found?

This means Node.js is not installed or not on your system path. Go back to Step 1 and make sure Node.js 20.19+ is installed correctly.

help me troubleshoot why npm is not found on my system

Install Axios for HTTP requests

Your React app needs a way to send HTTP requests to the Django API. Axios handles this with a clean syntax and features like automatic JSON parsing.

  • Install Axios by running this command (make sure you are inside the frontend/ directory):
npm install axios
  • Confirm axios was added by checking package.json:
cat package.json
  • You should see axios listed under dependencies in the output.

Don't see axios in your dependencies?

Make sure you ran npm install axios from inside the frontend/ directory, not from the project root or backend/.

help me figure out why axios did not install in my frontend project

Clean up the default template and start the dev server

Vite's default template includes demo content you do not need. You will clear it out so you have a blank canvas for the Task Manager UI.

  • In VS Code, open the file frontend/src/App.css from the Explorer sidebar.
  • Select all the content (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
  • Save the empty file.
  • Open frontend/src/App.jsx and replace all of its content with this minimal placeholder:
function App() {
  return (
    <div className="app">
      <h1>Task Manager</h1>
      <p>Frontend is ready.</p>
    </div>
  );
}

export default App;

What does this code do?

This is the simplest possible React component. It renders a heading and a paragraph to confirm the frontend is working. You will replace this with the full Task Manager UI in the next step.

  • Save App.jsx.
  • Start the Vite dev server by running this command in your terminal (still inside frontend/):
npm run dev
  • Open your browser and go to http://localhost:5173.
  • You should see a page with the heading "Task Manager" and the text "Frontend is ready." displayed.

Don't see your page at localhost:5173?

Check your terminal output for a different port number. If port 5173 is already in use, Vite automatically picks the next available port and displays it in the terminal.

Also make sure you ran npm install before npm run dev. Without the initial install, Vite's dependencies are missing.

help me troubleshoot why my Vite dev server is not showing my React app

Your frontend skeleton is in place and serving pages. Next up, you will connect this React app to your Django API so it can fetch and display real task data.

Connect React to the Django API

You have a Django REST API that serves task data and a React app waiting to display it. These two pieces live on different servers and need a bridge between them.

In this step, you'll use Axios to send HTTP requests from your React frontend to the Django backend. You'll build the full Task Manager interface that can fetch, create, and delete tasks.

In this step, get ready to:
  • Create an API utility module configured with the backend URL.
  • Build a component that fetches and displays tasks on page load.
  • Add a form to create tasks and buttons to delete them.
Create the API utility module

Every component that talks to the backend will need to know where the API lives. Instead of repeating the URL in every file, you'll create a single utility module that exports a pre-configured Axios instance.

  • In VS Code, create a new file called api.js inside the frontend/src/ folder.
  • Add the following code to api.js:
import axios from "axios";

const api = axios.create({
  baseURL: "http://localhost:8000/api",
});

export default api;

What does this code do?

axios.create() builds a reusable HTTP client with a base URL baked in. Every request you make with this api object automatically prepends http://localhost:8000/api to the path you provide.

So api.get("/tasks/") becomes a GET request to http://localhost:8000/api/tasks/.

  • Save api.js.

Why a separate utility file?

If you ever change the backend URL (for example, when deploying to a real server), you only need to update one file instead of hunting through every component.

Build the component that fetches and displays tasks

The main App component will load tasks from the API as soon as the page renders. React's useEffect hook is the standard way to run code on mount.

  • Open frontend/src/App.jsx in VS Code.
  • Replace its contents with the following code:
import { useState, useEffect } from "react";
import api from "./api";
import "./App.css";

function App() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    fetchTasks();
  }, []);

  const fetchTasks = async () => {
    try {
      const response = await api.get("/tasks/");
      setTasks(response.data);
    } catch (error) {
      console.error("Error fetching tasks:", error);
    }
  };

  return (
    <div className="app">
      <h1>Task Manager</h1>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <span>{task.title}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

What does this code do?

  • useState([]) creates a tasks variable that holds an array of task objects from the API.
  • useEffect(..., []) with an empty dependency array runs fetchTasks exactly once when the component first loads.
  • fetchTasks sends a GET request to /tasks/ and stores the response data in state.
  • The tasks.map() in the return block renders one <li> for each task in the array.
  • Save App.jsx.

You won't see results yet because the form and styles still need to be added.

Add create and delete functionality

A task manager that only reads data is not very useful. You need a form to add tasks and a button to remove them.

  • In App.jsx, replace the entire file contents with this updated version:
import { useState, useEffect } from "react";
import api from "./api";
import "./App.css";

function App() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState("");

  useEffect(() => {
    fetchTasks();
  }, []);

  const fetchTasks = async () => {
    try {
      const response = await api.get("/tasks/");
      setTasks(response.data);
    } catch (error) {
      console.error("Error fetching tasks:", error);
    }
  };

  const addTask = async (e) => {
    e.preventDefault();
    if (!newTask.trim()) return;
    try {
      const response = await api.post("/tasks/", { title: newTask });
      setTasks([response.data, ...tasks]);
      setNewTask("");
    } catch (error) {
      console.error("Error adding task:", error);
    }
  };

  const deleteTask = async (id) => {
    try {
      await api.delete(`/tasks/${id}/`);
      setTasks(tasks.filter((task) => task.id !== id));
    } catch (error) {
      console.error("Error deleting task:", error);
    }
  };

  return (
    <div className="app">
      <h1>Task Manager</h1>
      <form onSubmit={addTask}>
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Add a new task..."
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <span>{task.title}</span>
            <button onClick={() => deleteTask(task.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

What's new in this version?

  • newTask state holds the text from the input field. The form is a controlled component, meaning React owns the input's value.
  • addTask sends a POST request with { title: newTask } and prepends the API response to the tasks array so the new task appears at the top instantly.
  • deleteTask sends a DELETE request and filters the removed task out of the local state so it disappears without a page reload.
  • The <form onSubmit={addTask}> wiring lets the user press Enter or click Add to submit.
  • Save App.jsx.

Now add styles to make the interface look clean.

  • Open frontend/src/App.css and replace its contents with the following:
.app {
  max-width: 600px;
  margin: 2rem auto;
  padding: 1rem;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #333;
}

form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

form input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

form button {
  padding: 0.5rem 1rem;
  background: #4a90d9;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
}

form button:hover {
  background: #357abd;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  border: 1px solid #eee;
  border-radius: 4px;
  margin-bottom: 0.5rem;
}

li button {
  padding: 0.25rem 0.5rem;
  background: #e74c3c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

li button:hover {
  background: #c0392b;
}

What do these styles do?

  • .app centers the content with a max width of 600px.
  • The form uses flexbox to place the input and button side by side.
  • Each li becomes a card-like row with the task title on the left and a red Delete button on the right.
  • Save App.css.
  • In the terminal, make sure you are inside the frontend folder and start the dev server by running:
npm run dev
  • Open http://localhost:5173 in your browser. You should see the Task Manager heading, a text input with placeholder text, and a blue Add button.

Seeing a blank page or errors?

  • Check that api.js is saved inside frontend/src/ (not frontend/ root). The import path ./api looks for a sibling file.
  • If you see a console error about Network Error, that is expected right now. The Django server is not running yet. The page still renders because the try/catch in fetchTasks handles the failure gracefully.

Why isn't my React app rendering anything at localhost:5173?

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files: api.js, App.jsx, and App.css.

ⓧ I'd like to double check the full code

Here are the three files you created or modified in this step.

import axios from "axios";

const api = axios.create({
  baseURL: "http://localhost:8000/api",
});

export default api;
import { useState, useEffect } from "react";
import api from "./api";
import "./App.css";

function App() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState("");

  useEffect(() => {
    fetchTasks();
  }, []);

  const fetchTasks = async () => {
    try {
      const response = await api.get("/tasks/");
      setTasks(response.data);
    } catch (error) {
      console.error("Error fetching tasks:", error);
    }
  };

  const addTask = async (e) => {
    e.preventDefault();
    if (!newTask.trim()) return;
    try {
      const response = await api.post("/tasks/", { title: newTask });
      setTasks([response.data, ...tasks]);
      setNewTask("");
    } catch (error) {
      console.error("Error adding task:", error);
    }
  };

  const deleteTask = async (id) => {
    try {
      await api.delete(`/tasks/${id}/`);
      setTasks(tasks.filter((task) => task.id !== id));
    } catch (error) {
      console.error("Error deleting task:", error);
    }
  };

  return (
    <div className="app">
      <h1>Task Manager</h1>
      <form onSubmit={addTask}>
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Add a new task..."
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <span>{task.title}</span>
            <button onClick={() => deleteTask(task.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;
.app {
  max-width: 600px;
  margin: 2rem auto;
  padding: 1rem;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #333;
}

form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

form input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

form button {
  padding: 0.5rem 1rem;
  background: #4a90d9;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
}

form button:hover {
  background: #357abd;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  border: 1px solid #eee;
  border-radius: 4px;
  margin-bottom: 0.5rem;
}

li button {
  padding: 0.25rem 0.5rem;
  background: #e74c3c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

li button:hover {
  background: #c0392b;
}

Your frontend is ready to talk to the backend. Next up, you'll run both servers at the same time and test the full create-read-delete flow end to end.

Test the Full Stack End-to-End

You have a Django backend and a React frontend ready to go. Now it's time to run both servers at the same time and prove they talk to each other.

This is the moment everything comes together. You'll add tasks through the React UI, watch them persist in the Django database, and inspect the actual HTTP requests flying between the two servers.

In this step, get ready to:
  • Run both the Django and Vite dev servers simultaneously.
  • Add, view, and delete tasks through the React UI to confirm full CRUD works.
  • Inspect network requests in browser DevTools to understand the request/response cycle.
Run both servers simultaneously

A full-stack app needs both its backend and frontend running at the same time. The Django server handles API requests on port 8000, and the Vite dev server serves your React app on port 5173.

You'll need two separate terminal windows for this, one for each server.

  • In VS Code, open a new terminal by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows).
  • Start the Django development server by running these commands:
cd backend
python manage.py runserver

What does this command do?

python manage.py runserver starts Django's built-in development server on http://localhost:8000. This server listens for incoming API requests from your React frontend.

  • You should see output including a line that says the server is running at http://127.0.0.1:8000/.
  • Open a second terminal by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows) again.
  • Start the Vite dev server by running these commands:
cd frontend
npm run dev
  • You should see Vite output with a local URL at http://localhost:5173.
  • Open your browser and navigate to http://localhost:5173.

You should see the Task Manager heading with an empty task list and an input field. Both servers are now running simultaneously.

Seeing a blank page or connection error?

Make sure the Django server is still running in the first terminal. If it crashed, check for any error messages and restart it with python manage.py runserver.

If you see a CORS error in the browser console, confirm that http://localhost:5173 is listed in CORS_ALLOWED_ORIGINS in your settings.py.

Help me debug why my React app can't connect to my Django API.

Add, view, and delete tasks through the React UI

Your React frontend sends HTTP requests to the Django REST API every time you interact with the task list. Each action (adding, viewing, deleting) triggers a different HTTP method.

  • Type Buy groceries into the input field.
  • Click the Add button.

The task should appear immediately below the form. This sent a POST request to your Django API, which saved the task to the SQLite database and returned the new task data.

  • Add a second task by typing Walk the dog and clicking Add.
  • Add a third task by typing Read a book and clicking Add.

You should now see all three tasks listed, with the most recently added task at the top.

  • Refresh the browser page by pressing Cmd+R (macOS) or Ctrl+R (Windows).

All three tasks are still there. This confirms that your data persists in the Django database, not just in React's temporary state.

  • Click the Delete button next to any task.

The task disappears immediately. This sent a DELETE request to the Django API, which removed the task from the database permanently.

Why does the data survive a page refresh?

When you add a task, the React frontend sends the data to Django. Django writes it to a SQLite database file on disk. When you refresh the page, React calls fetchTasks() on mount, which reads from that database again.

This is the core benefit of connecting a frontend to a backend with a database. Your data outlives the browser session.

Tasks not appearing after you click Add?

Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for red error messages. A CORS error means your settings.py needs to include both http://localhost:5173 and http://127.0.0.1:5173 in CORS_ALLOWED_ORIGINS.

Also check that the baseURL in api.js matches exactly: http://localhost:8000/api (no trailing slash).

Help me figure out why tasks aren't saving from my React app to Django.

Inspect network requests in browser DevTools

Every interaction in your Task Manager triggers an HTTP request that travels from your React frontend to the Django backend. The browser's Network tab lets you see exactly what gets sent and received.

  • Open browser DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
  • Click the Network tab at the top of the DevTools panel.
  • Refresh the page to see the initial GET request.

You should see a request to /api/tasks/ with a status of 200. This is your React app fetching all tasks from the Django API when the page loads.

  • Add a new task (type anything and click Add).
  • In the Network tab, click the new tasks/ request that just appeared.

Click on the request to see its details. You should see the method is POST and the status is 201 (Created).

  • Click the Delete button on any task and watch the Network tab.

You should see a DELETE request with a status of 204 (No Content). This means Django successfully deleted the task and returned no body.

What do the HTTP status codes mean?

  • 200 (OK): The GET request succeeded and returned your tasks as JSON.
  • 201 (Created): The POST request created a new task in the database.
  • 204 (No Content): The DELETE request removed the task successfully.

These status codes are part of the HTTP standard. Django REST Framework sends them automatically based on the action your ModelViewSet handles.

Why two separate servers?

This architecture (React on one port, Django on another) mirrors how production full-stack apps work. The frontend is a standalone app that communicates with the backend purely through HTTP requests.

CORS is what makes this cross-origin communication possible. Without the django-cors-headers middleware you configured earlier, the browser would block every request from port 5173 to port 8000.

Your full-stack Task Manager is fully operational. You've proven that data flows from the React UI, through HTTP requests, into the Django API, and persists in the database. Next up, you'll tackle the Secret Mission and add edit functionality.

Secret mission

Add Edit Task Functionality

Your Task Manager can create and delete tasks, but what about updating them? Add inline editing so users can click an Edit button, modify a task's title directly in the list, and save changes with a PATCH request to the API.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The backend folder containing your Django project, Python virtual environment, and SQLite database.
  • The frontend folder containing your React project and node_modules.
  • Two running development servers (Django on port 8000 and Vite on port 5173).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your Task Manager app.

  • Your Django and Vite dev servers will keep running in the terminals where you started them.
  • Your task data persists in the SQLite database at backend/db.sqlite3.
  • Python and Node.js remain installed on your machine.

Pause - I'll come back to this later

Stop the running servers to free up terminal sessions and memory. Your project files stay on disk so you can restart later.

  • In the terminal running the Django server, press Ctrl+C to stop it.
  • In the terminal running the Vite dev server, press Ctrl+C to stop it.
  • When you come back, activate the virtual environment and run python manage.py runserver from the backend folder.
  • Run npm run dev from the frontend folder to restart the React app.

Delete - I don't want to use this again

Remove all project files and start fresh. This permanently deletes your code and task data.

  • In the terminal running the Django server, press Ctrl+C to stop it.
  • In the terminal running the Vite dev server, press Ctrl+C to stop it.

Delete the backend folder by running:

macOS

rm -rf ~/Desktop/backend

Windows

rmdir /s /q %USERPROFILE%\Desktop\backend

Delete the frontend folder by running:

macOS

rm -rf ~/Desktop/frontend

Windows

rmdir /s /q %USERPROFILE%\Desktop\frontend

Nice Work!

Nice Work!

Nice work! You just built a full-stack Task Manager with a Django REST API backend and a React frontend that communicates with it over HTTP.

You've learned how to:

  • Built a Django REST API with models, serializers, and viewsets that serve JSON data from a SQLite database.
  • Configured CORS to allow cross-origin requests between two development servers running on different ports.
  • Connected a React frontend to the live API using Axios, implementing full Create, Read, and Delete operations spanning both frontend and backend.
  • Added inline edit functionality with PATCH requests so users can update task titles without leaving the page.

Ready to quiz yourself?