Django Rest Framework (DRF) is a comprehensive toolkit for building Web APIs with Django. It offers serialization, authentication, viewsets, and interactive documentation, making it an excellent choice for developing reliable and maintainable RESTful services.
In this tutorial, you'll build a task management API using Django Rest Framework and SQLite as the database backend.
Prerequisites
Before getting started, ensure you have:
- Python installed on your system (preferably Python 3.13 or higher)
- A basic understanding of Python, Django, and web development concepts
Step 1 — Setting up the Django Rest Framework project
In this section, you'll create the directory structure and install the necessary dependencies for the Django Rest Framework project.
First, create a new directory for your project and navigate into it:
mkdir drf-task-api && cd drf-task-api
Next, create a virtual environment and activate it:
python3 -m venv venv
source venv/bin/activate
Install Django and the Django Rest Framework:
pip install django djangorestframework
Here's a breakdown of these packages:
Django
– The web framework that forms the foundation.Django Rest Framework
– The toolkit for building REST APIs with Django.
After that, create a new Django project:
django-admin startproject taskmanager .
Next, create a new Django app for handling tasks:
python manage.py startapp tasks
Now, update the settings.py
file to include Django Rest Framework and your new app. Open taskmanager/settings.py
and find the INSTALLED_APPS
list:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
# Local apps
'tasks',
]
# Rest of settings file...
Next, configure Django Rest Framework by adding some settings at the bottom of the settings.py
file:
# ... existing settings ...
# Django Rest Framework settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
This configuration sets up some basic defaults:
DEFAULT_PERMISSION_CLASSES
determines who can access your API. We're starting withAllowAny
for simplicity.DEFAULT_PAGINATION_CLASS
andPAGE_SIZE
configure automatic pagination of results, limiting responses to 10 items per page.
The default database configuration in settings.py
uses SQLite, perfect for this tutorial. The configuration looks like this:
# ... other settings ...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# ... more settings ...
With that, run initial migrations to set up your database:
python manage.py migrate
You should see output similar to:
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
...
Now run the development server:
python manage.py runserver
You should see output similar to:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
March 06, 2025 - 10:39:15
Django version 5.1.6, using settings 'taskmanager.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.`
Your Django application is now running with SQLite as the database. If you navigate to http://127.0.0.1:8000/ in your browser, you should see the default Django welcome page:
With Django now running, you'll define the data model for tasks in the next step.
Step 2 — Creating the Task model
With the Django project set up, you will define a model for your tasks. Django models specify your application's data's database schema and business logic.
Start by opening the tasks/models.py
file generated when you created your app. Modify this file to define the Task
model:
import uuid
from django.db import models
class Task(models.Model):
"""Model representing a task in the system."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200, db_index=True)
description = models.TextField(blank=True, null=True)
priority = models.IntegerField(default=1)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
"""String representation of the task."""
return self.title
This model includes:
- A UUIDField
for unique task IDs.
- A CharField
for titles, indexed for faster searches.
- A TextField
for optional descriptions.
- An IntegerField
for priority, defaulting to 1.
- A BooleanField
to track completion status.
- DateTimeFields
for creation and update timestamps.
- Default ordering by newest tasks first.
Now that you've defined your model, you need to create and apply a database migration. Migrations are Django's way of tracking and applying changes to your database schema.
First, create a migration file:
python manage.py makemigrations
You should see output similar to:
Migrations for 'tasks':
tasks/migrations/0001_initial.py
+ Create model Task
This creates a new file in the tasks/migrations
directory that describes the changes to be made to the database.
Next, apply the migration to create the database table:
python manage.py migrate
You should see output that includes:
Migrations for 'tasks':
tasks/migrations/0001_initial.py
+ Create model Task
(venv) stanley@MACOOKs-MacBook-Pro drf-task-api % python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, tasks
Running migrations:
Applying tasks.0001_initial... OK
Verify your model is working correctly by creating a task through Django's shell:
python manage.py shell
In the shell, paste these commands:
from tasks.models import Task
task = Task(title="Test Task", description="This is a test task", priority=3)
task.save()
print(f"Created task: {task.id} - {task.title}")
exit()
You should see a message confirming the task was created, with a UUID and the title "Test Task":
>>> from tasks.models import Task
>>> task = Task(title="Test Task", description="This is a test task", priority=3)
>>> task.save()
print(f"Created task: {task.id} - {task.title}")
exit()
>>> print(f"Created task: {task.id} - {task.title}")
Created task: 82f3534a-4f8a-4c82-a7d2-69dfd6758c58 - Test Task
>>> exit()
now exiting InteractiveConsole...
Now, register your model with Django's admin interface so you can easily view and edit tasks:
from django.contrib import admin
from .models import Task
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ('title', 'priority', 'completed', 'created_at')
list_filter = ('completed', 'priority')
search_fields = ('title', 'description')
readonly_fields = ('id', 'created_at', 'updated_at')
This sets up an admin interface for the Task model with:
- A customized list view showing key fields
- Filters for completion status and priority
- Search capability across title and description
- Protection for system-managed fields like IDs and timestamps
To access the admin interface, you'll need to create a superuser:
python manage.py createsuperuser
Follow the prompts to create an admin account, then start the development server again:
python manage.py runserver
You can visit http://127.0.0.1:8000/admin/
and log in with your superuser credentials to view and manage tasks:
Now that you have defined and worked on your data model, the next step is creating serializers to convert Django models and JSON representations suitable for your API.
Step 3 — Creating serializers for the Task model
Django Rest Framework uses serializers to convert complex data types, like Django model instances, to Python data types easily rendered into JSON, XML, or other content types. Serializers also handle deserialization, allowing parsed data to be converted into complex types after validation.
To create serializers for your Task model, first create a new file in the tasks app:
touch tasks/serializers.py
Now, open the newly created serializers.py
file and add the following code:
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
"""Serializer for the Task model."""
class Meta:
model = Task
fields = ['id', 'title', 'description', 'priority', 'completed', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
def validate_priority(self, value):
"""Validate that priority is between 1 and 5."""
if value < 1 or value > 5:
raise serializers.ValidationError("Priority must be between 1 and 5.")
return value
This serializer automatically generates fields based on the Task
model and ensures that specific fields, such as id
, created_at
, and updated_at
, remain read-only. It also includes custom validation to restrict the priority value between 1 and 5.
While you could use this single serializer for all operations, it's often better to create specialized serializers for different actions.
Add two more serializers to handle creating and updating tasks specifically:
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
"""Serializer for the Task model."""
...
class TaskCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating tasks."""
class Meta:
model = Task
fields = ['title', 'description', 'priority', 'completed']
def validate_priority(self, value):
"""Validate that priority is between 1 and 5."""
if value < 1 or value > 5:
raise serializers.ValidationError("Priority must be between 1 and 5.")
return value
class TaskUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating tasks."""
class Meta:
model = Task
fields = ['title', 'description', 'priority', 'completed']
extra_kwargs = {
'title': {'required': False},
'description': {'required': False},
'priority': {'required': False},
'completed': {'required': False},
}
def validate_priority(self, value):
"""Validate that priority is between 1 and 5."""
if value < 1 or value > 5:
raise serializers.ValidationError("Priority must be between 1 and 5.")
return value
These serializers serve specific purposes:
TaskCreateSerializer
is used to create new tasks, including only relevant fields and enforce priority validation.TaskUpdateSerializer
is designed for updates, making all fields optional so clients can modify only what they need.
You can test these serializers in the Django shell:
python manage.py shell
In the shell, try converting a task to and from JSON by paste the following:
from tasks.models import Task
from tasks.serializers import TaskSerializer
from rest_framework.renderers import JSONRenderer
# Get a task from the database
task = Task.objects.first()
# Serialize the task
serializer = TaskSerializer(task)
print(serializer.data)
# Convert to JSON
json_data = JSONRenderer().render(serializer.data)
print(json_data)
# Exit the shell
exit()
The output should include the serialized task data in Python dict format, followed by the JSON representation:
>>> from rest_framework.renderers import JSONRenderer
....
>>>
>>> # Get a task from the database
>>> task = Task.objects.first()
>>>
>>> # Serialize the task
>>> serializer = TaskSerializer(task)
>>> print(serializer.data)
{'id': '82f3534a-4f8a-4c82-a7d2-69dfd6758c58', 'title': 'Test Task', 'description': 'This is a test task', 'priority': 3, 'completed': False, 'created_at': '2025-03-06T10:50:40.845868Z', 'updated_at': '2025-03-06T10:50:40.846292Z'}
>>>
>>> # Convert to JSON
>>> json_data = JSONRenderer().render(serializer.data)
>>> print(json_data)
b'{"id":"82f3534a-4f8a-4c82-a7d2-69dfd6758c58","title":"Test Task","description":"This is a test task","priority":3,"completed":false,"created_at":"2025-03-06T10:50:40.845868Z","updated_at":"2025-03-06T10:50:40.846292Z"}'
>>>
>>> # Exit the shell
>>> exit()
now exiting InteractiveConsole...
The output demonstrates serializing a Task
instance using Django Rest Framework. First, it retrieves a task from the database and serializes it into a Python dictionary.
Then, the data is converted into a JSON string using JSONRenderer
. The final output includes the dictionary and JSON representation, which are ready for API responses.
Your serializers are now ready to convert Django models and API responses. In the next step, you'll implement views and URLs to expose your API endpoints.
Step 4 — Implementing views and URLs for task operations
Django Rest Framework provides several types of views to handle API requests. In this step, you'll implement ViewSets
, which combine the logic for multiple related operations into a single class, simplifying your API implementation.
Start by creating the views for your task operations. Open the tasks/views.py
file and replace its contents with:
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Task
from .serializers import TaskSerializer, TaskCreateSerializer, TaskUpdateSerializer
class TaskViewSet(viewsets.ModelViewSet):
"""ViewSet for handling Task CRUD operations."""
queryset = Task.objects.all()
def get_serializer_class(self):
"""Return appropriate serializer class based on the request method."""
if self.action == 'create':
return TaskCreateSerializer
elif self.action in ['update', 'partial_update']:
return TaskUpdateSerializer
return TaskSerializer
def get_queryset(self):
"""Filter queryset based on query parameters."""
queryset = Task.objects.all()
# Filter by completion status if provided
completed = self.request.query_params.get('completed', None)
if completed is not None:
completed = completed.lower() == 'true'
queryset = queryset.filter(completed=completed)
return queryset
This TaskViewSet
serves as the main entry point for handling task-related API operations. It retrieves all tasks by default but allows filtering based on completion status when a query parameter is provided.
The get_serializer_class()
method dynamically selects the appropriate serializer depending on the action. When creating a new task, it uses TaskCreateSerializer
, while updates rely on TaskUpdateSerializer
.
For all other actions, such as listing or retrieving tasks, it defaults to TaskSerializer
.
Next, you must set up the URL patterns to expose your API endpoints. Create the tasks/urls.py
file with the following:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet
# Create a router and register our viewset
router = DefaultRouter()
router.register(r'tasks', TaskViewSet)
# The API URLs are now determined automatically by the router
urlpatterns = [
path('', include(router.urls)),
]
The DefaultRouter
automatically creates the following URL patterns:
GET /tasks/
- List all tasks (with optional filtering)POST /tasks/
- Create a new taskGET /tasks/{id}/
- Retrieve a specific taskPUT /tasks/{id}/
- Update a task completelyPATCH /tasks/{id}/
- Update a task partiallyDELETE /tasks/{id}/
- Delete a task
Finally, update the main URLs file to ensure your app's URLs are included in the project. Open taskmanager/urls.py
and make sure it includes the tasks app URLs:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('tasks.urls')),
]
With your views and URLs configured, your API is now ready for testing. Start the development server:
python manage.py runserver
Now you can test your API using curl
or any API client.
You can create a new task by sending a POST request to the /api/tasks/
endpoint:
curl -X POST http://127.0.0.1:8000/api/tasks/ \
-H "Content-Type: application/json" \
-d '{"title":"Learn Django Rest Framework","description":"Complete the DRF tutorial and build a project","priority":2}' \
| python3 -m json.tool
You should receive a response with the newly created task:
{
"title": "Learn Django Rest Framework",
"description": "Complete the DRF tutorial and build a project",
"priority": 2,
"completed": false
}
To retrieve a list of all tasks, send a GET request to the /api/tasks/
endpoint:
curl http://127.0.0.1:8000/api/tasks/ | python3 -m json.tool
This should return a paginated response with all your tasks:
[
{
"id": "792235cd-0406-4ffe-a6b7-0ad825e5b02d",
"title": "Learn Django Rest Framework",
"description": "Complete the DRF tutorial and build a project",
"priority": 2,
"completed": false,
"created_at": "2025-03-06T11:13:16.390528Z",
"updated_at": "2025-03-06T11:13:16.390594Z"
},
{
"id": "82f3534a-4f8a-4c82-a7d2-69dfd6758c58",
"title": "Test Task",
"description": "This is a test task",
"priority": 3,
"completed": false,
"created_at": "2025-03-06T10:50:40.845868Z",
"updated_at": "2025-03-06T10:50:40.846292Z"
}
]
Retrieve a specific task (replace with your actual task ID):
curl "http://127.0.0.1:8000/api/tasks/<your_task_id>/" | python3 -m json.tool
Remember to replace <your_task_id>
with an actual UUID from your database. You should receive a response with the requested task:
{
"id": "792235cd-0406-4ffe-a6b7-0ad825e5b02d",
"title": "Learn Django Rest Framework",
"description": "Complete the DRF tutorial and build a project",
"priority": 2,
"completed": false,
"created_at": "2025-03-06T11:13:16.390528Z",
"updated_at": "2025-03-06T11:13:16.390594Z"
}
Update an existing task by sending a PUT request to the /api/tasks/<task_id>/
endpoint:
curl -X PUT "http://127.0.0.1:8000/api/tasks/<your_task_id>/" \
-H "Content-Type: application/json" \
-d '{"title":"Update API Documentation","description":"Document the API endpoints, models, and authentication","priority":4,"completed":true}' \
| python3 -m json.tool
{
"title": "Update API Documentation",
"description": "Document the API endpoints, models, and authentication",
"priority": 4,
"completed": true
}
Remove a task by sending a DELETE request to the /api/tasks/<task_id>/
endpoint:
curl -X DELETE "http://127.0.0.1:8000/api/tasks/792235cd-0406-4ffe-a6b7-0ad825e5b02d/" -v
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> DELETE /api/tasks/792235cd-0406-4ffe-a6b7-0ad825e5b02d/ HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 204 No Content
< Date: Thu, 06 Mar 2025 11:28:26 GMT
< Server: WSGIServer/0.2 CPython/3.13.2
....
* Connection #0 to host 127.0.0.1 left intact
The 204
status code indicates the task was successfully deleted, and no content is returned in the response body.
Django Rest Framework also provides a browsable API you can access through your web browser. Visit http://127.0.0.1:8000/api/tasks/ to see and interact with your API directly in the browser:
You've implemented a fully functional API with endpoints for creating, reading, updating, and deleting tasks.
Final thoughts
This tutorial has walked you through creating a task management API with Django Rest Framework and SQLite. Django Rest Framework's elegant, Pythonic design makes it ideal for API development.
For more insights into advanced topics like throttling, permissions, and authentication, check the official docs for Django, and Django Rest Framework.
Thanks for reading, and happy coding!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github