- George
- Posts
- How to Build a To-Do App with Django Rest Framework
How to Build a To-Do App with Django Rest Framework
A tutorial for beginners with a focus on security
Building a to-do app is a fun way to learn how to create APIs—think of them as messengers that let apps talk to each other.
In this tutorial, we’ll use Django Rest Framework (DRF) to make a secure to-do app where users can manage their tasks. We’ll focus on keeping things safe, so no one can sneak into someone else’s to-do list!
If you just want the code, grab it on my Github repo 👈

An Overview
This app lets users create, view, update, and delete tasks. We’ll use DRF to turn our Django app into an API that a mobile app or website could use. Security is key—we’ll make sure only logged-in users see their own tasks, and we’ll lock things down with tokens and rules to stop bad stuff like hackers messing with our data.
Here’s what we’ll do:
Set up Django and DRF.
Build a Task model to store to-dos.
Add serializers to handle data safely.
Create views to control the API.
Set up URLs to connect everything.
Add security with authentication and permissions.
Let’s get started!
Step 1: Set Up
First, we need to set up our project. This assumes you have python, pip installed already
Create a Project
Open your terminal and run these commands:
# Install Django and DRF
pip install django djangorestframework
# Start a new Django project called "todo_project"
django-admin startproject todo_project
# Go into the project folder
cd todo_project
# Create an app called "todo"
python manage.py startapp todo
Configure Settings
Open todo_project/settings.py and add our apps:
# List of apps Django uses
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # DRF for building APIs
'todo', # Our to-do app
]
# DRF settings for security
REST_FRAMEWORK = {
# Who can use the API? Only logged-in users with tokens
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', # Uses a token for login
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', # Must be logged in
],
# Limit how many requests people can make (stops abuse)
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle', # For non-logged-in users
'rest_framework.throttling.UserRateThrottle', # For logged-in users
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day', # 100 requests per day if not logged in
'user': '1000/day', # 1000 requests per day if logged in
},
}
What This Does: Adds DRF and our todo app to the project. Sets up security so only logged-in users with a special token can use the API, and limits how often people can call it to stop attacks.
Run this to set up the database:
python manage.py migrate
Step 2: Building the Model
Now, let’s create a Task model to store our to-dos. This is like a blueprint for what a task looks like.
Open todo/models.py and add:
from django.db import models
from django.contrib.auth.models import User
# A Task is something a user needs to do
class Task(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
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):
# Shows task as "title - completed" or "title - working on it"
status = "completed" if self.completed else "working on it"
return f"{self.title} - {status}"
What This Does:
user: Ties each task to a specific user (from Django’s built-in User system).
title: A short name for the task.
description: More info (can be empty).
completed: Tracks if it’s done.
created_at: Auto-sets the creation time.
Dunder string method: Makes tasks easy to read (e.g., "Buy milk - working on it").
Create the Database Table
Run these commands to update the database:
python manage.py makemigrations
python manage.py migrate
Step 3: Adding Serializers
Serializers turn our tasks into JSON (a format apps understand) and check that data is safe.
Create todo/serializers.py and add:
from rest_framework import serializers
from .models import Task
import re # For checking text
# Turns Task into JSON and checks data
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ['id', 'title', 'description', 'completed', 'created_at'] # What to show
read_only_fields = ['id', 'created_at'] # Can’t change these
# Check title is safe and not empty
def validate_title(self, value):
value = value.strip() # Remove extra spaces
if not value:
raise serializers.ValidationError("Title cannot be empty.")
# Only allow letters, numbers, and spaces (no weird symbols)
if not re.match(r'^[a-zA-Z0-9 ]+$', value):
raise serializers.ValidationError("Title can only have letters, numbers, and spaces.")
return value
What This Does:
fields: Lists what goes in the JSON.
read_only_fields: Stops changes to id and created_at for safety.
validate_title: Makes sure title isn’t empty and blocks special characters (like < or #) to stop hackers.
Step 4: Creating Views
Views control what the API does—like listing tasks or adding new ones.
Open todo/views.py and add:
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Task
from .serializers import TaskSerializer
# Controls the API for tasks
class TaskViewSet(viewsets.ModelViewSet):
serializer_class = TaskSerializer # Uses our serializer
permission_classes = [IsAuthenticated] # Only logged-in users allowed
# Only show tasks for the current user
def get_queryset(self):
return Task.objects.filter(user=self.request.user)
# When creating a task, link it to the current user
def perform_create(self, serializer):
serializer.save(user=self.request.user)
What This Does:
viewsets.ModelViewSet: Handles list, create, update, and delete actions.
permission_classes: Blocks non-logged-in users.
get_queryset: Limits tasks to the logged-in user (keeps data private).
perform_create: Ties new tasks to the user making them.
Step 5: Setting Up URLs
URLs connect the API to addresses like /api/tasks/.
Create todo/urls.py and add:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet
# Makes URLs for our API automatically
router = DefaultRouter()
router.register(r'tasks', TaskViewSet, basename='tasks') # Links "tasks" to TaskViewSet
urlpatterns = [
path('', include(router.urls)), # All API URLs go here
]
Update todo_project/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('todo.urls')), # Our to-do API
path('api-auth/', include('rest_framework.urls')), # Login page for testing
]
What This Does:
router.register: Sets up /api/tasks/ for listing and /api/tasks/1/ for details.
basename='tasks': Needed because we filter tasks manually.
Project URLs link to our app and add a login page.
Step 6: Adding Security
We’ve already added some security, but let’s make it even safer with tokens.
Enable Token Authentication
Add to INSTALLED_APPS in settings.py:
INSTALLED_APPS = [
...
'rest_framework.authtoken', # For tokens
]
Run:
python manage.py migrate
Create Users and Tokens
Make a superuser:
python manage.py createsuperuser
Generate a token in the shell:
python manage.py shell
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
user = User.objects.get(username='psyduck')
token = Token.objects.create(user=user)
print(token.key) # Copy this (e.g., "abc123...")
exit()
Test It
Start the server:
python manage.py runserver
Use curl to test:
# List tasks (use your token)
curl -H "Authorization: Token abc123..." http://localhost:8000/api/tasks/
What This Does:
Tokens prove who’s logged in—each user gets a unique key.
IsAuthenticated and get_queryset keep tasks private.
Throttling stops too many requests (like a hacker attack).
Wrap-Up
You’ve built a secure to-do API! Users can log in, manage their tasks, and keep them private. Try it out:
Log in at http://localhost:8000/api-auth/login/ with psyduck.
Visit /api/tasks/ to see your tasks.
Add more with curl or a tool like Postman.
If you want to look at my code, grab it on my Github repo 👈
Hi, my name is George 👋 I am building am affordable SaaS boilerplate to make building SaaS easier. Check out the free version here 👈