How to Build a Full-Stack TODO App: A Step-by-Step Django & React Tutorial
How to Build a Full-Stack TODO App with Django & React
Building a full-stack application can seem daunting, but it’s an incredibly rewarding skill. By combining Django, a powerful Python web framework, with React, a modern JavaScript library for user interfaces, you can create dynamic, data-driven applications. In this comprehensive tutorial, we’ll walk through building a complete TODO application step-by-step. You’ll learn how to construct a RESTful API with Django REST Framework and connect it to an interactive React frontend. Let’s dive in and turn your ideas into a working application!
Project Overview and Setup
Our project is split into two main parts: the backend (API) built with Django and the frontend built with React. They will communicate via HTTP requests (GET, POST, PUT, DELETE). We’ll start by setting up our development environment.
To watch the full tutorial on YouTube, click here.
Prerequisites
Before starting, ensure you have the following installed:
- Python (3.8 or higher)
- Node.js and npm (Node Package Manager)
- Git (for version control and cloning our repository)
- A code editor (like VS Code)
Cloning the Starter Repository
We have prepared a complete codebase for you. Open your terminal and clone the repository to get all the necessary files.
git clone https://github.com/chrisHalogen/todo-app.git
cd todo-app
Inside the repository, you will find two main directories: backend/ for Django and frontend/ for React. Let’s explore and set up each one.
Pro Tip: It’s highly recommended to use a virtual environment for Python projects to manage dependencies. We’ll set one up in the next step.
Part 1: Building the Django REST API Backend
Our backend will handle data storage, business logic, and serve as a secure API endpoint for our React app.
Setting Up the Django Project & Virtual Environment
Navigate to the backend directory and create a Python virtual environment. This keeps our project’s dependencies isolated.
cd backend
python -m venv venv
Activate the virtual environment:
- On macOS/Linux:
source venv/bin/activate - On Windows:
venv\Scripts\activate
Once activated, install the required Python packages from the requirements.txt file.
pip install -r requirements.txt
Understanding the Django Application Structure
Let’s look at the key files in our backend directory that make our API work.
manage.py: The Django command-line utility.backend/settings.py: Main configuration file for the Django project.backend/urls.py: The root URL router for the project.todos/: This is our Django “app” dedicated to the TODO functionality.
Creating the Todo Model
The model defines the structure of our data in the database. Open todos/models.py.
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
This simple model has four fields: a title, an optional description, a completion status, and an automatic timestamp. The __str__ method defines how a Todo object is displayed in the Django admin.
Creating Serializers with Django REST Framework
Serializers convert complex data types (like Django model instances) into native Python datatypes that can be easily rendered into JSON. Create or open todos/serializers.py.
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = '__all__' # Includes all model fields: id, title, description, completed, created_at
read_only_fields = ('id', 'created_at') # These fields are set automatically
Building API Views (Function-Based Views)
Views handle the logic for processing API requests. We are using function-based views decorated with @api_view. Open todos/views.py.
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Todo
from .serializers import TodoSerializer
@api_view(['GET', 'POST'])
def todo_list(request):
"""
List all todos, or create a new todo.
"""
if request.method == 'GET':
todos = Todo.objects.all().order_by('-created_at')
serializer = TodoSerializer(todos, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = TodoSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET', 'PUT', 'DELETE'])
def todo_detail(request, pk):
"""
Retrieve, update or delete a single todo instance.
"""
try:
todo = Todo.objects.get(pk=pk)
except Todo.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer = TodoSerializer(todo)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = TodoSerializer(todo, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
todo.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Configuring URLs
We need to map our views to specific URL paths. First, in the app’s todos/urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('todos/', views.todo_list, name='todo_list'),
path('todos/<int:pk>/', views.todo_detail, name='todo_detail'),
]
Then, include these app URLs in the project’s main backend/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('todos.urls')), # All our API routes will be under /api/
]
Running Migrations and Starting the Server
Migrations translate our model into database tables. Run the following commands:
python manage.py makemigrations
python manage.py migrate
Now, let’s create a superuser to access the Django admin interface.
python manage.py createsuperuser
Finally, start the Django development server:
python manage.py runserver
Your API is now live at http://127.0.0.1:8000/api/todos/. You can also visit /admin to log in and manage todos directly.
Key Takeaway: At this point, you have a fully functional REST API capable of CRUD (Create, Read, Update, Delete) operations. Test it using a tool like Postman or your browser before moving to the frontend.
Part 2: Building the React Frontend
Now, let’s build the user interface that will consume our Django API. Open a new terminal window, navigate to the project root, and then into the frontend directory.
Installing Dependencies and Project Structure
cd frontend
npm install
This installs all required packages (like React, Axios for HTTP requests, and React Icons). Let’s examine the main files:
src/App.js: The main React component.src/components/: Contains reusable components likeTodoList.js,TodoForm.js.src/services/: Contains theapi.jsfile which configures Axios to talk to our Django backend.
API Service Configuration
The src/services/api.js file sets up a base Axios instance, defining the base URL of our Django API. This is crucial for connecting the frontend to the backend.
import axios from 'axios';
const api = axios.create({
baseURL: 'http://127.0.0.1:8000/api/', // Your Django server address
});
export default api;
Main App Component and State Management
Open src/App.js. This component is the brain of our frontend. It manages the state (the list of todos) and coordinates data flow between the form and the list.
import React, { useState, useEffect } from 'react';
import './App.css';
import TodoList from './components/TodoList';
import TodoForm from './components/TodoForm';
import api from './services/api';
function App() {
const [todos, setTodos] = useState([]);
const [editingTodo, setEditingTodo] = useState(null);
// Fetch todos from API on component mount
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
const response = await api.get('/todos/');
setTodos(response.data);
} catch (error) {
console.error('Error fetching todos:', error);
}
};
const addTodo = async (todo) => {
try {
const response = await api.post('/todos/', todo);
setTodos([response.data, ...todos]); // Add new todo to the start of the list
} catch (error) {
console.error('Error adding todo:', error);
}
};
const updateTodo = async (updatedTodo) => {
try {
const response = await api.put(`/todos/${updatedTodo.id}/`, updatedTodo);
setTodos(todos.map(todo => (todo.id === updatedTodo.id ? response.data : todo)));
setEditingTodo(null); // Exit editing mode
} catch (error) {
console.error('Error updating todo:', error);
}
};
const deleteTodo = async (id) => {
try {
await api.delete(`/todos/${id}/`);
setTodos(todos.filter(todo => todo.id !== id)); // Remove todo from state
} catch (error) {
console.error('Error deleting todo:', error);
}
};
const toggleComplete = async (todo) => {
const updatedTodo = { ...todo, completed: !todo.completed };
await updateTodo(updatedTodo);
};
return (
<div className="app">
<header className="app-header">
<h1>My TODO List</h1>
</header>
<main className="app-main">
<TodoForm
addTodo={addTodo}
editingTodo={editingTodo}
updateTodo={updateTodo}
/>
<TodoList
todos={todos}
deleteTodo={deleteTodo}
toggleComplete={toggleComplete}
setEditingTodo={setEditingTodo}
/>
</main>
</div>
);
}
export default App;
Building the TodoForm Component
This component (src/components/TodoForm.js) handles both creating new todos and editing existing ones. It uses controlled form inputs.
import React, { useState, useEffect } from 'react';
import { FaSave, FaPlusCircle } from 'react-icons/fa';
const TodoForm = ({ addTodo, editingTodo, updateTodo }) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
// If we are editing a todo, pre-fill the form
useEffect(() => {
if (editingTodo) {
setTitle(editingTodo.title);
setDescription(editingTodo.description || '');
} else {
setTitle('');
setDescription('');
}
}, [editingTodo]);
const handleSubmit = (e) => {
e.preventDefault();
if (!title.trim()) return; // Don't submit empty titles
const todoData = { title, description };
if (editingTodo) {
updateTodo({ ...editingTodo, ...todoData });
} else {
addTodo(todoData);
}
// Reset form
setTitle('');
setDescription('');
};
return (
<form className="todo-form" onSubmit={handleSubmit}>
<div className="form-group">
<input
type="text"
placeholder="What needs to be done?"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="form-input"
/>
</div>
<div className="form-group">
<textarea
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="form-textarea"
/>
</div>
<button type="submit" className="btn btn-primary">
{editingTodo ? (<><FaSave /> Update Todo</>) : (<><FaPlusCircle /> Add Todo</>)}
</button>
</form>
);
};
export default TodoForm;
Building the TodoList and TodoItem Components
The TodoList component (src/components/TodoList.js) maps over the todos array and renders a TodoItem for each one.
import React from 'react';
import TodoItem from './TodoItem';
const TodoList = ({ todos, deleteTodo, toggleComplete, setEditingTodo }) => {
return (
<div className="todo-list">
{todos.length === 0 ? (
<p className="no-todos">No todos yet. Add one above!</p>
) : (
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
deleteTodo={deleteTodo}
toggleComplete={toggleComplete}
setEditingTodo={setEditingTodo}
/>
))
)}
</div>
);
};
export default TodoList;
The TodoItem component (src/components/TodoItem.js) displays a single todo and its action buttons.
import React from 'react';
import { FaCheck, FaTimes, FaEdit, FaTrash } from 'react-icons/fa';
const TodoItem = ({ todo, deleteTodo, toggleComplete, setEditingTodo }) => {
return (
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<div className="todo-content" onClick={() => toggleComplete(todo)}>
<div className="todo-status">
{todo.completed ? <FaCheck className="icon-check" /> : <div className="status-pending"></div>}
</div>
<div className="todo-text">
<h3 className="todo-title">{todo.title}</h3>
{todo.description && <p className="todo-description">{todo.description}</p>}
<small className="todo-date">Created: {new Date(todo.created_at).toLocaleDateString()}</small>
</div>
</div>
<div className="todo-actions">
<button onClick={() => setEditingTodo(todo)} className="btn btn-edit">
<FaEdit />
</button>
<button onClick={() => deleteTodo(todo.id)} className="btn btn-delete">
<FaTrash />
</button>
</div>
</div>
);
};
export default TodoItem;
Styling the Application
The visual appeal comes from src/App.css. The provided CSS uses Flexbox for layout, clear visual feedback for completed items, and responsive button styling. Key styles include the .completed class for strikethrough text and distinct colors for primary, edit, and delete actions.
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.todo-item.completed .todo-title {
text-decoration: line-through;
color: #888;
}
.btn-primary {
background-color: #4CAF50;
color: white;
}
.btn-edit {
background-color: #2196F3;
color: white;
}
.btn-delete {
background-color: #f44336;
color: white;
}
Running the React Development Server
With the Django backend still running, start the React frontend in your terminal (from the frontend directory):
npm start
Your application will open in your browser at http://localhost:3000. You can now add, edit, complete, and delete TODOs. The changes will be saved to your Django backend via the API!
Handling Cross-Origin Resource Sharing (CORS)
Since your frontend (localhost:3000) and backend (localhost:8000) are on different origins (ports), browsers block requests by default due to the same-origin policy. We installed and configured django-cors-headers in the backend to allow these requests. The settings are already in backend/settings.py.
Conclusion and Next Steps
Congratulations! You’ve successfully built a full-stack TODO application. You’ve learned how to:
- Create a Django model and REST API with Django REST Framework.
- Build a React frontend with functional components and Hooks (
useState,useEffect). - Perform CRUD operations by connecting React to a Django API using Axios.
- Style a responsive and user-friendly interface.
Where to Go From Here: This project is a solid foundation. Consider enhancing it by adding user authentication (with Django’s
django-rest-authordj-rest-auth), task due dates, categories, or deploying it to a platform like Heroku or DigitalOcean.
To watch the full tutorial on YouTube, click here.
Feel free to explore the complete source code on GitHub and modify it to fit your needs. Happy coding from the team at Halogenius Ideas!
Leave a Reply