• 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:

  1. Set up Django and DRF.

  2. Build a Task model to store to-dos.

  3. Add serializers to handle data safely.

  4. Create views to control the API.

  5. Set up URLs to connect everything.

  6. 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:

  1. Log in at http://localhost:8000/api-auth/login/ with psyduck.

  2. Visit /api/tasks/ to see your tasks.

  3. 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 👈